Сжатие последовательности в PostgreSQL

9

У меня есть id serial PRIMARY KEYстолбец в таблице PostgreSQL. Многие ids отсутствуют, потому что я удалил соответствующую строку.

Теперь я хочу «сжать» таблицу, перезапустив последовательность и переназначив ids таким образом, idчтобы сохранить первоначальный порядок. Является ли это возможным?

Пример:

  • Сейчас же:

 id | data  
----+-------
  1 | hello
  2 | world
  4 | foo
  5 | bar
  • После:

 id | data  
----+-------
  1 | hello
  2 | world
  3 | foo
  4 | bar

Я попробовал то, что было предложено в ответе StackOverflow , но это не сработало:

# alter sequence t_id_seq restart;
ALTER SEQUENCE
# update t set id=default;
ERROR:  duplicate key value violates unique constraint t_pkey
DETAIL:  Key (id)=(1) already exists.
рубик
источник

Ответы:

9

Во-первых, следует ожидать пробелов в последовательности. Спросите себя, действительно ли вам нужно их удалить. Ваша жизнь становится проще, если вы просто живете с этим. Чтобы получить числа без пробелов, (часто лучше) альтернатива состоит в использовании VIEWс row_number(). Пример в этом связанном ответе:

Вот несколько рецептов для устранения пробелов.

1. Новый, нетронутый стол

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

BEGIN;
LOCK tbl;

CREATE TABLE tbl_new (LIKE tbl INCLUDING ALL);

INSERT INTO tbl_new -- no target list in this case
SELECT row_number() OVER (ORDER BY id), data  -- all columns in default order
FROM   tbl;

ALTER SEQUENCE tbl_id_seq OWNED BY tbl_new.id;  -- make new table own sequence

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME TO tbl;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

COMMIT;

CREATE TABLE tbl_new (LIKE tbl INCLUDING ALL)копирует структуру вкл. ограничения и значения по умолчанию из исходной таблицы. Затем присвойте новому столбцу таблицы последовательность:

И сбросьте его до нового максимума:

Это дает преимущество в том, что новая таблица не имеет раздувания и кластеризована id.

2. UPDATEна месте

Это производит много мертвых строк и требует (авто) VACUUMпозже.

Если serialстолбец также PRIMARY KEY(как в вашем случае) или имеет UNIQUEограничение, вы должны избегать уникальных нарушений в процессе. (Более дешевое) значение по умолчанию для ограничений PK / UNIQUE должно быть NOT DEFERRABLE, что вызывает проверку после каждой отдельной строки. Все детали по этому связанному вопросу на SO:

Вы можете определить свое ограничение как DEFERRABLE(что делает его более дорогим).
Или вы можете удалить ограничение и добавить его обратно, когда закончите:

BEGIN;

LOCK tbl;

ALTER TABLE tbl DROP CONSTRAINT tbl_pkey;  -- remove PK

UPDATE tbl t  -- intermediate unique violations are ignored now
SET    id = t1.new_id
FROM  (SELECT id, row_number() OVER (ORDER BY id) AS new_id FROM tbl) t1
WHERE  t.id = t1.id;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

ALTER TABLE tbl ADD CONSTRAINT tbl_pkey PRIMARY KEY(id); -- add PK back

COMMIT;

Ни не возможнопока естьFOREIGN KEYограниченияссылающихся столбец (ы)потому что ( за документацию ):

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

Вам необходимо (заблокировать все задействованные таблицы и) удалить / воссоздать ограничения FK и обновить все значения FK вручную (см. Вариант 3. ). Или вы должны убрать значения с пути за секунду, UPDATEчтобы избежать конфликтов. Например, если у вас нет отрицательных чисел:

BEGIN;
LOCK tbl;

UPDATE tbl SET id = id * -1;  -- avoid conflicts

UPDATE tbl t
SET    id = t1.new_id
FROM  (SELECT id, row_number() OVER (ORDER BY id DESC) AS new_id FROM tbl) t1
WHERE  t.id = t1.id;

SELECT setval('tbl_id_seq', max(id)) FROM tbl;  -- reset sequence

COMMIT;

Недостатки как упомянуто выше.

3. Таблица Temp, TRUNCATE,INSERT

Еще один вариант, если у вас много оперативной памяти. Это сочетает в себе некоторые преимущества первых двух способов. Почти так же быстро , как вариант 1. и вы получите чистый, новый стол без наворотов , но сохранить все ограничения и зависимости в месте , как в варианте 2.
Тем не менее , в документации:

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

Жирный акцент мой.

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

SET temp_buffers = 500MB;   -- example value, see 1st link below

BEGIN;

CREATE TEMP TABLE tbl_tmp AS
SELECT row_number() OVER (ORDER BY id) AS new_id, *
FROM   tbl
ORDER  BY id;  -- order here to use index (if one exists)

-- drop FK constraints in other tables referencing this one
-- which takes out an exclusive lock on those tables

TRUNCATE tbl;

INSERT INTO tbl
SELECT new_id, data  -- list all columns in order
FROM tbl_tmp;        -- rely on established order in tbl_tmp
-- ORDER BY id;      -- only to be absolutely sure (not necessary)

--  example for table "fk_tbl" with FK column "fk_id"
UPDATE fk_tbl f
SET    fk_id = t.new_id  -- set to new ID
FROM   tbl_tmp t
WHERE  f.fk_id = t.id;   -- match on old ID

-- add FK constraints in other tables back

COMMIT;

Связанные, с более подробной информацией:

Эрвин Брандштеттер
источник
Если все FOREIGN KEYSнастроены на, CASCADEне могли бы вы просто перебрать старые первичные ключи и обновить их значения на месте (от старого значения до нового)? По сути, это вариант 3 без TRUNCATE tblзамены INSERTс использованием UPDATEи без необходимости обновления внешних ключей вручную.
Гили
@ Гили: Вы могли бы , но такой цикл очень дорогой. Поскольку вы не можете обновить всю таблицу сразу из-за нарушений уникального ключа в индексе, вам нужна отдельная UPDATEкоманда для каждой строки. См. Объяснение в ② или попробуйте сами.
Эрвин Брандштеттер,
Я не думаю, что производительность является проблемой в моем случае. На мой взгляд, есть два вида алгоритмов: те, которые «останавливают мир», и те, которые работают в фоновом режиме без необходимости выключать сервер. Предполагая, что сжатие происходит только один раз в голубой луне (например, при приближении к верхнему пределу типа данных), на самом деле не существует верхнего предела количества времени, которое это должно занять. Пока мы сжимаем записи быстрее, чем добавляются новые, у нас все будет хорошо.
Гили