Создание значений по умолчанию в CTE UPSERT с помощью PostgreSQL 9.3
Я нахожу, что использование записываемых CTEs для эмуляции upsert в PostgreSQL является довольно элегантным решением, пока мы не получим фактический upsert / merge в Postgres. (см.: https://stackoverflow.com/a/8702291/558819)
однако есть одна проблема: как я могу вставить значение по умолчанию? Используя NULL
не поможет, конечно, как NULL
явно вставляется как NULL
, в отличие, например, от MySQL. Пример:
WITH new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
, (NULL::int, 21, 1, 'b', 34, 2, NULL::boolean)
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
), upsert AS (
UPDATE playlist_items m
SET (playlist, item, group_name, duration, sort, legacy)
= (nv.playlist, nv.item, nv.group_name, nv.duration, nv.sort, nv.legacy)
FROM new_values nv
WHERE nv.id = m.id
RETURNING m.id
)
INSERT INTO playlist_items (playlist, item, group_name, duration, sort, legacy)
SELECT playlist, item, group_name, duration, sort, legacy
FROM new_values nv
WHERE NOT EXISTS (SELECT 1
FROM upsert m
WHERE nv.id = m.id)
RETURNING id
поэтому я хотел бы, например для legacy
столбец, чтобы принять его значение по умолчанию для второго VALUES
row.
Я пробовал несколько вещей, таких как, явно используя DEFAULT
в списке значений, который не работает, потому что CTE понятия не имеет, что он вставляет. Я также пробовал coalesce(col, DEFAULT)
в инструкции insert, которая, похоже, тоже не работала. Так можно ли делать то, что я хочу?
1 ответов
базы данных Postgres реализованы 9.5 UPSERT
. Увидеть ниже.
и Postgres 9.4 и старше
это сложная проблема. Вы сталкиваетесь с этим ограничением (в документации):
на
VALUES
список, появляющийся на верхнем уровнеINSERT
, an выражение может быть заменено наDEFAULT
, чтобы указать, что пунктом необходимо вставить значение по умолчанию столбца.DEFAULT
нельзя использовать когдаVALUES
появляется в других контекстах.
жирным выделено мной. Значения по умолчанию не определяются без таблицы для вставки. Так что нет прямые решение вашего вопроса, но есть ряд возможных альтернативные маршруты, в зависимости от конкретных требований.
выбрать значения по умолчанию из системного каталога?
вы мог бы fetch те из системного каталога pg_attrdef
как прокомментировал @Patrick или information_schema.columns
. Полные инструкции здесь:
но потом еще есть только список строки с текстовым представлением выражения для приготовления значения по умолчанию. Вам придется динамически создавать и выполнять операторы, чтобы заставить значения работать с. Скучно и грязно. Вместо этого мы можем позволить встроенная функциональность Postgres делает это для нас:
простой ярлык
вставьте фиктивную строку и верните ее для использования сгенерированных значений по умолчанию:
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
проблемы / область решения
- это гарантированно работает только для
STABLE
илиIMMUTABLE
выражения по умолчанию. БольшинствоVOLATILE
функции будут работать так же хорошо, но нет никаких гарантий. Thecurrent_timestamp
семейство функций квалифицируется как стабильное, поскольку их значения не изменяются в рамках транзакции.
В частности, это имеет побочные эффекты наserial
столбцы (или любой другой чертеж по умолчанию из последовательности). Но это не должно быть проблемой, потому что вы обычно не пишитеserial
колонки напрямую. Они не должны быть перечислены вINSERT
заявления на всех.
Оставшийся недостаток дляserial
столбцы: последовательность все еще расширенный одним вызовом, чтобы получить строку по умолчанию, создавая пробел в нумерации. Опять же, это не должно быть проблемой, потому что пробелы обычно можно ожидать наserial
столбцы.
можно решить еще две проблемы:
если у вас есть столбцы, определенные
NOT NULL
, вы должны вставить фиктивные значения и заменить наNULL
в результате.мы на самом деле не хотите вставьте фиктивную строку. Мы могли бы удалить позже (в одной транзакции), но это может иметь больше побочных эффектов, таких как триггеры
ON DELETE
. Есть лучший способ:
избегайте фиктивного ряда
клон a временная таблица включая значения по умолчанию столбца и вставить в это:
BEGIN;
CREATE TEMP TABLE tmp_playlist_items (LIKE playlist_items INCLUDING DEFAULTS)
ON COMMIT DROP; -- drop at end of transaction
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *;
...
же результат, меньше побочных эффектов. Поскольку выражения по умолчанию копируются дословно, клон рисует из того же последовательности, если таковые имеются. Но другие побочные эффекты от нежелательной строки или триггеров полностью избегаются.
хвала Игорю за идею:
удалить NOT NULL
ограничения
вам придется предоставить фиктивные значения для NOT NULL
столбцы, потому что (в документации):
ограничения Not-null всегда копируются в новый таблица.
либо разместить для тех, кто в INSERT
заявление или (лучше) устранить ограничения:
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
есть быстрый и грязный способ с привилегиями суперпользователя:
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
это просто временная таблица без данных и без другой цели, и она отбрасывается в конце транзакции. Так что короткий путь заманчив. Тем не менее, основное правило: никогда не вмешивайтесь в системные каталоги непосредственно.
Итак, давайте посмотрим в чисто:
Автоматизация с динамическим SQL в DO
заявление. Вам просто нужно обычные привилегии вы гарантированно имеете, так как та же роль создала временную таблицу.
DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$
гораздо чище и все еще очень быстро. Выполнить уход с динамической команды и остерегайтесь SQL-инъекции. Это утверждение безопасно. Я разместил несколько связанных ответов с более объяснение.
общее решение (9.4 и старше)
BEGIN;
CREATE TEMP TABLE tmp_playlist_items
(LIKE playlist_items INCLUDING DEFAULTS) ON COMMIT DROP;
DO $$BEGIN
EXECUTE (
SELECT 'ALTER TABLE tmp_playlist_items ALTER '
|| string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
|| ' DROP NOT NULL'
FROM pg_catalog.pg_attribute
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0
);
END$$;
LOCK TABLE playlist_items IN EXCLUSIVE MODE; -- forbid concurrent writes
WITH default_row AS (
INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *
)
, new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
VALUES
(651, 21, 30012, 'a', 30, 1, FALSE)
, (NULL, 21, 1, 'b', 34, 2, NULL)
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
)
, upsert AS ( -- *not* replacing existing values in UPDATE (?)
UPDATE playlist_items m
SET ( playlist, item, group_name, duration, sort, legacy)
= (n.playlist, n.item, n.group_name, n.duration, n.sort, n.legacy)
-- ..., COALESCE(n.legacy, m.legacy) -- see below
FROM new_values n
WHERE n.id = m.id
RETURNING m.id
)
INSERT INTO playlist_items
(playlist, item, group_name, duration, sort, legacy)
SELECT n.playlist, n.item, n.group_name, n.duration, n.sort
, COALESCE(n.legacy, d.legacy)
FROM new_values n, default_row d -- single row can be cross-joined
WHERE NOT EXISTS (SELECT 1 FROM upsert u WHERE u.id = n.id)
RETURNING id;
COMMIT;
вам нужно только LOCK
если у вас есть параллельные транзакции, пытающиеся записать в ту же таблицу.
по запросу это заменяет только значения NULL в столбце legacy
во входных строках для INSERT
случае. Может быть легко расширен для работы для других столбцов или в UPDATE
случае. Например, вы могли бы UPDATE
условно, а также: только если входное значение is NOT NULL
. Я добавил прокомментированную строку в UPDATE
выше.
в сторону: вам не нужно cast значения в любой строке, кроме первой в VALUES
выражение, так как типы являются производными от первый строки.
Postgres 9.5
осуществляет UPSERT с INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. Это в значительной степени упрощает работу:
INSERT INTO playlist_items AS m (id, playlist, item, group_name, duration, sort, legacy)
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
, (DEFAULT, 21, 1, 'b', 34, 2, DEFAULT) -- !
, (668, 21, 30012, 'c', 30, 3, FALSE)
, (7428, 21, 23068, 'd', 0, 4, FALSE)
ON CONFLICT (id) DO UPDATE
SET (playlist, item, group_name, duration, sort, legacy)
= (EXCLUDED.playlist, EXCLUDED.item, EXCLUDED.group_name
, EXCLUDED.duration, EXCLUDED.sort, EXCLUDED.legacy)
-- (..., COALESCE(l.legacy, EXCLUDED.legacy)) -- see below
RETURNING m.id;
мы можем прикрепить VALUES
предложение INSERT
напрямую, что позволяет DEFAULT
ключевое слово. В случае уникальных нарушений на (id)
, обновления Postgres вместо этого. Мы можем использовать исключенные строки в UPDATE
. инструкции:
на
SET
иWHERE
положенияON CONFLICT DO UPDATE
иметь доступ к существующая строка, использующая имя таблицы (или псевдоним) и строки предлагается для вставки с помощью специальногоexcluded
таблица.
и:
обратите внимание, что эффекты всех строк
BEFORE INSERT
триггеры отражено в исключенных значениях, поскольку эти последствия могли способствовать к строке, исключаемой из вставки.
оставшийся угловой корпус
у вас есть различные варианты UPDATE
: вы можете ...
- ... не обновлять вообще: добавить
WHERE
предложениеUPDATE
для записи только в выбранные строки. - ... обновлять только выбранные столбцы.
- ... только обновить, если столбец в настоящее время NULL:
COALESCE(l.legacy, EXCLUDED.legacy)
- ... только обновить, если новое значение
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
но нет никакого способа различить DEFAULT
значения и значения, фактически предоставленные в INSERT
. Только результат EXCLUDED
строки не видны. Если вам нужно различие, вернитесь к предыдущему решению, где у вас есть оба в нашем распоряжении.