PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Генерирайте стойности по подразбиране в CTE UPSERT с помощта на PostgreSQL 9.3

Postgres 9.5 имплементира UPSERT . Вижте по-долу.

Postgres 9.4 или по-стара версия

Това е труден проблем. Сблъсквате се с това ограничение (по документация):

В VALUES списък, който се появява на най-горното ниво на INSERT , анекспресията може да бъде заменена с DEFAULT за да посочите, че стойността по подразбиране на колоната дестинация трябва да бъде вмъкната. DEFAULT не може да се използва, когатоVALUES се появява в друг контекст

Удебелен акцент мой. Стойностите по подразбиране не се дефинират без таблица за вмъкване. Така че няма директен решение на вашия въпрос, но има редица възможни алтернативни маршрути, в зависимост от точните изисквания .

Извличане на стойностите по подразбиране от системния каталог?

Вие можете извлечете ги от системния каталог pg_attrdef като @Patrick коментира или от information_schema.columns . Пълни инструкции тук:

  • Вземете ли стойностите по подразбиране на колоните на таблицата в Postgres?

Но тогава ти все още имат само списък с редове с текстово представяне на израза за приготвяне на стойността по подразбиране. Ще трябва да изграждате и изпълнявате изрази динамично, за да получите стойности, с които да работите. Досадно и разхвърляно. Вместо това можем да оставим вградената функционалност на Postgres да направи това вместо нас :

Прост пряк път

Поставете фиктивен ред и го върнете, за да използва генерирани стойности по подразбиране:

INSERT INTO playlist_items DEFAULT VALUES RETURNING *;

Проблеми/обхват на решението

  • Това гарантирано работи само за STABLE или IMMUTABLE изрази по подразбиране . Най-VOLATILE функциите ще работят също толкова добре, но няма гаранции. current_timestamp семейството от функции се квалифицира като стабилно, тъй като техните стойности не се променят в рамките на транзакция.
    По-специално, това има странични ефекти върху serial колони (или всякакви други чертежи по подразбиране от последователност). Но това не би трябвало да е проблем, защото обикновено не пишете в serial колони директно. Те не трябва да са изброени в INSERT изявления изобщо.
    Оставащ недостатък за serial колони:последователността все още се напредва от единичното извикване, за да се получи ред по подразбиране, което води до пропуск в номерирането. Отново, това не би трябвало да е проблем, защото пропуските по принцип се очакват в serial колони.

Могат да бъдат решени още два проблема:

  • Ако имате дефинирани колони NOT NULL , трябва да вмъкнете фиктивни стойности и да ги замените с NULL в резултата.

  • Всъщност не искаме да вмъкваме фиктивния ред . Бихме могли да изтрием по-късно (в същата транзакция), но това може да има повече странични ефекти, като задействания ON DELETE . Има по-добър начин:

Избягвайте фиктивния ред

Клонирайте временна таблица включително колони по подразбиране и вмъкнете в това :

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 *;
...

Същият резултат, по-малко странични ефекти. Тъй като изразите по подразбиране се копират дословно, клонингът черпи от същите последователности, ако има такива. Но други странични ефекти от нежелания ред или тригери се избягват напълно.

Благодарение на Игор за идеята:

  • Postgresql, изберете "фалшив" ред

Премахнете 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 условно също:само ако входната стойност е NOT NULL . Добавих коментиран ред към UPDATE по-горе.

Настрана:Не е нужно да прехвърляте стойности във всеки ред, освен първия в 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 се виждат редове. Ако имате нужда от разликата, върнете се към предишното решение, където имате и двете на наше разположение.




  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Свържете се с база данни PostgreSQL в Docker контейнер

  2. Конкатениране на низ и число в PostgreSQL

  3. Rails Console намира потребители по масив от идентификатори

  4. кръстосана таблица с 2 (или повече) имена на редове

  5. Как да променя стойността на колоната по подразбиране в PostgreSQL?