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á vINSERT
, 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
.