СУБД PostgreSQL сейчас вероятно, является самым популярным бакендом для Django. К сожалению, у этой СУБД есть особенность - для построения индексов, она блокирует таблицу, и на время выпололнения индексирования, запись в эту таблицу оказывается недоступной, что приводит к частичной или полной неработоспособности приложения на время выполнения миграции, включающей в себя построение индекса.

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

Как решать эту проблему - описано в данной статье.


Что предлагает PostgreSQL

СУБД PostgreSQL поддерживает расширенный синтаксис команды CREATE INDEX CONCURRENTLY, который позволяет выполнять индексирование параллельно с записью в таблицу. Существенным ограничением этой модификации команды является то, что она может быть выполнена только вне транзакции.

Для разработчика Django это означает, что данная команда не может быть включена в стандартно сгенерированную миграцию, которая обычно выполняется внутри транзакции.

Что предлагает Django

В качестве расширения для PostgreSQL, Django предлагает использовать специальную операцию AddIndexConcurrently. Эта операция генерирует нужный синтаксис CREATE INDEX CONCURRENTLY для выполнения миграции.

Однако, из за ограничения транзакционности, она не может быть включена в обычную миграцию, поэтому ее нужно выносить в отдельную, не-транзакционную миграцию.

Такая не-транзакционная миграция может прерваться на середине, например из за нарушения уникальности данных, оставив артефакты своего частичного выполнения в структуре БД, что может помешать ее повторному выполнению. В частности, при неудачном выполнении CREATE INDEX CONCURRENTLY, частично созданный индекс останется присутствовать в БД в невалидном состоянии, что потребует ручного удаления этого индекса из БД, чтобы выполнить эту команду снова.

Операция AddIndexConcurrently может заменить AddIndex для конструирования индекса. Но некоторые другие операции также приводят к конструированию индекса:

  • AlterField, если у поля появляется атрибут db_index или unique

  • AlterIndexTogether и AlterUniqueTogether

  • AddConstraint(...constraint=UniqueConstraint(...))

Эти операции не имеют своих "конкурентных" аналогов в стандартной поставке Django.

Пакет Django Postpone Index

Пакет django-postpone-index позволяет отложить конструирование всех найденных им индексов на момент после миграции. Он перехватывает выполнение команд SQL и исключает те из них, которые приводят к конструированию индекса, сохраняя эти команды в отдельной таблице.

Сохраненные команды преобразуются в их "конкурентные" аналоги и выполняются отдельной командой python manage.py apply_postponed run после выполнения очередных миграций.

Миграции могут выполняться как пакетом, так и отдельно - пакет django-postpone-index следит за тем, чтобы устаревшие команды конструирования индексов удалялись или модифицировались в соответствие с новой структурой данных и индексов, объявленной в очередной миграции.

Все команды SQL, приводящие к созданию индексов, генерируемые AddIndex, AlterField, AlterIndexTogether, AlterUniqueTogether, AddConstraint и даже переданные для исполнения RunSQL, перехватываются и вместо выполнения, сохраняются в отдельной таблице отложенных команд SQL.

Перехватываются также некоторые другие команды и внутренние вызовы миграции, приводящие к изменениям структуры данных, которые требуют изменения или отмены отложенных команд.

Команда python manage.py apply_postponed run перечитывает таблицу отложенных команд SQL и выполняет их, заменяя на "конкурентный" аналог:

  • CREATE INDEX на CREATE INDEX CONCURRENTLY

  • ALTER TABLE CREATE CONSTRAINT UNIQUE на две команды:

    • CREATE UNIQUE INDEX CONCURRENTLY

    • ALTER TABLE CREATE CONSTRAINT UNIQUE USING INDEX

Все успешно выполненные команды помечаются как выполненные и остаются в таблице отложенных команд SQL, которую можно очистить от таких записей с помощью python manage.py apply_postponed cleanup.

Конкретное заполнение таблиц данными может препятствовать созданию индекса. Например, если в таблице найдутся дубликаты данных для уникального индекса, "конкурентное" создание такого индекса прервется с ошибкой целостности данных.

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

Исправив данные, вы можете повторно запустить команду apply_postponed run - она найдет все невыполненные команды SQL и попытается выполнить их "конкурентный" аналог еще раз.

Установите пакет django-postpone-index и выполните следующие изменения в вашем проекте Django:

  • добавьте пакет в settings.INSTALLED_APPS

  • установите параметр ENGINE для вашей БД PostgreSQL в одно из следующих значений:

    • 'postpone_index.contrib.postgres' - для обычной БД

    • 'postpone_index.contrib.postgis' - если используете GeoDjango

    • оставьте прежним, если используете свой кастомизированный ENGINE

  • если используете свой кастомизированный ENGINE:

    • модифицируйте Ваш DatabaseSchemaEditor если у вас свой кастомизированный DatabaseSchemaEditor - вставьте в начало списка его базовых классов специальный миксин pospone_index.contrib.postgres.schema.DatabaseSchemaEditorMixin

    • иначе, установите атрибут SchemaEditorClass вашего DatabaseWrapper в одно из следующих значений:

      • pospone_index.contrib.postgres.schema.DatabaseSchemaEditor - для обычной БД

      • pospone_index.contrib.postgis.schema.DatabaseSchemaEditor - для GeoDjango

Счастливого программирования!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы уже встречались с проблемой блокировки записи в таблицу при индексировании в PostgreSQL?
0%Да, мы понесли убытки, потому что у нас (или у клиентов) обновление шло слишком долго и ничего не работало0
50%Да, обновления с индексированием идут долго, но мы не спешим (запускаем на ночь), это не критично для нас2
0%Да, но у нас пока не слишком много данных, так что можно потерпеть0
0%Да, но я заранее использовал решение (расскажите, какое — в комментарии) и все прошло удачно0
25%Нет, но у нас (наших клиентов) растет размер БД и нам надо эту проблему решить заранее1
25%Нет, буду решать проблемы по мере возникновения1
0%Нет, потому что я… (расскажите в комментарии)0
0%Нет, автор ничего не понимает, потому что… (расскажите в комментарии)0
Проголосовали 4 пользователя. Воздержавшихся нет.