PostgreSQL'de yinelenen güncelleme eklensin mi?


644

Birkaç ay önce aşağıdaki sözdizimini kullanarak Yığın Taşması ile ilgili bir cevaptan MySQL'de aynı anda birden çok güncellemenin nasıl gerçekleştirileceğini öğrendim:

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);

Şimdi PostgreSQL'e geçtim ve görünüşe göre bu doğru değil. Tüm doğru tablolara atıfta bulunuluyor, bu yüzden kullanılan farklı anahtar kelimeler meselesi olduğunu varsayıyorum, ancak PostgreSQL belgelerinde bu kapsamın nerede olduğundan emin değilim.

Açıklığa kavuşturmak için, birkaç şey eklemek istiyorum ve bunları güncellemek için zaten varlarsa.


38
Bu soruyu bulan herkes Depesz'in "upsert neden bu kadar karmaşık?" . Konuyu ve olası çözümleri son derece iyi açıklar.
Craig Ringer

8
UPSERT Postgres 9.5 eklenecek: wiki.postgresql.org/wiki/...
tommed

4
@tommed - yapıldı: stackoverflow.com/a/34639631/4418
warren

Yanıtlar:


515

9.5 sürümünden bu yana PostgreSQL , ON CONFLICT deyimiyle UPSERT sözdizimine sahiptir . aşağıdaki sözdizimiyle (MySQL'e benzer)

INSERT INTO the_table (id, column_1, column_2) 
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE 
  SET column_1 = excluded.column_1, 
      column_2 = excluded.column_2;

Postgresql'in e-posta grubu arşivlerini "upsert" için aramak , kılavuzda yapmak istediğiniz şeyi yapmanın bir örneğini bulmanıza yol açar :

Örnek 38-2. UPDATE / INSERT ile istisnalar

Bu örnek, uygun şekilde UPDATE veya INSERT gerçekleştirmek için kural dışı durum işleme kullanır:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        -- note that "a" must be unique
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

Bilgisayar korsanlarının posta listesinde 9.1 ve üstü CTE'leri kullanarak bunun toplu olarak nasıl yapılacağına dair bir örnek var :

WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;

Daha net bir örnek için a_horse_with_no_name'nin cevabına bakınız .


7
Bu konuda sevmediğim tek şey, çok daha yavaş olacağıdır, çünkü her upert, veritabanına kendi bireysel çağrısı olacaktır.
baash05

@ baash05 toplu olarak yapmanın bir yolu olabilir, güncellenmiş cevabımı görün.
Stephen Denne

2
Farklı yapacağım tek şey, sadece LOOP yerine FOR 1..2 LOOP kullanmaktır, böylece başka bir benzersiz kısıtlama ihlal edilirse süresiz olarak dönmez.
olamork

2
excludedBuradaki ilk çözümde ne ifade ediyor?
ichbinallen

2
@ichbinallen dokümanlar ON CONFLICT DO UPDATE içindeki SET ve WHERE yan tümceleri, tablonun adını (veya bir takma adı) kullanarak varolan satıra ve hariç tutulan özel tablo kullanılarak eklenmesi önerilen satırlara erişebilir . Bu durumda, özel excludedtablo ilk başta INSERT yapmaya çalıştığınız değerlere erişmenizi sağlar.
TMichel

429

Uyarı: Bu, aynı anda birden fazla oturumdan yürütüldüğünde güvenli değildir (aşağıdaki uyarılara bakın).


Postgresql içinde bir "UPSERT" yapmanın bir başka akıllı yolu, her biri başarılı olmak ya da hiçbir etkisi olmayacak şekilde tasarlanmış iki ardışık UPDATE / INSERT ifadesi yapmaktır.

UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
       SELECT 3, 'C', 'Z'
       WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);

UPDATE, "id = 3" olan bir satır zaten varsa başarılı olur, aksi takdirde etkisi olmaz.

INSERT yalnızca "id = 3" içeren satır yoksa başarılı olur.

Bu ikisini tek bir dizede birleştirebilir ve her ikisini de uygulamanızdan tek bir SQL deyimi ile çalıştırabilirsiniz. Bunları tek bir işlemde birlikte yürütmeniz önemle tavsiye edilir.

Bu, tek başına veya kilitli bir tabloda çalıştırıldığında çok iyi çalışır, ancak aynı anda bir satır eklenirse yinelenen anahtar hatasıyla başarısız olabileceği veya bir satır aynı anda silindiğinde hiçbir satır eklenmeden sonlanabileceği anlamına gelen yarış koşullarına tabidir. . SERIALIZABLEPostgreSQL 9.1 veya üzerindeki bir işlem, çok yüksek bir serileştirme başarısızlık oranı pahasına güvenilir bir şekilde işleyecektir, yani çok tekrar denemeniz gerekecektir. Upert'in neden bu kadar karmaşık olduğunu görün , bu durumu daha ayrıntılı olarak tartışıyor.

Bu yaklaşım, uygulama etkilenen satır sayısını denetlemez ve etkilenen satırın ya da etkilenen bir satır olduğunu doğrulamazsa , tek read committedbaşına kayıp güncelleştirmelere tabidirinsertupdate .


6
Kısa cevap: kayıt varsa INSERT hiçbir şey yapmaz. Uzun cevap: INSERT içindeki SELECT ifadesi where yan tümcesiyle eşleştiği kadar çok sonuç döndürecektir. Bu en fazla bir (bir numaralı alt seçimin sonucu değilse), aksi takdirde sıfırdır. INSERT böylece bir veya sıfır satır ekleyecektir.
Peter Becker

3
'nerede' kısmı kullanılarak basitleştirilebilir:... where not exists (select 1 from table where id = 3);
Endy Tjahjono

1
Bu doğru cevap olmalı .. bazı küçük tweaks ile, bir toplu güncelleme yapmak için kullanılabilir .. Humm .. Bir geçici tablo kullanılabilir merak ediyorum ..
baash05

1
@keaplogik, bu 9.1 sınırlamasının bir diğer cevapta açıklanan yazılabilir CTE (ortak tablo ifadeleri) ile olmasıdır. Bu cevapta kullanılan sözdizimi çok basittir ve uzun zamandır desteklenmektedir.
sığır

8
Uyarı bunda kayıp güncellenebilir read committedBaşvurunuz kontrolleri emin olmak için sürece izolasyon insertveya updatesıfır olmayan bir rowcount var. Bkz. Dba.stackexchange.com/q/78510/7788
Craig Ringer

227

PostgreSQL 9.1 ile bu yazılabilir bir CTE ( ortak tablo ifadesi ) kullanılarak gerçekleştirilebilir:

WITH new_values (id, field1, field2) as (
  values 
     (1, 'A', 'X'),
     (2, 'B', 'Y'),
     (3, 'C', 'Z')

),
upsert as
( 
    update mytable m 
        set field1 = nv.field1,
            field2 = nv.field2
    FROM new_values nv
    WHERE m.id = nv.id
    RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1 
                  FROM upsert up 
                  WHERE up.id = new_values.id)

Şu blog girişlerine bakın:


Not bu çözüm olmadığını değil benzersiz bir anahtar ihlali önlemek ancak kayıp güncellemeler karşı savunmasız değildir.
Bkz dba.stackexchange.com Craig Ringer tarafından takip yukarı


1
@ FrançoisBeausoleil: Bir yarış koşulu şansı "istisna / dene istisnası" yaklaşımından çok daha az
a_horse_with_no_name

2
@a_horse_with_no_name Yarış koşullarındaki şansın çok daha küçük olduğu anlamına nasıl geliyorsunuz? Ben aynı kayıt ile aynı anda bu sorguyu yürüttüğümde "yinelenen anahtar değeri benzersiz kısıtlamayı ihlal" sorgusu kayıt eklendiğini algılayana kadar% 100 hata alıyorum. Bu tam bir örnek mi?
Jeroen van Dijk

4
@a_horse_with_no_name upsert deyimini aşağıdaki kilitle sardığınızda çözümünüz eşzamanlı durumlarda çalışıyor gibi görünüyor: BEGIN WORK; PAYLAŞ SIRA ÖZEL MODUNDA KİLİT MASASI; <BURADA UPSERT>; ÇALIŞMA KOMİTESİ;
Jeroen van Dijk

2
@JeroenvanDijk: teşekkürler. Ne "çok daha küçük" ile kastettiğim bu (ve değişikliği taahhüt!) İçin birkaç işlem her şey sadece tek bir ifade olduğu gibi güncelleme ve insert arasındaki zaman aralığı daha küçük olmasıdır. Her zaman iki bağımsız INSERT deyimi tarafından bir pk ihlali oluşturabilirsiniz. Tüm tabloyu kilitlerseniz, ona tüm erişimi etkili bir şekilde serileştirirsiniz (serileştirilebilir yalıtım seviyesiyle de başarabileceğiniz bir şey).
a_horse_with_no_name

12
Ekleme işlemi geri alınırsa, bu çözüm kayıp güncellemelere tabidir; UPDATEetkilenen herhangi bir satırı zorlamak için herhangi bir kontrol yoktur .
Craig Ringer

132

PostgreSQL 9.5 ve daha yeni sürümlerde kullanabilirsiniz INSERT ... ON CONFLICT UPDATE.

Belgelere bakın .

Bir MySQL INSERT ... ON DUPLICATE KEY UPDATEdoğrudan a ON CONFLICT UPDATE. Her ikisi de SQL standardı sözdizimi değildir, ikisi de veritabanına özgü uzantılardır. Bunun MERGEiçin kullanılmamasının iyi nedenleri var, sadece eğlence için yeni bir sözdizimi oluşturulmadı. (MySQL'in sözdiziminin doğrudan kabul edilmediği anlamına gelen sorunları da vardır).

örneğin verilen kurulum:

CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);

MySQL sorgusu:

INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

dönüşür:

INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

farklılıklar:

  • Sen gerekir benzersizliği denetimi için kullanmak sütun adını (veya benzersiz kısıtlama adı) belirtin. İşteON CONFLICT (columnname) DO

  • Anahtar kelime SET, bu normal bir UPDATEifade gibi kullanılmalıdır

Bazı güzel özellikleri de var:

  • Bir olabilir WHEREsenin üzerine maddesi UPDATE(etkin oluşturmasını sağlayarak ON CONFLICT UPDATEiçine ON CONFLICT IGNOREbelirli değerleri)

  • Ekleme için önerilen değerler EXCLUDED, hedef tabloyla aynı yapıya sahip olan satır değişkeni olarak kullanılabilir. Tablo adını kullanarak tablodaki orijinal değerleri alabilirsiniz. Yani bu durumda EXCLUDED.colacak 10ve (biz eklemek için çalıştığı şey budur çünkü) "table".colacak 3o tablodaki geçerli değer olduğu için. SETİfadelerde ve deyimde birini veya her ikisini de kullanabilirsiniz WHERE.

Upert ile ilgili arka plan için PostgreSQL'de UPSERT (MERGE, INSERT ... DUPLICATE UPDATE ÜZERİNE) bölümüne bakınız.


Ben MySQL altında iken otomatik artış alanında boşluklar yaşıyorum çünkü ben yukarıda açıklandığı gibi PostgreSQL 9.5 çözüm baktım ON DUPLICATE KEY UPDATE. Postgres 9.5'i indirdim ve kodunuzu uyguladım, ancak garip bir şekilde Postgres altında oluşuyor: birincil anahtarın seri alanı ardışık değil (ekler ve güncellemeler arasında boşluklar var.). Burada neler olduğuna dair bir fikrin var mı? Bu normal mi? Bu davranıştan nasıl kaçınılacağına dair bir fikrin var mı? Teşekkür ederim.
WM

@WM Bu daha üst düzey bir operasyonun doğasında var. Eklemeyi denemeden önce diziyi oluşturan işlevi değerlendirmeniz gerekir. Bu sekanslar eşzamanlı olarak çalışmak üzere tasarlandıkları için normal işlem semantiğinden muaftırlar, ancak nesil olmasa bile nesil bir alt işlemde çağrılmaz ve geri alınmaz, normal olarak tamamlanır ve işlemin geri kalanıyla tamamlanır. Yani bu "aralıksız" dizi uygulamalarında bile olur. Veritabanının bundan kaçınmasının tek yolu, sıra denetiminin anahtar denetiminden sonraya kadar ertelenmesi olacaktır.
Craig Ringer

1
@WM kendi sorunlarını yaratacaktı. Temel olarak, sıkışıp kaldın. Ancak, seri / auto_increment'in boşluksa güveniyorsanız, zaten hatalarınız var. Hiç güvenmek, yük altında yeniden doğmuş, istemci hataları orta işlem, çöker, vb asla gerekir - Sen sebebiyle geçici hataları dahil olmak üzere geri almalar için dizi boşluklar olabilir SERIAL/ SEQUENCEveya AUTO_INCREMENTboşluklar olmaması. Boşluksuz dizilere ihtiyacınız varsa bunlar daha karmaşıktır; genellikle bir sayaç tablosu kullanmanız gerekir. Google size daha fazlasını anlatacak. Ancak boşluksuz dizilerin tüm uç eşzamanlılıklarını engellediğini unutmayın.
Craig Ringer

@WM Kesinlikle boşluksuz sekanslar ve upert gerektiriyorsa, bir sayaç tablosu kullanan boşluksuz bir sekans uygulaması ile birlikte kılavuzda tartışılan fonksiyon tabanlı upert yaklaşımını kullanabilirsiniz. BEGIN ... EXCEPTION ...Bir alt işlemdeki hataya geri döndürülen işlemler nedeniyle, INSERTbaşarısızlık durumunda sıra artışınız geri alınır .
Craig Ringer

Çok teşekkür ederim @Craig Ringer, bu oldukça bilgilendirici oldu. Bu otomatik artış birincil anahtarından vazgeçebileceğimi fark ettim. 3 alandan oluşan bir bileşik birincil yaptım ve özel şu anki ihtiyacım için, boşluksuz bir otomatik artış alanına gerek yok. Tekrar teşekkürler, sağladığınız bilgiler gelecekte doğal ve sağlıklı bir DB davranışını önlemeye çalışarak zaman kazandıracaktır. Şimdi daha iyi anlıyorum.
WM

17

Buraya geldiğimde aynı şeyi arıyordum, ancak genel bir "upert" fonksiyonunun eksikliği beni biraz rahatsız etti, bu yüzden güncellemeyi geçip kılavuzu bu fonksiyondaki argümanlar olarak sql ekleyebileceğinizi düşündüm

bu şöyle görünecektir:

CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
    RETURNS VOID
    LANGUAGE plpgsql
AS $$
BEGIN
    LOOP
        -- first try to update
        EXECUTE sql_update;
        -- check if the row is found
        IF FOUND THEN
            RETURN;
        END IF;
        -- not found so insert the row
        BEGIN
            EXECUTE sql_insert;
            RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- do nothing and loop
        END;
    END LOOP;
END;
$$;

ve belki de, preformance isabet bakın çok küçük olacaktır SQL_UPDATE ve döngü ayrı güncellemeleri bölmek için Tcl kullanabilirsiniz, başlangıçta, toplu "Upsert" yapmak istediğini yapmak http://archives.postgresql.org/pgsql- performans / 2006-04 / msg00557.php

en yüksek maliyet sorgunuzu kodunuzdan yürütmektir, veritabanı tarafında yürütme maliyeti çok daha düşüktür


3
Bunu yine bir yeniden deneme döngüsünde çalıştırmanız gerekiyor DELETEve tabloyu kilitlemediğiniz veya SERIALIZABLEPostgreSQL 9.1 veya daha üstündeki işlem yalıtımında olmadığınız sürece eşzamanlı olarak yarışlara yatkın .
Craig Ringer

13

Bunu yapmak için basit bir komut yoktur.

En doğru yaklaşım, dokümanlardaki gibi işlevi kullanmaktır .

Başka bir çözüm (bu kadar güvenli olmasa da), iade ile güncelleme yapmak, hangi satırların güncelleme olduğunu kontrol etmek ve geri kalanını eklemek

Çizgileri boyunca bir şey:

update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;

id: 2 döndürüldüğü varsayılarak:

insert into table (id, column) values (1, 'aa'), (3, 'cc');

Elbette, burada açık bir yarış durumu olduğu için er ya da geç (eşzamanlı ortamda) kurtarılacak, ancak genellikle işe yarayacak.

İşte konuyla ilgili daha uzun ve daha kapsamlı bir makale .


1
Bu seçeneği kullanıyorsanız, güncelleme hiçbir şey yapmasa bile kimliğin döndürüldüğünden emin olun. Ben "Güncelleme tablo foo set bar = 4 nerede bar = 4" gibi veritabanları optimize-away sorguları gördüm.
thelem

10

Şahsen, insert deyimine ekli bir "kural" kurdum. Diyelim ki, müşteri başına dns isabetlerini bir kereye mahsus olarak kaydeden bir "dns" tablonuz var:

CREATE TABLE dns (
    "time" timestamp without time zone NOT NULL,
    customer_id integer NOT NULL,
    hits integer
);

Güncellenmiş değerlere sahip satırları yeniden ekleyebilmek veya henüz yoksa satırlar oluşturmak istediniz. Customer_id ve saati girin. Bunun gibi bir şey:

CREATE RULE replace_dns AS 
    ON INSERT TO dns 
    WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time") 
            AND (dns.customer_id = new.customer_id)))) 
    DO INSTEAD UPDATE dns 
        SET hits = new.hits 
        WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));

Güncelleme: Unique_violation istisnaları oluşturacağından eşzamanlı insertler oluyorsa bu başarısız olma potansiyeline sahiptir. Ancak, sonlandırılmamış işlem devam eder ve başarılı olur ve sadece sonlandırılan işlemi tekrarlamanız gerekir.

Bununla birlikte, sürekli olarak tonlarca ek varsa, ekleme ifadelerinin çevresine bir tablo kilidi koymak isteyeceksiniz: SATIR SATIŞI ÖZEL kilitleme, hedef tablonuza satır ekleyebilen, silebilen veya güncelleyebilecek işlemleri önleyecektir. Ancak, benzersiz anahtarı güncellemeyen güncelleştirmeler güvenlidir, bu nedenle hiçbir işlem yapmazsanız, bunun yerine öneri kilitlerini kullanın.

Ayrıca, COPY komutu RULES kullanmaz, bu nedenle COPY ile ekliyorsanız, bunun yerine tetikleyiciler kullanmanız gerekir.


9

Bu işlevi birleştirme kullanıyorum

CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
  RETURNS void AS
$BODY$
BEGIN
    IF EXISTS(SELECT a FROM tabla WHERE a = key)
        THEN
            UPDATE tabla SET b = data WHERE a = key;
        RETURN;
    ELSE
        INSERT INTO tabla(a,b) VALUES (key, data);
        RETURN;
    END IF;
END;
$BODY$
LANGUAGE plpgsql

1
Sadece updateilkini yapmak ve daha sonra güncellenen satır sayısını kontrol etmek daha etkilidir . (Ahmed'in cevabına bakınız)
a_horse_with_no_name

8

INSERT VE REPLACE istiyorsanız, yukarıdaki özel "upsert" işlevi:

'

 CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)

 RETURNS void AS
 $BODY$
 BEGIN
    -- first try to insert and after to update. Note : insert has pk and update not...

    EXECUTE sql_insert;
    RETURN;
    EXCEPTION WHEN unique_violation THEN
    EXECUTE sql_update; 
    IF FOUND THEN 
        RETURN; 
    END IF;
 END;
 $BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
 ALTER FUNCTION upsert(text, text)
 OWNER TO postgres;`

Ve yürütmeden sonra, böyle bir şey yapın:

SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

Derleyici hatalarını önlemek için çift dolar virgül koymak önemlidir

  • hızı kontrol et ...

7

En çok beğenilen cevaba benzer, ancak biraz daha hızlı çalışır:

WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)

(kaynak: http://www.the-art-of-web.com/sql/upsert/ )


3
İki oturumda da eşzamanlı olarak çalıştırılırsa bu başarısız olur, çünkü her iki güncelleme de mevcut bir satırı görmez, bu nedenle her iki güncelleme de sıfır satıra çarpacaktır, bu nedenle her iki sorgu da bir ekleme yayınlayacaktır.
Craig Ringer

6

Hesap değerlerini ad değeri çiftleriyle yönetmekle aynı sorun yaşıyorum. Tasarım kriterleri, farklı istemcilerin farklı ayar setlerine sahip olabilmeleridir.

Çözümüm, JWP'ye benzer şekilde, uygulamanızda birleştirme kaydı oluşturarak toplu olarak silmek ve değiştirmek.

Bu oldukça kurşun geçirmez, platformdan bağımsızdır ve istemci başına yaklaşık 20'den fazla ayar olmadığından, bu sadece 3 oldukça düşük yük db çağrısıdır - muhtemelen en hızlı yöntem.

Tek tek satırları güncellemenin alternatifi - istisnaları kontrol edip ekleyerek - veya bazı kombinasyonları iğrenç kod, yavaş ve sık sık kesiliyor çünkü (yukarıda belirtildiği gibi) standart olmayan SQL istisna işleme db'den db'ye değişiyor - hatta serbest bırakılıyor.

 #This is pseudo-code - within the application:
 BEGIN TRANSACTION - get transaction lock
 SELECT all current name value pairs where id = $id into a hash record
 create a merge record from the current and update record
  (set intersection where shared keys in new win, and empty values in new are deleted).
 DELETE all name value pairs where id = $id
 COPY/INSERT merged records 
 END TRANSACTION

SO hoş geldiniz. Güzel tanıtım! :-)
Don Question

1
Bu daha REPLACE INTOçok INSERT INTO ... ON DUPLICATE KEY UPDATE, tetikleyicileri kullanırsanız soruna neden olabilir. Sonunda güncelleme güncellemeleri yerine silme ve tetikleme / kural ekleme işlemlerini gerçekleştireceksiniz.
cHao


5
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
  RETURNS boolean AS
$BODY$
BEGIN
    UPDATE users SET name = _name WHERE id = _id;
    IF FOUND THEN
        RETURN true;
    END IF;
    BEGIN
        INSERT INTO users (id, name) VALUES (_id, _name);
    EXCEPTION WHEN OTHERS THEN
            UPDATE users SET name = _name WHERE id = _id;
        END;
    RETURN TRUE;
END;

$BODY$
  LANGUAGE plpgsql VOLATILE STRICT

5

Küçük kümeleri birleştirmek için yukarıdaki işlevi kullanmak iyidir. Ancak, büyük miktarda veri birleştiriyorsanız, http://mbk.projects.postgresql.org adresine bakmanızı öneririm.

Şu anda farkında olduğum en iyi uygulama:

  1. Yeni / güncellenmiş verileri geçici tabloya KOPYALA (emin olun veya maliyet uygunsa INSERT yapabilirsiniz)
  2. Kilidi Al [isteğe bağlı] (danışma, masa kilitleri, IMO için tercih edilir)
  3. Birleştirmek. (eğlenceli kısım)

5

UPDATE, değiştirilen satır sayısını döndürür. JDBC (Java) kullanıyorsanız, bu değeri 0'a karşı kontrol edebilirsiniz ve hiçbir satır etkilenmemişse bunun yerine INSERT komutunu verin. Başka bir programlama dili kullanıyorsanız, değiştirilmiş satırların sayısı hala elde edilebilir, belgelere bakın.

Bu kadar zarif olmayabilir, ancak arama kodundan daha önemsiz olan daha basit bir SQL'e sahipsiniz. Farklı bir şekilde, on satır komut dosyasını PL / PSQL'de yazarsanız, muhtemelen yalnızca bunun için bir veya başka türden bir birim testine sahip olmalısınız.


4

Düzenleme: Bu beklendiği gibi çalışmıyor. Kabul edilen cevabın aksine, iki süreç tekrar tekrar upsert_fooaynı anda çağırdığında bu benzersiz anahtar ihlalleri üretir .

Eureka! Bir sorguda bunu yapmak için bir yol düşündüm: UPDATE ... RETURNINGherhangi bir satır etkilenmiş olup olmadığını sınamak için kullanın :

CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);

CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
    UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;

CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
    INSERT INTO foo
        SELECT $1, $2
        WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;

Ne UPDATEyazık ki, bu bir sözdizimi hatası olduğu için ayrı bir yordamda yapılması gerekir:

... WHERE NOT EXISTS (UPDATE ...)

Şimdi istendiği gibi çalışıyor:

SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');

1
Yazılabilir bir CTE kullanıyorsanız bunları tek bir ifadede birleştirebilirsiniz. Ancak burada yayınlanan çoğu çözüm gibi, bu da yanlıştır ve eşzamanlı güncellemeler varlığında başarısız olacaktır.
Craig Ringer
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.