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