СУБД PostgreSQL сейчас вероятно, является самым популярным бакендом для Django. К сожалению, у этой СУБД есть особенность - для построения индексов, она блокирует таблицу, и на время выпололнения индексирования, запись в эту таблицу оказывается недоступной, что приводит к частичной или полной неработоспособности приложения на время выполнения миграции, включающей в себя построение индекса.
Эта особенность создает проблемы с маштабированием приложения при изменении структуры данных - таблицы с большим количеством строк могут индексироваться заметное время - от нескольких минут, до часов. Все это время, приложение ограничено в функциональности, что может быть для него недопустимо.
Как решать эту проблему - описано в данной статье.
Что предлагает PostgreSQL
СУБД PostgreSQL поддерживает расширенный синтаксис команды CREATE INDEX CONCURRENTLY, который позволяет выполнять индексирование параллельно с записью в таблицу. Существенным ограничением этой модификации команды является то, что она может быть выполнена только вне транзакции.
Для разработчика Django это означает, что данная команда не может быть включена в стандартно сгенерированную миграцию, которая обычно выполняется внутри транзакции.
Что предлагает Django
В качестве расширения для PostgreSQL, Django предлагает использовать специальную операцию AddIndexConcurrently. Эта операция генерирует нужный синтаксис CREATE INDEX CONCURRENTLY для выполнения миграции.
Однако, из за ограничения транзакционности, она не может быть включена в обычную миграцию, поэтому ее нужно выносить в отдельную, не-транзакционную миграцию.
Такая не-транзакционная миграция может прерваться на середине, например из за нарушения уникальности данных, оставив артефакты своего частичного выполнения в структуре БД, что может помешать ее повторному выполнению. В частности, при неудачном выполнении CREATE INDEX CONCURRENTLY, частично созданный индекс останется присутствовать в БД в невалидном состоянии, что потребует ручного удаления этого индекса из БД, чтобы выполнить эту команду снова.
Операция AddIndexConcurrently может заменить AddIndex для конструирования индекса. Но некоторые другие операции также приводят к конструированию индекса:
AlterField, если у поля появляется атрибутdb_indexилиuniqueAlterIndexTogetherиAlterUniqueTogetherAddConstraint(...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 CONCURRENTLYALTER TABLE CREATE CONSTRAINT UNIQUEна две команды:CREATE UNIQUE INDEX CONCURRENTLYALTER 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
Счастливого программирования!
