Dmitriy Kononov
Обсудим задачуСвязаться

Новости и разборы

ON DELETE CASCADE, шарды и реплики: три решения в базе, о которых жалеют позже

PlanetScale за неделю написала про каскадное удаление, шард-группы и реплики в Neki. Что из этого касается базы на одном сервере и как я проектирую сам.

PlanetScale за одну неделю выпустила три текста про Postgres: почему не стоит доверять ON DELETE CASCADE, как проектировать шарды и что нового в их шардированном Neki. Шарды компании из тридцати человек не понадобятся ещё долго. А вот каскадное удаление и вопрос «что считать удалённым» касаются любой базы, даже той, что стоит на одной виртуальной машине и хранит заявки, сметы и клиентов.

Что вышло

PlanetScale 21 октября объяснила, почему не доверяет ON DELETE CASCADE. Соблазн понятен: описал внешние ключи, и Postgres сам удаляет дочерние строки. Проблемы приходят позже. Большое каскадное удаление держит блокировки на всех затронутых таблицах, а обычная миграция вроде добавления колонки встаёт за ним в очередь и тянет за собой все новые запросы: у них это закончилось остановкой записи в production. Один такой DELETE в больших средах писал сотни гигабайт WAL за транзакцию. Внешний ключ без индекса превращает удаление одной строки в полный проход по таблице. А при логической репликации подписчик каскады не выполняет, и если у него есть своя дочерняя таблица, в ней остаются сироты без единой ошибки. Их ответ: мягкое удаление. Одна строка получает deleted_at, а настоящие DELETE идут позже, пакетами, в тихие часы, например через 30 дней.

23 октября та же команда разобрала проектирование шардов в Neki. Единица проектирования там shard group: таблицы, которые хешируют ключ одинаково, кладут строки с одним customer_id на один шард, и заказы, позиции и адреса клиента всегда рядом. Справочники вроде стран и валют объявляются reference tables и копируются на каждый шард. Каталог можно шардировать по lower(sku), чтобы AB-1042 и ab-1042 попали в одно место. И одна оговорка: транзакция внутри одного шарда сохраняет семантику Postgres, транзакция через два шарда атомарного коммита не имеет.

20 октября в обзоре обновлений Neki они написали, что за месяц preview добавили read-only реплики для шардов, узлы Metal от $50 в месяц вплоть до 1/8 vCPU, 1 ГБ памяти и 10 ГБ NVMe, запуск в Azure, поддержку материализованных представлений, LISTEN/NOTIFY и курсоров, а COPY TO STDOUT теперь работает на шардированных таблицах, так что pg_dump читает данные. Распределённый INSERT ... SELECT, по их словам, стал примерно в 76 раз быстрее, чем на старте preview.

Зачем это бизнесу

Возьмём прокат автомобилей. Менеджер удаляет клиента, потому что тот попросил, или по ошибке. Если в базе включён каскад, вместе с клиентом уходят его брони, акты выдачи, подписи и фото осмотров. Через месяц приходит спор по повреждению, и доказывать нечем. Если каскада нет, а есть отметка «удалён», клиента не видно в списках, но история цела, и за 30 дней решение можно отменить.

Вывод: в бизнес-системе удаление должно быть статусом, который можно отменить. А шарды и реплики нужны, когда одна машина перестаёт справляться, и до этого момента большинству компаний далеко. Я чаще встречаю базы без индекса на внешнем ключе, чем базы, которым тесно на одном сервере.

Как это устроено у меня

MariaDB стоит почти во всех моих проектах, и каскадное удаление я предпочитаю не включать: связи описаны, а что считать удалённым, решает логика на сервере.

У производственной компании в номенклатуре десятки тысяч позиций, и программа расчёта деталей и материалов сверяет их с 1С. Если позиции нет в номенклатуре, не хватает спецификации или файла документации, программа не пропускает это молча: пометка видна в результате. Это та же история про сирот из текста PlanetScale, решённая на уровне правил. Один заказ программа обрабатывает в среднем за 65 секунд, раньше на это уходило 4–5 часов, и шардов для этого не понадобилось.

В PDR-компании в США в 2023 году я предложил перейти с Google Таблиц на SQL-базу ради скорости и единого формата данных. Идею поддержали, и после перехода на MariaDB мы подключали сервисы один за другим: парсеры документов, телефонию, leaderboard. Каждый звонок там записывается, а каждый настоящий лид проходит воронку до конца, и конверсия выросла на 30 %. Строить такое на каскадном удалении я бы не стал: лид с записью звонка и документами нужен потом для отчётов, даже если сделка закрыта или клиент ушёл.

Neki и его шард-группы у себя я не проверял: ни у одного моего заказчика данные не выросли до уровня, где одна база на Debian в Google Cloud перестаёт справляться.

Что я бы сделал

Посмотреть, есть ли в базе ON DELETE CASCADE. Если есть, выписать цепочку таблиц, которая удалится за одной строкой, и показать её владельцу бизнеса. Обычно на этом каскад и выключают.

Ввести мягкое удаление для клиентов, заказов и документов: поле deleted_at и фильтр в запросах. Физическую чистку запускать отдельной задачей, пакетами, в нерабочие часы.

Проверить индексы на внешних ключах. Это одна команда и часто самая дешёвая починка медленной базы.

Про шарды не думать, пока одна машина справляется. Если справляться перестаёт, сначала реплика на чтение для отчётов, и только потом разговор про шард-группы.

Как сервер держит правила и состояние системы, я писал в статье Сервер: данные, доступ и выполнение процесса. Направления: внутренние системы и обработка данных на Python. Кейсы: расчёт заказа для производства и технический партнёр PDR-компании. По той же теме: миграции схемы без страха и бэкап, который никто не восстанавливал. Раньше я писал про тяжёлые запросы и большие значения в базе. Если в вашей базе что-то удаляется каскадом и вы не уверены, что именно, расскажите, как она устроена: Обсудить задачу.