sql >> Databáze >  >> RDS >> PostgreSQL

Generujte DEFAULT hodnoty v CTE UPSERT pomocí PostgreSQL 9.3

Postgres 9.5 implementoval UPSERT . Viz níže.

Postgres 9.4 nebo starší

To je zapeklitý problém. Narážíte na toto omezení (podle dokumentace):

V VALUES seznam zobrazený na nejvyšší úrovni INSERT , výraz může být nahrazen výrazem DEFAULT označující, že by měla být vložena výchozí hodnota cílového sloupce. DEFAULT nelze použít, když VALUES se objevuje v jiných kontextech.

Odvážný důraz můj. Výchozí hodnoty nejsou definovány bez tabulky pro vložení. Neexistuje tedy žádný přímý řešení vaší otázky, ale existuje řada možných alternativních cest v závislosti na přesných požadavcích .

Načíst výchozí hodnoty ze systémového katalogu?

Mohli byste načtěte je ze systémového katalogu pg_attrdef jako komentář @Patrick nebo z information_schema.columns . Kompletní pokyny zde:

  • Získat výchozí hodnoty sloupců tabulky v Postgresu?

Ale pak pořád mít pouze seznam řádků s textovou reprezentací výrazu pro vaření výchozí hodnoty. Abyste získali hodnoty, se kterými byste mohli pracovat, museli byste vytvářet a provádět příkazy dynamicky. Nudné a chaotické. Místo toho můžeme nechat vestavěnou funkci Postgres, aby to udělala za nás :

Jednoduchá zkratka

Vložte fiktivní řádek a nechte jej vrátit, aby používal vygenerované výchozí hodnoty:

INSERT INTO playlist_items DEFAULT VALUES RETURNING *;

Problémy / rozsah řešení

  • Je zaručeno, že to bude fungovat pouze pro STABLE nebo IMMUTABLE výchozí výrazy . Nejvíce VOLATILE funkce budou fungovat stejně dobře, ale neexistují žádné záruky. current_timestamp rodina funkcí se kvalifikuje jako stabilní, protože jejich hodnoty se v rámci transakce nemění.
    To má zejména vedlejší účinky na serial sloupce (nebo jakékoli jiné výchozí hodnoty kreslené ze sekvence). To by ale neměl být problém, protože do serial běžně nezapisujete sloupce přímo. Ty by neměly být uvedeny v INSERT vůbec.
    Zbývající chyba pro serial sloupce:sekvence je stále posouvána jediným voláním, aby se získal výchozí řádek, což vytváří mezeru v číslování. Opět by to neměl být problém, protože mezery obecně lze očekávat v serial sloupce.

Lze vyřešit další dva problémy:

  • Pokud máte definované sloupce NOT NULL , musíte vložit fiktivní hodnoty a nahradit je NULL ve výsledku.

  • Ve skutečnosti nechceme vložit fiktivní řádek . Můžeme smazat později (v rámci stejné transakce), ale to může mít více vedlejších účinků, jako jsou spouštěče ON DELETE . Existuje lepší způsob:

Vyhněte se fiktivní řadě

Naklonujte dočasnou tabulku včetně výchozích hodnot sloupců a vložit do toho :

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

Stejný výsledek, méně vedlejších účinků. Vzhledem k tomu, že výchozí výrazy jsou kopírovány doslovně, klon kreslí ze stejných sekvencí, pokud existují. Ale jiným vedlejším účinkům nechtěného řádku nebo spouštěčů se zcela zabrání.

Poděkování Igorovi za nápad:

  • Postgresql, vyberte „falešný“ řádek

Odstraňte NOT NULL omezení

Budete muset zadat fiktivní hodnoty pro NOT NULL sloupců, protože (podle dokumentace):

Nenulová omezení se vždy zkopírují do nové tabulky.

Buď se přizpůsobí těm v INSERT nebo (lépe) odstranit omezení:

ALTER TABLE tmp_playlist_items
   ALTER COLUMN foo DROP NOT NULL
 , ALTER COLUMN bar DROP NOT NULL;

Existuje rychlá a špinavá cesta s oprávněními superuživatele:

UPDATE pg_attribute
SET    attnotnull = FALSE
WHERE  attrelid = 'tmp_playlist_items'::regclass
AND    attnotnull
AND    attnum > 0;

Je to jen dočasná tabulka bez dat a bez dalšího účelu a na konci transakce je vypuštěna. Takže zkratka je lákavá. Přesto základní pravidlo zní:nikdy nemanipulujte přímo se systémovými katalogy.

Pojďme se tedy podívat na čistý způsob :Automatizujte pomocí dynamického SQL v DO prohlášení. Potřebujete pouze běžná oprávnění zaručeně budete mít, protože stejná role vytvořila dočasnou tabulku.

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$$

Mnohem čistší a stále velmi rychlý. Buďte opatrní s dynamickými příkazy a dávejte si pozor na SQL injection. Toto prohlášení je bezpečné. Poslal jsem několik souvisejících odpovědí s podrobnějším vysvětlením.

Obecné řešení (9.4 a starší)

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;

Potřebujete pouze LOCK pokud máte souběžné transakce, které se snaží zapisovat do stejné tabulky.

Jak bylo požadováno, nahradí pouze hodnoty NULL ve sloupci legacy ve vstupních řádcích pro INSERT případ. Může být snadno rozšířen tak, aby fungoval pro další sloupce nebo v UPDATE případ také. Můžete například UPDATE podmíněně také:pouze pokud je vstupní hodnota NOT NULL . Přidal jsem do UPDATE řádek s komentářem výše.

Stranou:Nemusíte obsazovat hodnoty v libovolném řádku kromě prvního v VALUES výraz, protože typy jsou odvozeny od prvního řádek.

Postgres 9.5

implementuje UPSERT s INSERT .. ON CONFLICT .. DO NOTHING | UPDATE . To značně zjednodušuje operaci:

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;

Můžeme připojit VALUES klauzule na INSERT přímo, což umožňuje DEFAULT klíčové slovo. V případě jedinečných porušení na (id) , místo toho se aktualizuje Postgres. Můžeme použít vyloučené řádky v UPDATE . Manuál:

SET a WHERE klauzule v ON CONFLICT DO UPDATE mít přístup k existujícímu řádku pomocí názvu tabulky (nebo aliasu) a k řádkům navrženým pro vložení pomocí speciálního excluded tabulka.

A:

Všimněte si, že účinky všech na řádek BEFORE INSERT spouštěče se odrážejí ve vyloučených hodnotách, protože tyto efekty mohly přispět k vyloučení řádku z vložení.

Zbývající rohové pouzdro

Pro UPDATE máte různé možnosti :Můžete ...

  • ... neaktualizovat vůbec:přidejte WHERE klauzule do UPDATE zapisovat pouze do vybraných řádků.
  • ... aktualizujte pouze vybrané sloupce.
  • ... aktualizujte, pouze pokud je sloupec aktuálně NULL:COALESCE(l.legacy, EXCLUDED.legacy)
  • ... aktualizujte pouze v případě, že nová hodnota je NOT NULL :COALESCE(EXCLUDED.legacy, l.legacy)

Ale neexistuje způsob, jak rozeznat DEFAULT hodnoty a hodnoty skutečně poskytnuté v INSERT . Pouze výsledné EXCLUDED řádky jsou viditelné. Pokud potřebujete rozlišení, vraťte se k předchozímu řešení, kde máte k dispozici obojí.




  1. 3 způsoby, jak najít pozici podřetězce v řetězci v MySQL

  2. Porozumění tomu, jak Android.com ukládá data v SQL Database Tutorial

  3. Ukládání a analýza dokumentů v systému souborů Windows pomocí sémantického vyhledávání SQL Server – část 2

  4. Vztah mezi katalogem, schématem, uživatelem a instancí databáze