Если одновременно выполняется много операций по изменению схемы БД, сервис не может корректно работать на запись. Разработчик Владимир Колясинский объяснил, какие операции в PostgreSQL требуют длительных блокировок и как команда Яндекс.Коннекта обеспечивает почти стопроцентную доступность сервиса на запись во время выполнения подобных операций. Кроме того, вы узнаете о библиотеке для Django, которая призвана автоматизировать часть описанных процессов.


У нас большие нагрузки, тысячи RPS, и простой в несколько минут, не говоря о большем времени, недопустим. Нужно, чтобы миграции происходили незаметно для пользователя. А с такими нагрузками уже не получится встать в четыре часа ночи, что-то накатить, когда нет нагрузки, и снова лечь спать — потому что нагрузка идет круглые сутки.

— Всем добрый вечер! Меня зовут Владимир, я работаю в Яндексе пять лет. Последние два года занимаюсь разработкой внутренних сервисов и сервисов для организаций.

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

Некоторое время назад мы решили, что сервисы у нас классные и что они могут быть полезны не только внутри Яндекса, но и людям снаружи. Мы начали выводить их на единую платформу Яндекс.Коннект, добавив туда существующие внешние сервисы, такие как Почта для домена.



Я в настоящий момент занимаюсь разработкой Конструктора форм и Вики. Используемый стек — это в основном сервисы, написанные на Python второй и третьей версии; Django 1.9-1.11. Как БД в большинстве своем PostgreSQL. Также это Celery с MongoDB и SQS в качестве брокеров. Все это работает в Docker.

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

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

Сначала рассмотрим, какие операции с PostgreSQL требуют длительных блокировок на таблице. Под блокировкой я имею в виду любой тип lock'а, который препятствует нормальной работе с таблицей — будь то access exclusive, который препятствует и записи, и чтению, или более слабые уровни блокировки, которые препятствуют только записи.

Дальше посмотрим, как можно избежать блокировок во время выполнения таких операций. Потом поговорим о том, какие операции с PostgreSQL изначально быстрые и не требуют долгих блокировок. И в конце поговорим о нашей библиотеке zero_downtime_migrations, которую мы используем для автоматизации некоторых описанных ранее приемов избежания долгих блокировок.

Операции, которые требуют долгой блокировки:



Создание индекса. По умолчанию она не блокирует операции чтения в таблице, но все операции записи будут заблокированы на все время создания индекса, соответственно, сервис будет read only.

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

Про изменение типа колонки — произойдет похожее, табличка будет также перезаписана заново. Следует отметить, что это не только занимает долгое время на больших таблицах, но еще и кратковременно требует до двукратного объема свободной памяти от занимаемого таблицей.

Также операция VACUUM FULL требует аналогичного уровня блокировки, как и предыдущие операции — это access exclusive. VACUUM FULL также заблокирует все операции чтения и записи в таблицу.

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





Создание индекса. Тут достаточно просто, его можно создавать с применением ключевого слова CONCURRENTLY. В чем отличие? Эта операция будет занимать больше времени, так как будет выполнен не один, а несколько проходов по таблице, а также она будет дожидаться завершения всех текущих операций, которые потенциально могут изменять индекс. И также она может закончиться неудачей — например, если при создании уникального индекса обнаружится нарушение условий уникальности. Тогда индекс пометится как невалидный, и его надо будет удалить и создать заново. Команду REINDEX использовать не рекомендуется, так как она работает так же, как обычный CREATE INDEX, то есть заблокирует таблицу на запись.

Касательно удаления индекса — начиная с версии 9.3 удалять индекс тоже можно CONCURRENTLY для избежания блокировок во время его удаления, хотя в целом это и так быстрая операция.



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

Как можно ее переписать, чтобы избежать перезаписи таблицы? Сначала в одной транзакции добавляем новую колонку без значения по умолчанию, и отдельным запросом добавляем значение по умолчанию. В чем тут будет разница? Когда к существующей колонке мы добавляем значение по умолчанию, это не приводит к изменению существующих данных в таблице. Меняются только метаданные. То есть для всех новых строчек это значение по умолчанию уже будет гарантироваться. Нам остается обновить все существующие строчки, которые были в таблице на момент выполнения этой команды. Что мы и будем делать пачками по несколько тысяч экземпляров, чтобы не блокировать надолго большой объем данных.

После того, как все данные мы обновили, остается только выполнить SET NOT NULL, если мы создаем NOT NULL-колонку. Если не создаем, то не надо. Таким образом можно избегать перезаписи таблицы при выполнении такого рода изменений.

Такая последовательность команд занимает больше времени, чем выполнение обычной команды, так как зависит от размера таблицы и количества индексов в ней, а обычная команда просто блокирует все операции и перезаписывает таблицу независимо от нагрузки, так как нагрузки нет в этот момент. Но это не имеет такого большого значения, так как во время операции таблица доступна и на чтение, и на запись. Занимает долго, просто нужно за этим следить и все.



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

После этого остается в одной транзакции удалить триггер, удалить старую колонку и переименовать старую колонку в новую. Таким образом добились такого же результата: поменяли тип колонки, при этом блокировка таблицы не была долгой.



Про добавление уникальной колонки. Берется блокировка на время создания. Ее можно избежать, если знать, что уникальность в PostgreSQL гарантируется за счет построения уникального индекса. Можем сами построить требуемый уникальный индекс с применением CONCURRENTLY. И после построения данного индекса создать CONSTRAINT, используя данный индекс. После этого этого определение изначального индекса из таблицы пропадет, и результат, который покажет нам определение таблицы, будет ничем не отличаться после выполнения этих двух операций.



И в целом при добавлении CONSTRAINT. Можно пользоваться таким приемом, чтобы избежать блокировки на время проверки данных. Мы сначала добавляем CONSTRAINT с указанием ключевого слова NOT VALID. Это говорит о том, что не гарантируется выполнение данного CONSTRAINT для всех строчек в таблице. Но при этом для всех новых строчек этот CONSTRAINT уже будет применяться, и будут выкидываться соответствующие исключения, если он не выполняется.

Нам остается только провалидировать все существующие значения, что можно выполнить отдельной командой VALIDATE CONSTRAINT, и при этом эта команда уже не препятствует ни чтению, ни записи в таблице. Таблица на это время будет доступна.

Операции, которые изначально быстро работают в PostgreSQL и не требуют длительных блокировок:



Одна из таких операций — добавление колонки без значений по умолчанию и каких-либо ограничений. Потому что никакие изменения в саму таблицу не вносятся, меняются только ее мета-данные. А все NULL-значения, которые мы видим в результате SELECT, подмешиваются просто на выходе.

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

Также быстрая операция установки SET NOT NULL, тут она занимает немного больше времени, чем описано ранее, примерно несколько секунд на таблицу в 30 млн записей. Этого времени тоже можно избежать, если оно имеет значение.

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



Касательно добавления NOT NULL-колонки. Чтобы избежать блокировки на время валидации, можно выполнить прием, о котором говорили ранее — добавляем CONSTRAINT, соответствующую CHECK (column IS NOT NULL) NOT VALID, и отдельной командой валидируем ее.

Разница в целом в том, что это ограничение будет существовать на уровне таблицы, а не на уровне колонки в определении таблицы. Еще одно отличие — в том, что это может повлиять на производительность, около одного процента. При этом не будет блокировки, если сервис высоконагруженный, даже несколько секунд блокировки могут привести к тому, что скопится огромная очередь транзакций и будет проблема на сервисе.



Удаление данных в PostgreSQL — в целом, быстрая операция, так как данные не удаляются сразу, просто колонка помечается устаревшей в атрибутах у таблицы, а данные реально будут удалены только после запуска очередного вакуума.



Поговорим про библиотеку. Я тут говорю про Django, миграции. В целом Django — библиотека для Python, веб-фреймворк, изначально он создавался для быстрого создания веб-сайтов по типу новостных, с тех пор он значительно прокачался. Там есть ORM-система, которая позволяет общаться с записями в БД, с таблицами, как будто они являются объектами или классами Python. То есть каждой таблице соответствует свой класс в Python. И когда мы вносим изменения в наш код на Python, то есть добавляем новые атрибуты как бы колонки в таблицу, Django во время процедуры создания миграции замечает эти изменения, и создает файлы миграции, чтобы внести зеркальные изменения в саму БД, чтобы они не расходились.

Библиотека была написана, чтобы во время выполнения таких миграций автоматизировать некоторые из рассмотренных ранее приемов избежания долгих блокировок на таблице. Она работает с Django начиная с версии 1.8 до 2.1 включительно, и Python с 2.7 до 3.7 включительно.

Касательно текущих возможностей библиотеки, это добавление колонки со значением по умолчанию без блокировок, nullable или нет, это создание индекса CONCURRENTLY, а также возможности перезапуска при падении. В стандартной реализации Django, если мы добавляем колонку со значением по умолчанию, заблокировали таблицу, и если она большая, это может быть 40 минут блокировки по моему опыту. Блокируется таблица, и все, ждем, пока скопируется и внесутся изменения. Прошло 30 минут — поймали connection error к базе, миграция падает, изменения не коммитятся, и приходится запускать заново, снова ждать 40 минут, вновь блокируя на это время таблицу.


Ссылка на GitHub

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



Про подключение в целом. Есть документация; если кратко, надо заменить engine в настройках БД Django на engine из библиотеки. Также там есть различные mixins, если вы используете свои engines для подключения.



Пример работы, про добавление колонки со значением по умолчанию. Тут добавляем колонки с булевым значением, True по умолчанию. Какие операции выполнены стандартной SchemaEditor? Выполняемые операции можно посмотреть, если запустить SQL migrate. Это достаточно полезно, по самому виду миграции не всегда бывает понятно, что же там Django на самом деле может изменить. И полезно запускать и смотреть, будут ли выполнены ожидаемые нами операции и не попало ли туда что-то лишнее и ненужное.

Какие команды выполнит SchemaEditor? Сначала в одну транзакцию добавится новая колонка, добавится значение по умолчанию. Потом, пока такой Update не вернет, что он обновил ноль, данные будут обновляться.

Затем выставится SET NOT NULL у колонки, и значение по умолчанию будет удалено, повторяя поведение Django, который хранит значение по умолчанию не в базе, а у себя на уровне логики в коде.

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



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

В целом библиотека развивается, принимаем пул-реквесты. Кому интересно — присоединяйтесь.

Стоит обращать внимание, что с ростом БД миграции имеют неизбежное свойство замедляться. Нужно следить, какие lock'и берет таблица, запускать SQL migrations, чтобы смотреть, какие операции применяются. Мы же со своей стороны в Яндекс.Коннекте применяем эту библиотеку там, где позволяют ее возможности. А там, где не позволяют, мы уже самостоятельно, руками фейкаем Django-миграции, запускаем свои SQL-запросы.

Таким образом мы достигаем высокой доступности сервисов на чтение и на запись. У нас большие нагрузки, тысячи RPS, и простой в несколько минут, не говоря о большем времени, недопустим. Нужно, чтобы миграции происходили незаметно для пользователя. А с такими нагрузками уже не получится встать в четыре часа ночи, что-то накатить, когда нет нагрузки, и снова лечь спать — потому что нагрузка идет круглые сутки.

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

Представим, что запускается операция, которая — пусть и на несколько миллисекунд — требует access exclusive. Пример такой операции — добавление колонки без значения по умолчанию. Представим, что в момент ее запуска в другой транзакции идет какая-либо другая долгая операция — скажем, SELECT с агрегацией. В этом случае наша операция встанет за ней в очередь. Это произойдет, так как access exclusive конфликтует со всеми другими типами блокировок.

Пока наша операция добавления колонки ждет lock'а, все остальные встанут за ней в очередь и не будут выполнены до ее завершения. При этом выполняемая операция — SELECT с агрегацией — может не конфликтовать с остальными, и если бы не наше создание колонки, они бы не встали в очередь, а выполнялись бы параллельно.

Такая ситуация может создавать большие проблемы на сервисе. Поэтому перед запуском ALTER TABLE или любой другой операции, которая требует блокировки access exclusive, надо смотреть, чтобы на БД в текущий момент не шли долгие запросы. Или можно просто вставлять очень маленький log timeout. Тогда, если бы не удалось быстро взять lock, операция бы падала. Мы бы могли просто перезапустить ее, а не блокировать таблицу на долгий срок, пока операция будет ждать выдачи ей гранта на блокировки. На этом всё, спасибо.

Комментарии (40)


  1. gecube
    13.01.2019 11:41

    Отличная статья! Спасибо, очень познавательно. Больше бы статей про правильную работу с базами данных и их схемами.
    Еще очень интересно про репликацию и ее варианты.


  1. Melkij
    13.01.2019 14:01
    +2

    Операции, которые изначально быстро работают в PostgreSQL и не требуют длительных блокировок:

    SET NOT NULL

    Эмм. Ну нет.
    SET NOT NULL блокировку требует и на время полного seqscan таблицы в один поток. Либо очень грязным хаком напрямую системного каталога.
    Уж поверьте человеку, который ради этого вообще впервые полез писать пачт к postgresql. Хотя мой небольшой патч, предоставляющий возможность обойтись штатными средствами уже очевидно не примут.
    Хотя дальше пишете про constraint, и вот это именно то, что я в патче предлагал для валидации set not null и таким образом после добавления временного constraint set not null был бы быстрым, а потом спилить лишний constraint. И сообщество вроде как согласно, что да, это лучше чем ничего, но патч висит как есть.

    Касательно же add column default, про который много уже где написано как его правильно вносить — уже понемногу теряет актуальность, в postgresql 11 наконец эта штука существует штатно. Зато отдельное спасибо за упоминание очереди блокировок. А то везде в статьях назовут операцию быстрой и безопасной, а потом случайно с какой-нибудь транзакцией пересекаются и лежат, ждут блокировку.


    1. tbicr
      13.01.2019 14:36

      обратите внимание как django использует DEFAULT — только для заполнения таблицы значениями, после этого DEFAULT удаляется, но если не тушить машинки с django, то после миграции могут быть вставки старым кодом и DEFAULT просто не сработает пока не обновится код на всех машинках с django


  1. tbicr
    13.01.2019 14:32
    +1

    Очень похоже на то, что я писал https://habr.com/post/425063/ + библиотечка https://github.com/tbicr/django-pg-zero-downtime-migrations


    Было пару вопросов к библиотеке:


    • как поведёт себя RunSql("UPDATE table SET sql = 'CREATE INDEX' WHERE sql = 'CREATE INDEX CONCURRENTLY'")
    • обходит ли библиотека описаные проблемы с SET NOT NULL и RENAME COLUMN
    • что будет с CREATE COLUMN SET DEFAULT NOT NULL когда мы раскатываем миграцию для кластера из нескольких инстансов с django (то есть старый/новый код работает со новой/старой схемой)?


    1. Tatikoma
      14.01.2019 11:41

      Рискну ответить не полностью разобравшись в вопросе. Первый пункт про возможные проблемы при использовании ключевых слов в строках запросов? — Строки из запросов можно просто вырезать: всё что в одинарных кавычках, а так же все числа не соседствующие с буквами можно заменить на плейсхолдеры, после чего спокойно обрабатывать запрос.

      Не уверен насчёт корректности такого метода, — когда-то писал тулзу которая парсит slowlog постгреса, удаляет из запросов все параметры и считает суммарное время исполнения для каждого запроса: идея в том, чтобы найти запросы которые съедают больше всего времени, посмотреть глазами и оптимизровать. И в моём случае это сработало, — по результатам добавил один индекс и нагрузка на сервера упала вдвое… :-) Кстати, может быть есть готовые тулзы?


      1. Melkij
        14.01.2019 11:47

        удаляет из запросов все параметры и считает суммарное время исполнения для каждого запроса

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


        1. Tatikoma
          14.01.2019 11:52

          О, спасибо!


        1. tbicr
          14.01.2019 14:24

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


          1. Melkij
            14.01.2019 14:28

            да, pg_stat_statements не отменяет необходимость следить за запросами попадающими в лог по подходящей для проекта отсечке log_min_duration_statement.
            Ну и давняя известная беда, pg_stat_statements считает только время выполнения, но не время планирования.


            1. tbicr
              14.01.2019 14:33

              Можете привести примеры или почитать про проблемы с планированием — вроде никогда не сталкивался?


              1. Melkij
                14.01.2019 14:39
                +1

                Из простого — создайте табличку партицированную на тысячи разделов вместо явно рекомендуемых в документации up to perhaps a hundred child tables. Потом посмотрите explain analyze — увидите что planning time может даже превышать время выполнения. Но в pg_stat_statements этого видно не будет, он сейчас считает только execution time.


              1. ishergin
                14.01.2019 15:37

                Так же planning time может быть сильно выше execution time из за bloat таблиц, мониторинг и pg_repack помогает.


                1. tbicr
                  14.01.2019 16:15

                  autovacuum не достаточно, чтобы избежать bloat?


                  1. ishergin
                    15.01.2019 15:07
                    +1

                    Если таблица уже имеет bloat по какой то причине — автовакуум не поможет. Или он может не успевать за изменениями, долгие запросы на standby так же могут аффектить


                    1. Melkij
                      15.01.2019 16:46

                      долгие запросы на standby так же могут аффектить

                      При включенном hot_standby_feedback. Либо при отставании слота логической репликации
                      А вот долгие запросы на мастере — мешать автовакууму будут.


                      1. ishergin
                        16.01.2019 13:06

                        все так =)


      1. tbicr
        14.01.2019 14:10

        Позволю себе предположить, что:


        if 'CREATE INDEX' in sql:
            sql = sql.replace('CREATE INDEX', 'CREATE INDEX CONCURRENTLY')

        • хак, я привёл пример при которых можно получить не совсем то, чего хотелось.

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


  1. b1rdex
    13.01.2019 16:47

    Есть ли у кого-либо подобный материал по MySQL?


    1. m8rge
      13.01.2019 17:29

      Материала нет, есть тулза:


      1. m8rge
        13.01.2019 17:29

        percona. com/doc/percona-toolkit/2.0/pt-online-schema-change.html


      1. b1rdex
        13.01.2019 17:32

        Да, знаю о ней. Не всегда удобно её использовать и есть проблемы при использовании репликации.


  1. Devtime
    13.01.2019 17:34

    А как вы решили проблему с vacuum full? Там кроме блокировок ещё надо место на жестком диске почти равное данной таблице


    1. Melkij
      13.01.2019 19:57

      Если места на диске не хватает или одним куском табличка не пролезает (например, диски не тянут столько писать разом или сеть до реплики) — есть утилитка pgcompacttable

      И на vacuul full места надо не «почти равное», а даже вплоть до двукратного — создать копию таблички и всех индексов, а так же прогнать всё это через WAL.


  1. theo85
    13.01.2019 17:34

    Спасибо за статью.
    А каким образом делаете VACUUM FULL?


    1. Melkij
      13.01.2019 19:58

      Могу ответить за нас — pgcompacttable или pg_repack по ситуации.


    1. smosker
      14.01.2019 14:05

      Да, как написали выше пользуемся pg_repack


  1. mspain
    14.01.2019 07:28

    Мыши кололись, но упорно продолжали не есть Монго :(


    1. Tatikoma
      14.01.2019 12:02

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


  1. Yo1
    14.01.2019 12:27

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


    1. Melkij
      14.01.2019 12:45

      Вы про смену типа поля? Пропустили добавление триггера. Соответственно триггеры на before insert и before update поддерживают актуальность нового поля всё время пока идёт обновление.


      1. Yo1
        14.01.2019 13:00

        нет, я про первый кейс с дефолтным значением колонки. с тригерами тоже не понятно. а блокировки кто будет выставлять? копировальщик зачитает на момент t1, запишет в момент t3. между ними гарантирован лост апдейт. постгрес то не блокировочник. в яндексе об этом в курсе?


        1. Melkij
          14.01.2019 13:40

          Дефолтное значение колонки — а зачем там дополнительные блокировки? Дефолт же ещё выставляем, значит приложение с этим работать вообще ещё не умеет и не трогает, не задеплоили же базу ещё для этой фичи. Ну или умеет, но понимает, что null = default, а хотим сделать not null, вычитываем все is null — т.е. которые ещё не обновили, если их уже обновило приложение — мы их и не вычитываем и не меняем. А, пригляделся, для самого update «and some_bool is null» нужен в этом случае для recheck. Тут маленький race condition, либо for update какой-то из select хватать. Но не трёхдневный точно.

          Триггеры — зачем какая-то дополнительная блокировка? Триггер вызывается уже после взятия блокировки строки. Мы же разумеется не говорим о тупейшем отдельно select и затем update по одной строке из результата select в миграции? Здесь один update, который нормально берёт для себя блокировки и не гоняет бесполезно данные туда-сюда между приложением и БД (хотя код модуля я не смотрел, не интересно во всяких orm копаться)


          1. Yo1
            14.01.2019 15:25

            после того как вы выставили дефолт вы пишите:

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

            никакие тригеры тут не фигурируют. пока вторая пачка переносит значения, строки первой пачки уже сменили значения и в рассинхроне.
            про тригеры ок.


            1. Melkij
              14.01.2019 15:38

              Вы ведь про добавление нового поля? Откуда взялось «пачка переносит значения»? Никто ничего не переносит.

              Мы поставили set default — значит все новые сроки будут писаться с этим default. Затем в фоне обновляем все старые строки простым update, который сам корректно беспокоится о взятии row level lock. А приложение использующее это новое поле будет задеплоено уже после завершения миграции. Если вы хотите задеплоить раньше — то у вас и приложение должно ожидать получить NULL из колонки которую вы хотите NOT NULL назвать.

              Ну и пишу это не я. Я не автор ни статьи, ни инструмента и вообще не работаю в яндексе. Автор статьи даже в ЛС не отвечает и не правит одну явную опечатку.


              1. Yo1
                14.01.2019 16:04

                я процитировал откуда взялась пачка

                Затем в фоне обновляем все старые строки простым update, который сам корректно беспокоится о взятии row level lock.

                корректно будет лишь в одном случае — если row level lock ставить на все записи и одной транзакцией перенести. я же доступно расписал: пока вторая пачка переносит значения, строки первой пачки уже сменили значения и в рассинхроне.


                1. Melkij
                  14.01.2019 16:45

                  я процитировал откуда взялась пачка

                  Почему пачка — понятно и очевидно. Почему переносит значения? Откуда это взялось? Зачем куда-то что-то переносить?

                  Если вы про MVCC сущность тапла — то update выполняется под корректной блокировкой строки и никто в это вмешаться не может до конца транзакции.

                  пока вторая пачка переносит значения, строки первой пачки уже сменили значения

                  Почему сменили? Кто сменил? Кто разрешил взять блокировку на строку?


                  1. Yo1
                    14.01.2019 17:10

                    взялось из текста. была колонка, там были значения. на них он хочет поставить not null, там где нулл чем-то дефолтным заполнить. в этом суть.

                    Почему сменили? Кто сменил? Кто разрешил взять блокировку на строку?

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

                    видимо пачка = транзакция. первая пачка прошла, локи отпускает, что бы «не блокировать надолго большой объем данных»


                    1. Melkij
                      14.01.2019 17:21

                      Так я дважды уже переспросил «вы про добавление новой колонки?»
                      Если нет — то откуда у вас «была колонка, там были значения»? Раз колонка новая — то не было там никаких значений. Цитируете вы фрагмент именно добавления нового поля. Нет там никаких старых значений и не было.

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

                      Пачка — да, отдельная транзакция. В этом и смысл.
                      Мы один раз обновили пачку строк, заполнив в ней значение нового поля и более к этим строкам не возвращаемся. Потому что в них теперь есть значение и под is null они больше не попадают. И нас вообще не волнует, что с теми строками происходит дальше. Потому что default мы им проставили, а приложение это новое поле ещё не использует и следовательно поле как неизменившаяся часть тапла копируется при обновлениях строки.


                      1. Yo1
                        14.01.2019 17:45

                        попробуйте сосредоточится и вникнуть в текст. я точно процитировал кусок о котором речь, я разжувал что именно аффтор имеет ввиду. у него задача поставить not null на существующую колонку. это важно во тут сосредоточится. колонка существует, значения там есть. то что он это делает через добавление новой колонки и предварительным заполнением дефолтным значением — реализация задачи.


                        1. Melkij
                          14.01.2019 18:06

                          у него задача поставить not null на существующую колонку

                          эмм. А это вы откуда достали? Попробуйте вы тоже что ли сосредоточиться.
                          Не имеет ровно никакого смысла для set not null делать новую колонку. Потому что set not null всё равно хочет seqscan всей таблицы под эксклюзивной блокировкой и это единственный code path этого tablecmd.c.

                          Автор пишет о добавлении новой (!) колонки. Которой раньше никогда не было. И никаких данных в ней тоже не было. Только статичный default, который мы и вносим. После обновления всех строк на этот статичный default нам надо проставить SET NOT NULL (и если надо, о чём тоже написано), как оно было в исходной долгой блокирующей alter table.
                          Не имеет никакого смысла только для проставления NOT NULL копировать колонку. База всё равно будет валидировать все check и все not null при попытке проставить not null любой колонке.