Русский
Русский
English
Статистика
Реклама

Serial

PostgreSQL Antipatterns уникальные идентификаторы

20.08.2020 10:19:07 | Автор: admin
Достаточно часто у разработчика возникает потребность формировать для записей таблицы PostgreSQL некие уникальные идентификаторы как при вставке записей, так и при их чтении.


Таблица счетчиков


Казалось бы чего проще? Заводим отдельную табличку, в ней запись со счетчиком. Надо получить новый идентификатор читаем оттуда, чтобы записать новое значение делаем UPDATE

Так делать не надо! Потому что завтра же вам придется решать проблемы:


Объект SEQUENCE


Для таких задач в PostgreSQL предусмотрена отдельная сущность SEQUENCE. Она нетранзакционна, то есть не вызывает блокировок, но две параллельные транзакции заведомо получат разные значения.

Чтобы получить следующий ID из последовательности, достаточно воспользоваться функцией nextval:

SELECT nextval('seq_name'::regclass);

Иногда необходимо получить сразу несколько ID для потоковой записи через COPY, например. Использовать для этого setval(currval() + N) в корне неправильно! По той простой причине, что между вызовами внутренней (currval) и внешней (setval) функций конкурирующая транзакция могла изменить текущее значение последовательности. Корректный способ вызвать nextval нужное количество раз:

SELECT  nextval('seq_name'::regclass)FROM  generate_series(1, N);

Псевдотип serial


В ручном режиме с последовательностями работать не очень удобно. Но ведь типовая задача у нас обеспечить вставку новой записи с новым sequence-ID! Специально для этой цели в PostgreSQL придуман псевдотип serial, который при генерации таблицы разворачивается во что-то типа id integer NOT NULL DEFAULT nextval('tbl_id_seq').

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

Однако, поскольку работа с последовательностью нетранзакционна, если идентификатор из нее получала rollback'нувшаяся транзакция, то в сохраненных записях таблицы последовательность ID окажется дырявой.

GENERATED-столбцы


Начиная с PostgreSQL 10, появилась возможность объявления идентифицирующего столбца (GENERATED AS IDENTITY), соответствующего стандарту SQL:2003. В варианте GENERATED BY DEFAULT поведение эквивалентно serial, а вот с GENERATED ALWAYS все интереснее:

CREATE TABLE tbl(  id    integer      GENERATED ALWAYS AS IDENTITY);

INSERT INTO tbl(id) VALUES(DEFAULT);-- Запрос успешно выполнен: одна строка изменена за 10 мс.INSERT INTO tbl(id) VALUES(1);-- ERROR:  cannot insert into column "id"-- DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.-- HINT:  Use OVERRIDING SYSTEM VALUE to override.

Да, чтобы вставить конкретное значение поперек такого столбца, придется приложить дополнительные усилия с помощью OVERRIDING SYSTEM VALUE:

INSERT INTO tbl(id) OVERRIDING SYSTEM VALUE VALUES(1);-- Запрос успешно выполнен: одна строка изменена за 11 мс.

Заметьте, что теперь у нас в таблице два одинаковых значения id = 1 то есть GENERATED не накладывает дополнительных UNIQUE-условий и индексов, а является исключительно декларацией, равно как и serial.

В общем случае, на современных версиях PostgreSQL использование serial не рекомендуется с предпочтительной заменой его на GENERATED. Кроме, разве что, ситуации поддержки кросс-версионных приложений, работающих с PG ниже 10.

Генерируемый UUID


Все хорошо, пока вы работаете в рамках одного экземпляра БД. Но когда их несколько, адекватного способа синхронизации последовательностей не существует (впрочем, это не мешает неадекватно их синхронизировать, если очень хочется). Тут на помощь приходит тип UUID и функции генерации значений для него. Я обычно использую uuid_generate_v4() как наиболее случайную.

Скрытые системные поля


tableoid/ctid


Иногда при выборке записей из таблицы требуется как-то адресоваться к конкретной физической записи, или узнать, из какой конкретной секции была получена та или иная запись при обращении к родительской таблице при использовании наследования.

В этом случае нам помогут скрытые системные поля, присутствующие в каждой записи:

  • tableoid хранит oid-идентификатор таблицы то есть tableoid::regclass::text дает имя конкретной таблицы-секции
  • ctid физический адрес записи в формате (<страница>,<смещение>)

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

oid


Вплоть до PostgreSQL 11 существовала возможность объявить при создании таблицы атрибут WITH OIDS:

CREATE TABLE tbl(id serial) WITH OIDS;

Каждая запись этой таблицы получала дополнительное скрытое поле oid с глобально-уникальным значением в рамках БД как это организовано для системных таблиц вроде pg_class, pg_namespace,

При вставке записи в такую таблицу генерируемое значение возвращается сразу с результатом запроса:

INSERT INTO tbl(id) VALUES(DEFAULT);

Запрос успешно выполнен: строка с OID 16400 добавлена за 11 мс.

Такое поле невидимо при обычном запросе таблицы:

SELECT * FROM tbl;

id-- 1

Его, как и остальные системные поля надо запрашивать в явном виде:

SELECT tableoid, ctid, xmin, xmax, cmin, cmax, oid, * FROM tbl;

tableoid | ctid  | xmin | xmax | cmin | cmax | oid   | id---------------------------------------------------------   16596 | (0,1) |  572 |    0 |    0 |    0 | 16400 |  1

Правда, значение oid имеет всего 32 бита, поэтому весьма несложно получить переполнение, после которого даже создать никакую таблицу (ей нужен новый oid!) не удастся. Поэтому, начиная с PostgreSQL 12, WITH OIDS более не поддерживается.

Честное время clock_timestamp


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

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

SELECT  now(), clock_timestamp()FROM  generate_series(1, 4);

              now              |        clock_timestamp-------------------------------+------------------------------- 2020-08-19 16:26:05.626629+03 | 2020-08-19 16:26:05.626758+03 2020-08-19 16:26:05.626629+03 | 2020-08-19 16:26:05.626763+03 2020-08-19 16:26:05.626629+03 | 2020-08-19 16:26:05.626764+03 2020-08-19 16:26:05.626629+03 | 2020-08-19 16:26:05.626765+03
Подробнее..

PostgreSQL Antipatterns накручиваем себе проблемы

28.06.2020 20:20:26 | Автор: admin
Некоторые ситуации в работе PostgreSQL кажутся неочевидными, пока не попытаешься детально понять, почему это работает так. Из-за незнания таких особенностей иногда разработчик сам провоцирует проблемы для нормальной работы своего приложения в будущем.

Сегодня разберем пару примеров, как неудачная организация БД и кода могут превратить наше приложение в клубок проблем:

  • накрутка serial при ON CONFLICT
  • накрутка счетчика транзакций


Накрутка serial при ON CONFLICT


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

Наше приложение (или все-таки разработчики?) любит суррогатные ключи, поэтому сразу добавим в таблицу в качестве PRIMARY KEY автоинкремент-поле с типом serial. Точнее, smallserial ведь мы точно знаем, что строк будет не больше 215:

CREATE TABLE tbl(  pk    smallserial      PRIMARY KEY, val    integer      UNIQUE);

Пытаться вставлять данные в него мы иногда будем, но новых среди них будет немного. Поэтому для удобства вставки, чтобы не заниматься обработкой исключений уникальности в своем коде, воспользуемся появившимся с версии 9.5 функционалом INSERT ... ON CONFLICT ...:

INSERT INTO tbl(val) VALUES(1) ON CONFLICT DO NOTHING;-- 1 строкаINSERT INTO tbl(val) VALUES(1) ON CONFLICT DO NOTHING;-- 0 строк, и никаких ошибок!

Теперь оставим наше приложение спокойно работать, и Вот ровно с такой ошибкой к нам и прибегут через несколько дней или недель:

ERROR:  nextval: reached maximum value of sequence "tbl_pk_seq" (32767)

И потом начнут приходить все чаще и чаще. Но как появилось столько записей? Почему не сработал ON CONFLICT?

Дело в том, что они и не появились. Давайте еще раз с нуля посмотрим на происходящее в нашей таблице:

TRUNCATE TABLE tbl RESTART IDENTITY;INSERT INTO tbl(val) VALUES(1) ON CONFLICT DO NOTHING RETURNING *;-- 1 строка: pk = 1, val = 1INSERT INTO tbl(val) VALUES(1) ON CONFLICT DO NOTHING RETURNING *;-- 0 строкINSERT INTO tbl(val) VALUES(2) ON CONFLICT DO NOTHING RETURNING *;-- 1 строка: pk = 3, val = 2

Как так pk = 3, ведь вставилось всего 2 строки? Мы запутались


На самом-то деле, все объясняется просто. Посмотрим внимательно, что из себя представляет наша таблица:

_tmp=# \d tbl                             Table "public.tbl" Column |   Type   | Collation | Nullable |             Default--------+----------+-----------+----------+--------------------------------- pk     | smallint |           | not null | nextval('tbl_pk_seq'::regclass) val    | integer  |           |          |Indexes:    "tbl_pkey" PRIMARY KEY, btree (pk)    "tbl_val_key" UNIQUE CONSTRAINT, btree (val)

Типы данных smallserial, serial и bigserial не являются настоящими типами, а представляют собой просто удобное средство для создания столбцов с уникальными идентификаторами (подобное свойству AUTO_INCREMENT в некоторых СУБД). В текущей реализации запись:

CREATE TABLE имя_таблицы (    имя_столбца SERIAL);

равнозначна следующим командам:

CREATE SEQUENCE имя_таблицы_имя_столбца_seq AS integer;CREATE TABLE имя_таблицы (    имя_столбца integer NOT NULL DEFAULT nextval('имя_таблицы_имя_столбца_seq'));ALTER SEQUENCE имя_таблицы_имя_столбца_seq OWNED BY имя_таблицы.имя_столбца;

То есть при определении такого типа создаётся целочисленный столбец со значением по умолчанию, извлекаемым из генератора последовательности.
То есть наш smallserial превратился в тыкву в поле smallint с DEFAULT-значением из последовательности tbl_pk_seq.

А последовательность штука нетранзакционная:
Значение, выделенное из последовательности, считается задействованным, даже если строку с этим значением не удалось вставить в таблицу. Это может произойти, например, при откате транзакции, добавляющей данные.
То есть мы сначала сгенерировали DEFAULT-значение, использовали значение pk = 2, а потом его не вставили в таблицу из-за конфликта уникальности val, скрыв проблему с помощью ON CONFLICT DO NOTHING. И после очередной такой попытки у нас просто кончилась последовательность.

Что делать?


  • хорошо
    Стараться не использовать лишние суррогатные ключи в таблицах, где уникальный ключ и так уже есть.
  • просто
    Сконвертировать поле и вместо smallserial использовать serial или bigserial это позволит продлить агонию приложения на месяцы или даже годы.
  • разумно
    Не использовать serial и ON CONFLICT на таблицах с ожидаемо существенным количеством конфликтующих вставок.
  • странно
    Написать триггер INSTEAD OF для аналогичного по структуре VIEW (или можно хранимую процедуру, но мы ведь не ищем легких путей).

Давайте в научно-познавательных целях попробуем собрать последний вариант:

CREATE TABLE tbl(  pk    smallserial      PRIMARY KEY, val    integer      UNIQUE);-- отвязываем DEFAULTALTER TABLE tbl ALTER COLUMN pk DROP DEFAULT;-- создаем "промежуточное" VIEWCREATE VIEW _tbl AS TABLE tbl;CREATE OR REPLACE FUNCTION tbl_serial() RETURNS trigger AS $$BEGIN  IF NEW.pk IS NULL THEN    LOOP -- эмуляция UPSERT через цикл      PERFORM 1 FROM tbl WHERE val = NEW.val;      EXIT WHEN FOUND; -- выходим при наличии такого значения в словаре      BEGIN        NEW.pk = nextval(pg_get_serial_sequence('tbl', 'pk'));        INSERT INTO tbl VALUES(NEW.*);        RETURN NEW;      EXCEPTION        WHEN unique_violation THEN -- защита от конкурентной вставки      END;    END LOOP;  END IF;  RETURN NULL;END;$$ LANGUAGE plpgsql;-- триггер INSTEAD OF выполняется "вместо" заказанной операции над VIEWCREATE TRIGGER serial INSTEAD OF INSERT ON _tbl  FOR EACH ROW    EXECUTE PROCEDURE tbl_serial();

Обратите внимание, что дальнейшие вставки мы производим как бы во VIEW:

INSERT INTO _tbl(val) VALUES(1) ON CONFLICT DO NOTHING RETURNING *;-- 1 строка: pk = 1, val = 1INSERT INTO _tbl(val) VALUES(1) ON CONFLICT DO NOTHING RETURNING *;-- 0 строкINSERT INTO _tbl(val) VALUES(2) ON CONFLICT DO NOTHING RETURNING *;-- 1 строка: pk = 2, val = 2

Ура! Получили ровно то, что хотели, хоть и весьма нетривиально. Поэтому все получилось аккуратно, но котик несколько насторожен.



Он понимает, что со следующей ситуацией так просто уже не разобраться.

Накрутка счетчика транзакций


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

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

SET statement_timeout = '1s';

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

BEGIN TRANSACTION;  INSERT INTO _tbl(val) SELECT 1 FROM pg_sleep(0.1); -- эмулируем задержку  SAVEPOINT sp1;  INSERT INTO _tbl(val) SELECT 2 FROM pg_sleep(0.6);  SAVEPOINT sp2;  INSERT INTO _tbl(val) SELECT 3 FROM pg_sleep(1.1);  -- ERROR:  canceling statement due to statement timeout  ROLLBACK TO SAVEPOINT sp2;COMMIT TRANSACTION;

Давайте проверим, что первые две записи успешно сохранились в нашей таблице, несмотря на возникновение ошибки в ходе выполнения транзакции:

_tmp=# SELECT xmin, * FROM tbl;   xmin    | pk | val-----------+----+----- 926944639 |  1 |   1 926944641 |  2 |   2(2 rows)

Вот только у наших записей оказался разный идентификатор создавшей транзакции он увеличивается с каждым вызовом SAVEPOINT.
Для детального понимания внутренней механики работы транзакций, субтранзакций и 2PC в PostgreSQL рекомендую ознакомиться со статьей Transactions in PostgreSQL and their mechanism от Movead Li.
На практике такая ситуация приводит к тому, что autovacuum: VACUUM ... (to prevent wraparound) мы будем видеть очень и очень часто, а если ресурсы сервера не резиновые это может стать проблемой.

Что делать?


  • не можем позволить себе ждать завершения операции бесконечно
  • не хотим терять весь прогресс транзакции
  • не должны неоправданно накручивать счетчик транзакций

Единственный приемлемый вариант лавировать между Сциллой и Харибдой.

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

Ну как, стало немного полегче?..

Подробнее..

DBA Когда почти закончился serial

24.03.2021 18:04:11 | Автор: admin

"Шеф, всё пропало, у нас serial на мегатаблице почти закончился!" - а это значит, что либо вы его неаккуратно накрутили сами, либо у вас действительно данных столько, что разрядности integer-столбца уже не хватает для вашей большой и активной таблицы в PostgreSQL-базе.

Да и столбец этот не простой, а целый PRIMARY KEY, на который еще и ряд других немаленьких таблиц по FOREIGN KEY завязан. А еще и приложение останавливать совсем не хочется, ибо клиентам 24x7 обещано...

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

Организуем небольшой тестовый полигон:

CREATE TABLE tblpk(  pk    serial      PRIMARY KEY, valx    integer);INSERT INTO tblpk(valx)SELECT generate_series(1, 1e6);CREATE TABLE tblfk(  fk    integer      REFERENCES tblpk, valy    integer);INSERT INTO tblfk(fk, valy)SELECT (random() * (1e6 - 1))::integer + 1, generate_series(1, 1e6);-- не забываем, что для FK нужно создавать индекс "вручную"CREATE INDEX ON tblfk(fk);

Подготовительные работы

Первую часть работы можно сделать без наложения каких-либо блокировок вообще.

Добавляем новое поле:

ALTER TABLE tblpk ADD COLUMN _pk bigint;ALTER TABLE tblfk ADD COLUMN _fk bigint;

Универсальный копирующий триггер

Чтобы для всех добавляемых и изменяемых записей состояние нового и старого полей у нас не разбегалось, повесим на таблицу копирующий триггер - на вставку новой записи или изменение отслеживаемого поля:BEFORE INSERT OR UPDATE OF <PK-поле>.

Ровно такую же задачу нам придется решать и для таблицы tblfk, поэтому сразу напишем триггерную функцию, которую можно будет универсально применять на любой таблице, использовав немного SQL-магии:

CREATE OR REPLACE FUNCTION copy_fld() RETURNS trigger AS $$DECLARE  fld_src text := quote_ident(TG_ARGV[0]); -- имя исходного поля  fld_dst text := quote_ident(TG_ARGV[1]); -- имя целевого поляBEGIN  EXECUTE $q$                 -- собираем тело запроса как текст    SELECT      (        json_populate_record( -- наполняем запись данными из JSON          $1                  -- NEW        , json_build_object(  -- {[fld_dst] : NEW[fld_src]}::json            '$q$ || fld_dst || $q$'          , $1.$q$ || fld_src || $q$::text          )        )      ).*                     -- "разворачиваем" record по столбцам    $q$    USING NEW -- используем NEW в качестве $1-аргумента    INTO NEW; -- результат складываем обратно в NEW  RETURN NEW; -- не забываем вернуть NEW, иначе изменения не применятсяEND $$ LANGUAGE plpgsql;

Теперь мы можем передать синхронизируемые поля как аргументы триггера - разные для каждой из таблиц:

CREATE TRIGGER copy BEFORE INSERT OR UPDATE OF pk   ON tblpk   FOR EACH ROW   EXECUTE PROCEDURE copy_fld('pk', '_pk'); -- откуда/кудаCREATE TRIGGER copy BEFORE INSERT OR UPDATE OF fk   ON tblfk   FOR EACH ROW   EXECUTE PROCEDURE copy_fld('fk', '_fk');

Массовое обновление записей

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

UPDATE tblpk SET _pk = pk WHERE _pk IS NULL;UPDATE tblfk SET _fk = fk WHERE _fk IS NULL;

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

Лучше всего воспользоваться сегментным обновлением, как это описано в статье "PostgreSQL Antipatterns: обновляем большую таблицу под нагрузкой". В результате единый UPDATE превратится в серию быстрых запросов, которые отлично садятся на индекс первичного ключа:

UPDATE  tblpkSET  _pk = pkWHERE  pk BETWEEN $1 AND $1 + 999 AND -- перебираем сегменты значений по 1K  _pk IS NULL;

Создаем новый индекс

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

CREATE UNIQUE INDEX CONCURRENTLY _pk ON tblpk(_pk); -- индекс под новый PKCREATE INDEX CONCURRENTLY _fk ON tblfk(_fk);        -- индекс под новый FK

В принципе, индексы можно было создать и раньше, но тогда все наши UPDATE писали бы еще и в него, поэтому работали бы существенно дольше.

Быстрая неблокирующая* конвертация

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

  • снимаем все autovacuum/autoanalyze, которые блокируют наши таблицы

    Эти процессы запустятся с очень большой вероятностью практически сразу, поскольку мы UPDATE'нули все записи в каждой из таблиц. Если мы не снимем их и накладываемые ими блокировки, все наши ALTER TABLE будут ждать получения блокировки сами (Access Exclusive), а за ними будет копиться очередь всех остальных запросов, даже SELECT (Access Share) по этим таблицам.

  • блокируем таблицы в монопольном режиме

    Если этого не сделать, какой-нибудь настырный SELECT из параллельного подключения вполне может вклиниться между нашими ALTER TABLE, что опять-таки приведет к длительным блокировкам.

    Пытаемся наложить блокировку в NOWAIT-режиме, чтобы при наличии активного SELECT-запроса (их-то мы не снимали) по любой из таблиц не висеть и ждать его, создавая за собой очередь, а отвалиться сразу.

  • модифицируем последовательность: привязываем ее к новому столбцу (OWNED BY) и снимаем ограничение на максимальное значение (NO MAXVALUE)

  • модифицируем основную таблицу:

    • удаляем старый столбец каскадно, что заодно удалит и ненужный нам более copy-триггер, старый первичный ключ вместе с индексом и все смотрящие на него внешние ключи

    • переименовываем новый столбец в старый

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

    • создаем новый первичный ключ с использованием заранее подготовленного уникального индекса, что заодно этот индекс и переименует

  • аналогично модифицируем связанную таблицу в чуть другом порядке:

    • удаляем и переименовываем столбцы

    • восстанавливаем внешний ключ в NOT VALID-режиме без фактической проверки уже содержащихся в таблице данных

    • восстанавливаем имя индекса под внешним ключом

BEGIN;  -- снимаем все процессы autovacuum/autoanalyze по нашим таблицам  SELECT    pg_terminate_backend(pid)  FROM    pg_stat_activity sa  WHERE    CASE      WHEN backend_type = 'autovacuum worker' THEN        EXISTS(          SELECT            NULL          FROM            pg_locks          WHERE            locktype = 'relation' AND            relation = ANY(ARRAY['tblpk', 'tblfk']::regclass[])        )    END;  -- сразу блокируем все таблицы, чтобы никто не влез  LOCK TABLE tblpk, tblfk IN ACCESS EXCLUSIVE MODE NOWAIT;  -- sequence  ALTER SEQUENCE tblpk_pk_seq OWNED BY tblpk._pk;  ALTER SEQUENCE tblpk_pk_seq NO MAXVALUE;  -- tblpk  ALTER TABLE tblpk    DROP COLUMN pk CASCADE; -- сносит заодно copy-триггер, PK и все FK  ALTER TABLE tblpk    RENAME COLUMN _pk TO pk;  ALTER TABLE tblpk    ALTER COLUMN pk SET DEFAULT nextval('tblpk_pk_seq');  ALTER TABLE tblpk    ADD CONSTRAINT tblpk_pkey PRIMARY KEY USING INDEX _pk;  -- tblfk  ALTER TABLE tblfk    DROP COLUMN fk CASCADE;  ALTER TABLE tblfk    RENAME COLUMN _fk TO fk;  ALTER TABLE tblfk    ADD CONSTRAINT tblfk_fk_fkey      FOREIGN KEY(fk)      REFERENCES tblpk      NOT VALID; -- без проверки ограничения по существующим данным  ALTER INDEX _fk RENAME TO tblfk_fk_fkey;COMMIT;

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

При этом все внешние ключи будут пересозданы с признаком "невалидности", хотя все данные под ними заведомо корректны. Жить это не мешает ровно никак, но если очень хочется отвалидировать FK настолько сильно, что мы даже готовы на ExclusiveLock, что заблокирует даже чтение из таблицы, пока вся она будет перечитываться, то делаем так:

ALTER TABLE tblfk  VALIDATE CONSTRAINT tblfk_fk_fkey;

Что мы забыли?

Приведенный выше код вполне работает, но только в простейших случаях.

Связанные объекты

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

Тут мы встречаем нескольких персонажей, чьи имена мы нигде ранее не упоминали, не задавали, и потому знать не можем:

  • tblpk_pkey - имя ограничения первичного ключа

  • tblfk_fk_fkey - имя ограничения внешнего ключа

  • tblpk_pk_seq - имя serial-последовательности

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

Сложные индексы

Аналогично, мы исходили из предположения, что индексы у нас самые простые, из единственного поля и без всяких условий. Но FK-индекс запросто может иметь вид tblfk(fk) WHERE fk IS NOT NULL, чтобы NULL-строки не замусоривали его, а PK включать в себя и другие поля, кроме serial.

Действия внешних ключей

Внешние ключи также могут быть определены существенно более сложно, чем в нашей модели - там может оказаться что-то вроде MATCH PARTIAL INITIALLY DEFERRED или ON DELETE SET NULL ON UPDATE RESTRICT.

Триггеры

Удалив каскадно старый столбец, мы снесли также и copy-триггер. А что если он был не один на этом поле?..

Имена и комментарии

Имя индекса внешнего ключа мы восстанавливали "по наитию", но нет абсолютно никакой гарантии, что оно совпадает с именем FK-ограничения.

А еще мы забыли восстановить комментарии объектов, которые могли быть наложены через COMMENT ON.

Скрипт миграции

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

  • sequence ссылается на поле через OWNED BY, а оно обратно через DEFAULT

  • индексы и триггеры ссылаются на поле напрямую

  • FK-constraint связывает поля пары таблиц и уникальный индекс на ведущей таблице

  • и все это может быть откомментировано

Создадим для теста максимально-проблемную для переноса ситуацию - сложные имена таблиц и полей, комментарии, триггеры и "хитрые" именованные FK:

CREATE TABLE "1st table"(  "primary key col"    serial      PRIMARY KEY, valx    integer);COMMENT ON COLUMN "1st table"."primary key col"  IS 'col-comment';INSERT INTO "1st table"(valx)SELECT generate_series(1, 1e5);CREATE TABLE "2nd table"(  fk    integer      CONSTRAINT "FK-name" REFERENCES "1st table"        ON UPDATE SET NULL        ON DELETE RESTRICT, valy    integer);COMMENT ON CONSTRAINT "FK-name" ON "2nd table"  IS 'con-comment';INSERT INTO "2nd table"(fk, valy)SELECT (random() * (1e5 - 1))::integer + 1, generate_series(1, 1e5);CREATE INDEX "FK-idx-name" ON "2nd table"(fk);COMMENT ON INDEX "FK-idx-name"  IS 'idx-comment';CREATE OR REPLACE FUNCTION tmp() RETURNS trigger AS $$BEGIN  RAISE NOTICE 'NEW : %', NEW::text;END $$ LANGUAGE plpgsql;CREATE TRIGGER tmp AFTER INSERT OR UPDATE OF "primary key col"  ON "1st table"  FOR EACH ROW  EXECUTE PROCEDURE tmp();COMMENT ON TRIGGER tmp ON "1st table"  IS 'trg-comment';

Ну, а теперь дело за малым! Вот наш скрипт:

Скрипт расширения serial -> bigserial
-- $1 : '"1st table"'     - с кавычками!-- $2 : 'primary key col' - без кавычек!WITH src(rel, fld) AS (  VALUES($1::regclass, $2::name)), fld AS (  SELECT    *  FROM    src  JOIN    pg_attribute at      ON (at.attrelid, at.attname) = (src.rel, src.fld)), idx AS (  SELECT    idx.*  FROM    fld  JOIN    pg_index idx      ON indrelid = attrelid AND      indkey::smallint[] && ARRAY[attnum]), con AS (  SELECT    CASE contype      WHEN 'p' THEN attnum      WHEN 'f' THEN conkey[array_position(confkey, attnum)]    END idkey  , con.*  FROM    fld  JOIN    pg_constraint con      ON (conrelid = attrelid AND conkey && ARRAY[attnum]) OR      (confrelid = attrelid AND confkey && ARRAY[attnum]))-- столбцы, входящие в PK или FK, colkey AS (  SELECT    *  , attrelid::regclass::text _attrel  , '_' || md5(attname) _attname  , quote_ident(attname) _qiattname  , replace(col_description(attrelid, attnum), '''', '''''') dsccol  FROM    con  INNER JOIN    pg_attribute at      ON (attrelid, attnum) = (conrelid, idkey)  WHERE    atttypid <> 'bigint'::regtype), code_col AS (  SELECT    string_agg($$-- $$ || _attrel || $$ALTER TABLE $$ || _attrel || $$  ADD COLUMN $$ || _attname || $$ bigint;$$ ||      CASE        WHEN dsccol IS NOT NULL THEN$$COMMENT ON COLUMN $$ || _attrel || '.' || _attname || $$  IS '$$ || dsccol || $$';$$        ELSE ''      END || $$CREATE TRIGGER copy  BEFORE INSERT OR UPDATE OF $$ || _qiattname || $$    ON $$ || _attrel || $$    FOR EACH ROW    EXECUTE PROCEDURE copy_fld('$$ || attname || $$', '$$ || _attname || $$');UPDATE $$ || _attrel || $$ SET $$ || _attname || $$ = $$ || _qiattname || $$ WHERE $$ || _attname || $$ IS NULL; -- лучше сегментно!!!$$    , ''    ) code  FROM    colkey)-- индексы, indkey AS (  SELECT    *  , quote_ident('_' || md5(sch || '.' || rel || '.' || idxname)) _idxname  FROM    (      SELECT        pg_get_indexdef(indexrelid) def      , cli.relnamespace::regnamespace::text sch      , idx.indrelid::regclass::text rel      , quote_ident(cli.relname) idxname      , replace(obj_description(cli.oid, 'pg_class'), '''', '''''') dscidx      , *      FROM        colkey      JOIN        pg_index idx          ON indrelid = attrelid AND          indkey::smallint[] && ARRAY[attnum]      JOIN        pg_class cli          ON cli.oid = idx.indexrelid    ) T), code_idx AS (  SELECT    string_agg(      E'-- ' || idxname || E'\n' ||      regexp_replace(        regexp_replace(          def        , E'(CREATE(?: UNIQUE)? INDEX ).*?( ON ).*?( USING )'        , E'\\1CONCURRENTLY ' || _idxname || E'\n  ON ' || sch || '.' || rel || E'\n  USING '        )      , E'(USING \\S+ \\(.*)' || _qiattname || E'(.*\\))'      , E'\\1' || _attname || E'\\2'      , 'g'      ) || E';\n'      || CASE        WHEN dscidx IS NOT NULL THEN$$COMMENT ON INDEX $$ || _idxname || $$  IS '$$ || dscidx || $$';$$        ELSE ''      END    , ''    ) code  FROM    indkey)-- тфблицы, code_rel AS (  SELECT    $q$-- зачищаем мешающие autovacuumSELECT  pg_terminate_backend(pid)FROM  pg_stat_activity saWHERE  CASE    WHEN backend_type = 'autovacuum worker' THEN      EXISTS(        SELECT          NULL        FROM          pg_locks        WHERE          locktype = 'relation' AND          relation = ANY('$q$ || array_agg(rel)::text || $q$'::regclass[])      )    END;-- блокируем все таблицыLOCK TABLE $q$ || string_agg(rel, ', ') || $q$ IN ACCESS EXCLUSIVE MODE NOWAIT;$q$ code  FROM    (      SELECT DISTINCT        _attrel rel      FROM        colkey    ) T)-- последовательность, seqkey AS (  SELECT    pg_get_serial_sequence(attrelid::regclass::text, attname) seq  , *  FROM    colkey), code_seq AS (  SELECT$q$ALTER SEQUENCE $q$ || seq || $q$  OWNED BY $q$ || _attrel || '.' || _attname || $q$;ALTER SEQUENCE $q$ || seq || $q$  NO MAXVALUE;$q$  FROM    seqkey  WHERE    seq IS NOT NULL)-- столбцы, code_col_tx AS (  SELECT    string_agg($$-- $$ || _attrel || $$ALTER TABLE $$ || _attrel || $$  DROP COLUMN $$ || _qiattname || $$ CASCADE;ALTER TABLE $$ || _attrel || $$  RENAME COLUMN $$ || _attname || $$ TO $$ || _qiattname || $$;$$ ||      CASE        WHEN adsrc IS NOT NULL THEN$$ALTER TABLE $$ || _attrel || $$  ALTER COLUMN $$ || _qiattname || $$    SET DEFAULT $$ || adsrc || $$;$$        ELSE ''      END    ,   ''    ) code  FROM    colkey  LEFT JOIN    pg_attrdef ad      ON (adrelid, adnum) = (attrelid, attnum))-- индексы, code_idx_tx AS (  SELECT    string_agg($$ALTER INDEX $$ || _idxname || $$  RENAME TO $$ || idxname || $$;$$    , '')  FROM    indkey)-- ключи, code_con_tx AS (  SELECT    string_agg(    (      SELECT        string_agg(          'ALTER TABLE ' || conrelid::regclass::text || E'\n  ADD ' ||          CASE con.contype            WHEN 'p' THEN              'PRIMARY KEY USING INDEX ' || idxname            WHEN 'u' THEN              'UNIQUE USING INDEX ' || idxname            WHEN 'f' THEN              'CONSTRAINT ' || quote_ident(con.conname) || ' ' || pg_get_constraintdef(con.oid) || CASE WHEN pg_get_constraintdef(con.oid) !~* 'NOT VALID' THEN E'\n    NOT VALID' ELSE '' END          END || E';\n' ||          CASE            WHEN obj_description(con.oid, 'pg_constraint') IS NOT NULL THEN$$COMMENT ON CONSTRAINT $$ || quote_ident(conname) || $$ ON $$ || conrelid::regclass::text || $$  IS '$$ || replace(obj_description(con.oid, 'pg_constraint'), '''', '''''') || $$';$$            ELSE ''          END        , ''        ORDER BY          CASE con.contype            WHEN 'p' THEN 0            WHEN 'u' THEN 1            WHEN 'f' THEN 2          END        )      FROM        pg_constraint con      WHERE        conindid = indexrelid    )    , ''    ) code  FROM    indkey)-- триггеры, trgkey AS (  SELECT    pg_get_triggerdef(trg.oid) def  , replace(obj_description(trg.oid, 'pg_trigger'), '''', '''''') dsctrg  , *  FROM    colkey  JOIN    pg_trigger trg      ON tgrelid = attrelid AND      tgattr::smallint[] && ARRAY[attnum]  WHERE    NOT tgisinternal), code_trg AS (  SELECT    string_agg(      def || E';\n'      || CASE        WHEN dsctrg IS NOT NULL THEN$$COMMENT ON TRIGGER $$ || quote_ident(tgname) || $$ ON $$ || _attrel || $$  IS '$$ || dsctrg || $$';$$        ELSE ''      END    , ''    ) code  FROM    trgkey)SELECT  E'-- столбцы\n' ||  (TABLE code_col) ||  E'\n-- индексы\n' ||  (TABLE code_idx) ||  E'\nBEGIN;\n' ||  regexp_replace(    (TABLE code_rel) ||    E'\n-- последовательность\n' ||    (TABLE code_seq) ||    E'\n-- столбцы\n' ||    (TABLE code_col_tx) ||    E'\n-- индексы\n' ||    (TABLE code_idx_tx) ||    E'\n-- ключи\n' ||    (TABLE code_con_tx) ||    E'\n-- триггеры\n' ||    (TABLE code_trg)  , E'^(.)'  , E'  \\1'  , 'gm'  ) ||  E'COMMIT;\n';

Надеюсь, когда-то этот скрипт пригодится и вам.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru