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

Jak používat RETURNING s ON CONFLICT v PostgreSQL?

Aktuálně přijímaná odpověď se zdá být v pořádku pro jediný konfliktní cíl, málo konfliktů, malé n-tice a žádné spouštěče. Vyhne se problému souběžnosti 1 (viz níže) hrubou silou. Jednoduché řešení má svou přitažlivost, vedlejší účinky mohou být méně důležité.

Ve všech ostatních případech to však nedělejte aktualizovat identické řádky bez potřeby. I když na povrchu nevidíte žádný rozdíl, existují různé vedlejší účinky :

  • Může spouštět spouštěče, které by se spouštět neměly.

  • Zablokuje „nevinné“ řádky, což může způsobit náklady na souběžné transakce.

  • Řádek se může zdát nový, i když je starý (časové razítko transakce).

  • To je nejdůležitější , s modelem MVCC PostgreSQL je nová verze řádku zapsána pro každou UPDATE , bez ohledu na to, zda se data řádku změnila. To způsobí penalizaci výkonu pro samotný UPSERT, bloat tabulky, index bloat, penalizaci výkonu pro následné operace na stole, VACUUM náklady. Menší efekt pro několik duplikátů, ale masivní pro většinou podvodníky.

Plus , někdy není praktické nebo dokonce možné použít ON CONFLICT DO UPDATE . Manuál:

Pro ON CONFLICT DO UPDATE , conflict_target musí být poskytnuto.

Jedna "konfliktní cíl" není možný, pokud se jedná o více indexů / omezení. Zde je však související řešení pro více dílčích indexů:

  • UPSERT na základě omezení UNIQUE s hodnotami NULL

Zpět k tématu, můžete dosáhnout (téměř) stejného bez prázdných aktualizací a vedlejších efektů. Některá z následujících řešení také fungují s ON CONFLICT DO NOTHING (žádný „konfliktní cíl“), abyste chytili všechny možné konflikty, které by mohly nastat – což může nebo nemusí být žádoucí.

Bez souběžného zatížení zápisu

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, ins AS (
   INSERT INTO chats (usr, contact, name) 
   SELECT * FROM input_rows
   ON CONFLICT (usr, contact) DO NOTHING
   RETURNING id  --, usr, contact              -- return more columns?
   )
SELECT 'i' AS source                           -- 'i' for 'inserted'
     , id  --, usr, contact                    -- return more columns?
FROM   ins
UNION  ALL
SELECT 's' AS source                           -- 's' for 'selected'
     , c.id  --, usr, contact                  -- return more columns?
FROM   input_rows
JOIN   chats c USING (usr, contact);           -- columns of unique index

source sloupec je volitelný doplněk, který demonstruje, jak to funguje. Možná to budete potřebovat, abyste poznali rozdíl mezi oběma případy (další výhoda oproti prázdným zápisům).

Poslední JOIN chats funguje, protože nově vložené řádky z připojeného CTE upravujícího data ještě nejsou v podkladové tabulce viditelné. (Všechny části stejného příkazu SQL vidí stejné snímky podkladových tabulek.)

Protože VALUES výraz je volně stojící (není přímo připojen k INSERT ) Postgres nemůže odvodit datové typy z cílových sloupců a možná budete muset přidat explicitní přetypování typu. Manuál:

Když VALUES se používá v INSERT , jsou všechny hodnoty automaticky převedeny na datový typ odpovídajícího cílového sloupce. Pokud se používá v jiných kontextech, může být nutné zadat správný datový typ. Pokud jsou všechny položky citované doslovné konstanty, vynucení první postačí k určení předpokládaného typu pro všechny.

Samotný dotaz (nepočítáme-li vedlejší účinky) může být pro málo trochu dražší dupes, kvůli režii CTE a dalšímu SELECT (což by mělo být levné, protože dokonalý index existuje již z definice – s indexem je implementováno jedinečné omezení).

Může být (mnohem) rychlejší pro mnohé duplikáty. Efektivní náklady na další zápisy závisí na mnoha faktorech.

Existuje však méně vedlejších účinků a skrytých nákladů v každém případě. Je to s největší pravděpodobností celkově levnější.

Připojené sekvence jsou stále pokročilé, protože výchozí hodnoty jsou vyplněny před testování na konflikty.

O CTE:

  • Jsou dotazy typu SELECT jediným typem, který lze vnořit?
  • Deduplikujte příkazy SELECT v relačním dělení

Se současným zatížením zápisu

Za předpokladu výchozího nastavení READ COMMITTED izolace transakcí. Související:

  • Souběžné transakce vedou ke sporu s jedinečným omezením vložení

Nejlepší strategie obrany proti rasovým podmínkám závisí na přesných požadavcích, počtu a velikosti řádků v tabulce a v souborech UPSERT, počtu souběžných transakcí, pravděpodobnosti konfliktů, dostupných zdrojích a dalších faktorech ...

Problém souběžnosti 1

Pokud se souběžná transakce zapsala do řádku, který se nyní vaše transakce pokouší UPSERT, vaše transakce musí počkat, až se dokončí ta druhá.

Pokud druhá transakce končí ROLLBACK (nebo jakákoli chyba, např. automatický ROLLBACK ), vaše transakce může pokračovat normálně. Menší možný vedlejší účinek:mezery v pořadových číslech. Ale žádné chybějící řádky.

Pokud druhá transakce skončí normálně (implicitní nebo explicitní COMMIT ), váš INSERT detekuje konflikt (UNIQUE index / omezení je absolutní) a DO NOTHING , tudíž také nevrací řádek. (Také nelze zamknout řádek, jak je ukázáno v souběžném problému 2 níže, protože není vidět .) SELECT vidí stejný snímek od začátku dotazu a také nemůže vrátit dosud neviditelný řádek.

Všechny takové řádky v sadě výsledků chybí (i když v podkladové tabulce existují)!

Toto může být v pořádku . Zvláště pokud nevracíte řádky jako v příkladu a jste spokojeni s vědomím, že tam řádek je. Pokud to nestačí, existují různé způsoby, jak to obejít.

Můžete zkontrolovat počet řádků na výstupu a zopakovat příkaz, pokud neodpovídá počtu řádků na vstupu. Může být dost dobré pro vzácný případ. Jde o to, spustit nový dotaz (může být ve stejné transakci), který pak uvidí nově potvrzené řádky.

Nebo zkontrolujte chybějící řádky výsledků v rámci stejný dotaz a přepsat ti s trikem s hrubou silou demonstrovaným v Alextoniho odpovědi.

WITH input_rows(usr, contact, name) AS ( ... )  -- see above
, ins AS (
   INSERT INTO chats AS c (usr, contact, name) 
   SELECT * FROM input_rows
   ON     CONFLICT (usr, contact) DO NOTHING
   RETURNING id, usr, contact                   -- we need unique columns for later join
   )
, sel AS (
   SELECT 'i'::"char" AS source                 -- 'i' for 'inserted'
        , id, usr, contact
   FROM   ins
   UNION  ALL
   SELECT 's'::"char" AS source                 -- 's' for 'selected'
        , c.id, usr, contact
   FROM   input_rows
   JOIN   chats c USING (usr, contact)
   )
, ups AS (                                      -- RARE corner case
   INSERT INTO chats AS c (usr, contact, name)  -- another UPSERT, not just UPDATE
   SELECT i.*
   FROM   input_rows i
   LEFT   JOIN sel   s USING (usr, contact)     -- columns of unique index
   WHERE  s.usr IS NULL                         -- missing!
   ON     CONFLICT (usr, contact) DO UPDATE     -- we've asked nicely the 1st time ...
   SET    name = c.name                         -- ... this time we overwrite with old value
   -- SET name = EXCLUDED.name                  -- alternatively overwrite with *new* value
   RETURNING 'u'::"char" AS source              -- 'u' for updated
           , id  --, usr, contact               -- return more columns?
   )
SELECT source, id FROM sel
UNION  ALL
TABLE  ups;

Je to jako dotaz výše, ale přidáme ještě jeden krok s CTE ups , než vrátíme kompletní sada výsledků. To poslední CTE většinu času nic neudělá. Pouze pokud ve vráceném výsledku chybí řádky, použijeme hrubou sílu.

Ještě více nad hlavou. Čím více konfliktů s již existujícími řádky, tím je pravděpodobnější, že to překoná jednoduchý přístup.

Jeden vedlejší efekt:2. UPSERT zapisuje řádky mimo pořadí, takže znovu zavádí možnost uváznutí (viz níže), pokud tři nebo více transakce zapisované do stejných řádků se překrývají. Pokud se jedná o problém, potřebujete jiné řešení – například opakování celého výše uvedeného prohlášení.

Problém souběžnosti 2

Pokud se souběžné transakce mohou zapisovat do příslušných sloupců ovlivněných řádků a vy se musíte ujistit, že nalezené řádky tam jsou i v pozdější fázi stejné transakce, můžete zamknout existující řádky levně v CTE ins (který by se jinak odemkl) pomocí:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

A přidejte zamykací klauzuli do SELECT také jako FOR UPDATE .

To způsobí, že konkurenční operace zápisu čekají na konec transakce, kdy se uvolní všechny zámky. Takže buďte stručný.

Další podrobnosti a vysvětlení:

  • Jak zahrnout vyloučené řádky do RETURNING z INSERT ... ON CONFLICT
  • Je SELECT nebo INSERT ve funkci náchylný k závodům?

Zablokování?

Obraňte se proti blokům vkládáním řádků v konzistentním pořadí . Viz:

  • Zablokování u víceřádkových INSERTů navzdory tomu, že ON CONFLICT NEDĚLÁ NIC

Datové typy a přetypování

Stávající tabulka jako šablona pro datové typy ...

Explicitní typ přetypování pro první řádek dat ve samostatně stojícím VALUES výraz může být nepohodlný. Jsou cesty kolem toho. Jako šablonu řádku můžete použít jakýkoli existující vztah (tabulku, pohled, ...). Cílová tabulka je jasnou volbou pro případ použití. Vstupní data jsou automaticky převedena na příslušné typy, jako v VALUES klauzule INSERT :

WITH input_rows AS (
  (SELECT usr, contact, name FROM chats LIMIT 0)  -- only copies column names and types
   UNION ALL
   VALUES
      ('foo1', 'bar1', 'bob1')  -- no type casts here
    , ('foo2', 'bar2', 'bob2')
   )
   ...

U některých datových typů to nefunguje. Viz:

  • Při aktualizaci více řádků odesílání typu NULL

... a jména

To také funguje pro všechny datové typy.

Při vkládání do všech (úvodních) sloupců tabulky můžete názvy sloupců vynechat. Za předpokladu tabulky chats v příkladu se skládá pouze ze 3 sloupců použitých v UPSERT:

WITH input_rows AS (
   SELECT * FROM (
      VALUES
      ((NULL::chats).*)         -- copies whole row definition
      ('foo1', 'bar1', 'bob1')  -- no type casts needed
    , ('foo2', 'bar2', 'bob2')
      ) sub
   OFFSET 1
   )
   ...

Stranou:nepoužívejte vyhrazená slova jako "user" jako identifikátor. To je nabitá kopačka. Používejte legální identifikátory s malými písmeny, bez uvozovek. Nahradil jsem ho usr .



  1. Jak změnit hodnotu automatického přírůstku databáze MySQL / MariaDB

  2. Proč jsem obdržel tuto výjimku SQLSyntaxError:ORA-00933:Příkaz SQL nebyl správně ukončen, když se pokouším provést tento dotaz JDBC?

  3. Přehled replikace na úrovni svazku pro PostgreSQL pomocí DRBD

  4. Jak získat Insert id v MSSQL v PHP?