Хабрахабр

Типичные ошибки при работе с PostgreSQL

Чуть более месяца назад в Москве состоялась крупнейшая конференция постгресового сообщества PGConf.Russia 2019, собравшая в МГУ свыше 700 человек. Мы решили выложить видео и расшифровку лучших докладов. Выступление Ивана Фролкова с разбором типичных ошибок при работе с PostgreSQL было отмечено лучшим на конференции, поэтому мы начнем с него.

В этой статье речь пойдет о непоследовательном именовании, о constraints, о том, где лучше сосредоточить логику — в базе или в приложении. Для удобства мы разбили расшифровку на две части. Во второй части будут разобраны обработка ошибок, конкурентный доступ, неотменяемые операции, CTE и JSON.

Насмотрелся я приложений самых разных. В нашей компании я занимаюсь поддержкой клиентов по вопросам, связанным с приложениями, то есть помогаю в случаях проблем с соединениями, с оптимизацией запросов и прочими подобными вещами. Может быть даже больше, чем хотелось бы. Чего я только не видел! Часть из того, что я буду рассказывать, относится не только к PostgreSQL, а к любой базе, но кое-что прежде всего к PostgreSQL.

Был замечательный проект (я не могу упоминать все компании, с которыми мы работали), в котором еще более замечательное приложение создавало таблицы миллионами. Главный вывод, который я смог сделать из того, что я видел, довольно неожиданный: фактически любое приложение при должной настойчивости можно заставить работать. На выходные дни запускают VACUUM FULL, и в понедельник она опять работает хорошо. Выглядело это так: в понедельник система работает неплохо, а уже в пятницу она практически не работает. Другой товарищ сделал странную вещь: у него всё было построено на триггерах, процедур не было вообще. Оказывается, над PostgreSQL можно вот так издеваться, и всё это довольно долго будет жить и работать. Если я повторно вкачу данные, она сломается. То есть большую часть таблиц трогать нельзя, сделать что-либо не получалось, но и эта база жила.
Он объяснял это так: «база переходит из одного консистентного состояния в другое консистентное. Подход дикий, но в то же время некоторый смысл в этом есть. Но так как у меня триггеры и уникальный ключ, я данные повторно вкатить не могу». Первая ошибочка, о которой я буду говорить, это: Может, делать надо было по-другому, но учитывать особенности заказчиков тоже надо.

На слайде вы видите, как именовалась одна и та же сущность в разных колонках. Вот реальный пример, с которым я сталкивался. Другие объекты именовались так же непоследовательно. Можно было бы еще и с пробелами. Если у вас есть id_user и user_id в одной таблице, работа начинается с исследования: что бы это всё значило. Если вам что-то нужно взять в другой таблице, то нужно посмотреть, как оно там называется, то же самое ли это.

Сразу скажу, это была не «1С». У других клиентов все объекты именовались так: две буквы, дальше пять цифр. Зачем они так делали — не знаю: никакой логики в этом не прослеживалось, но мое дело оптимизировать запросы.

Это затрудняет понимание и плодит новые ошибки. Еще пример: часть названий на русском, часть на нерусском, но с каким-то русским акцентом. В реальной жизни последовательно именовать, к сожалению, не очень получается — и у меня в том числе. Сам я стараюсь называть колонки так, как будто рассчитываю на сервис, который из этих имен колонок будет автоматически делать нормальные названия столбцов в каком-нибудь отчете. Но стремиться надо. Особенно это сложно при коллективной разработке.

Вы сможете написать запрос и выбрать, скажем, все картинки — вообще все картинки — из базы. Еще важная причина именовать последовательно: имена объектов доступны через запросы к метаданным, то есть имена это тоже данные.

Особенно если учесть типичные проблемы с документацией — а по моему опыту документация обычно либо отсутствует, либо неполная, либо неверная, либо и то, и другое: потому что задача написать хорошую документацию по сложности сравнима с задачей написания самого кода. Понятные метаданные — это очень удобно. И логичное, последовательное именование объектов этому способствует, а когда что-то непонятно, приходится писать код сниппета и смотреть, как это работает. Так что лучше, когда код самодокументированный. Один раз это ничего, два — ничего, но когда ты весь день только этим и занимаешься, это выматывает.

Мы ее перенесли в Postgres. Реальный случай: у очень серьезной организации, с которой мы работали, была база — документооборот на Oracle. Их там не было и, к сожалению, наложить нам их не удалось: оказалось что в таблицах очень много каких-то «левых» строк, и что с ними делать, никто не знает, включая заказчика. Одним из условий договора было то, что мы наложим FOREIGN KEYs.

Очень хорошо помогает, когда по договору программист сам оплачивает ошибки, и желательно, чтобы суммы были большие — тогда просветление наступает в течение минут, наверное, пятнадцати. Когда нужно не progress-bar-ы смотреть, а работать с документами на выплату денег, то ситуация грустная. Сразу появляются constraint-ы, сразу всё начинает проверяться.

Особенно если сумма большая. Вы даже не представляете (ну, может, кто-то уже представляет), насколько удобнее разбираться со случаем, когда выплата не прошла, чем когда она прошла, но не туда. Это из личного опыта.

Да, снижают, но, если вы хотите иметь корректные данные, для вас других вариантов просто нет. С другой стороны, часто можно услышать, что constraint-ы снижают производительность. Если у вас приложение, учитывающее количество посещений магазина покупателями, то тогда могут остаться неточности, которые на статистике не особенно скажутся, а если мы считаем деньги, constraint-ы нужны обязательно.

Когда вы будете в дальнейшем обрабатывать ошибку, то по имени constraint-а, вы сможете дать внятное сообщение пользователю, классифицировать ошибку и сообщить, нужно ли повторно пытаться выполнить операцию, или что эту операцию выполнять уже не обязательно, или что повторять ее просто нельзя. Имена constraint-ы обычно генерируются ORM-ом или системой, и именованием constraint-ов обычно никто специально не заморачивается — а зря!

Дело в том, что рано или поздно вы полезете что-то менять в коде, и очень может быть, что одну из проверок вы сломаете. Еще одна вещь, которую я тоже не встречал, но которую очень рекомендую: на все важные финансовые (и не только финансовые) операции проверок должно быть как минимум две. Если сделаете три — тоже неплохо. Тогда вас спасет вторая.

На клиенте или на сервере? Часто возникает вопрос: где проверять корректность данных. У вас ошибка в клиенте, тогда сервер не
пропустит, или у вас ошибка на сервере, тогда хотя бы клиент поможет отследить ее. По-моему, очевидно, что проверять нужно и там, и там. Вопрос несколько дискуссионный, и мы плавно переходим к теме: где держать логику базы: в приложении или в базе?

Если у вас логика в компилируемом коде, то вам нужно собрать, задеплоить, посмотреть, что получилось. В базе удобно потому, что, по моему опыту, бизнес регулярно выдает срочные правки: сию секунду убрать или вставить то-то и то-то. В базе это делать удобней. Часто это уже просто невозможно. Процентов 80 серверного кода написаны совершенно в процедурном стиле: у нас есть функция «получить_юзера()» и она возвращает тип «юзер», а если «получить_список_юзеров()», то она возвращает массив «юзеров». Но есть известный афоризм: опытные программисты на фортране пишут на фортране на любом языке. На Java такие вещи писать действительно удобнее, чем на SQL или pgsql.

Вы просто берете его в таблице или в представлении. С другой стороны: зачем вам функция «получить_юзера()»? Тут важно, во-первых, четко определиться с какими данными мы работаем: если данные у нас мусорные или полумусорные, то и результат будет соответствующий, и убиваться, наверное, особо не следует. Раз у вас реляционная база, то и писать надо, как мне кажется, реляционно. Повторю: лучше не выполнить операцию, чем выполнить ее неправильно. Если данные для нас важны, если это деньги, имущество или юридические операции, то нужны constraint-ы и чем больше, тем лучше. И не надо писать процедурный код в реляционной базе: сильно пожалеете.

Видимо, им удалось создать «красивую и сложную» схему БД. Я видел таблицу с 30 тысяч строк (товары), в которой запрос «покажите список актуальных товаров» выполнялся около секунды. Если у вас какой-нибудь магазин или обычное приложение для учета людей, то вряд ли там есть очень сложные взаимоотношения между сущностями. Лично я считаю, что, если вы делаете что-то сильно мудреное, то, скорее всего, вы либо делаете что-то не так, либо у вас действительно очень, очень сложная задача.

Очень большие, пухлые базы становятся такими обычно из-за архивов. Когда я начинал свой профессиональный путь, таблица в DBF-файле в 60 мегабайт в банковской системе казалась очень большой, а сейчас 60 мегабайт это вообще ничто — железо стало лучше, софт стал лучше, всё работает быстрее, но остается вопрос: откуда у вас столько данных? Архив скорее всего не меняется, и большинство возможностей СУБД для работы с ним вообще не нужны. В любой СУБД и в PostgreSQL много усилий потрачено на то, чтобы обеспечить консистентную конкурентную работу приложений. Стоит подумать о вынесении его наружу СУБД.

Но тут сама постановка вопроса странная: положить данных в базу можно сколько угодно, сколько хватит места на диске, столько и будет лежать. То и дело с этаким комиссарским прищуром задают вопрос: а потянет ли PostgreSQL базу такого-то объема. Я сильно подозреваю, что хотя бы частично эти требования к объемам связаны с желанием продавцов оборудования продать вам побольше. Вопрос в том, как, например, бэкапить архивов в петабайт, куда вы положите полный backup и сколько вы будете его снимать.

Скорее всего такие файлы будут вообще readonly. Если вы храните документы в базе, вы их там обрабатывать вряд ли будете: экселевскую таблицу можно, конечно, модифицировать и на сервере, но это странное занятие. В конце концов можно хранить цифровую подпись таблицы — чтобы ее не поменяли (если вы решите соответствующие законодательные вопросы). Лучше хранить ссылки на документы, а сами в каком-то внешнем хранилище.

Если вы не храните в ней видео, конечно. Еще такое наблюдение: если у вас не какой-то мега-мега бизнес, не какое-то, скажем, федеральное предприятие, то вряд ли у вас база сильно большая.

Баз без индексов я не встречал, зато довольно часто встречал базы, где несколько индексов на одни и те же колонки в одном и том же порядке. Еще одна причина того, что база большая — ненужные индексы. Когда вы создаете индекс, пожалуйста, посмотрите, не дублирует ли он уже имеющийся. База это позволяет сделать. Может, он вообще не требуется. Посмотреть, какие индексы не нужны, можно, заглянув в pg_stat_user_indexes, чтобы понять насколько активно индекс используется.

Во всех СУБД большие таблицы лучше секционировать, но в PostgreSQL это особенно актуально из-за нашего любимого VACUUM-а. Я натыкался и на ситуации (кстати, типичные), когда очень большая таблица не секционирована. Может быть начиная с 50. Я бы посоветовал секционировать таблицы начиная, наверное, со 100 гигабайт. Но это многовато, лучше было бы их порезать. Я видел и несекционированные терабайтные таблицы, и они жили, правда, на SSD.

Живые, меняющиеся данные попадаются в таких базах редко. И еще одно наблюдение: практически все базы большого объема это архивы append only. И, кстати, можно к нему же из базы обеспечить доступ. Определитель с тем, что у вас — если архив, то можно подумать о том, как его вынести куда-то. Тогда и приложение менять не надо: для него ничего не изменится.

Часто, во-первых, есть унаследованный код. Некоторые из этих наблюдение из разряда «лучше быть богатым и здоровым, чем бедным и больным». Но тем не менее: не надо сильно мудрить. Во-вторых, что-то неожиданное произошло, о чем-то не подумали, и получается что все не так красиво, как хотелось бы. Помните, что если вы сильно мудрите, то, скорее всего, делаете что-то не то.

[Продолжение следует.]

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть