Создание значений по умолчанию в 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 строки не видны. Если вам нужно различие, вернитесь к предыдущему решению, где у вас есть оба в нашем распоряжении.