PostgreSQL'de UPSERT (MERGE, INSERT… DUPLICATE UPDATE ÜZERİNE) nasıl?


267

Burada çok sık sorulan bir soru, MySQL'in çağırdığı INSERT ... ON DUPLICATE UPDATEve standardın MERGEoperasyonun bir parçası olarak desteklediği bir upert'in nasıl yapılacağıdır .

PostgreSQL'in doğrudan desteklemediği göz önüne alındığında (sayfa 9.5'ten önce), bunu nasıl yapıyorsunuz? Aşağıdakileri göz önünde bulundur:

CREATE TABLE testtable (
    id integer PRIMARY KEY,
    somedata text NOT NULL
);

INSERT INTO testtable (id, somedata) VALUES
(1, 'fred'),
(2, 'bob');

Artık "Upsert" dizilerini istediğinizi düşünün (2, 'Joe'), (3, 'Alan')yeni tablo içeriği olurdu böylece:

(1, 'fred'),
(2, 'Joe'),    -- Changed value of existing tuple
(3, 'Alan')    -- Added new tuple

İnsanların bir upsert. En önemlisi, herhangi bir yaklaşım , aynı tabloda çalışan birden fazla işlemin varlığında - açık kilitleme kullanarak veya sonuçta ortaya çıkan yarış koşullarına karşı başka bir şekilde savunarak - güvenli olmalıdır .

Bu konu PostgreSQL'deki yinelenen güncellemeyle Insert'de kapsamlı olarak tartışılıyor mu? , ancak bu MySQL sözdizimine alternatiflerle ilgilidir ve zamanla ilgisiz bir ayrıntıya dönüşmüştür. Kesin cevaplar üzerinde çalışıyorum.

Bu teknikler "yoksa, başka türlü hiçbir şey yapmayın", yani "yinelenen anahtar yoksaymasına ekle ..." için de kullanışlıdır.



8
@MichaelHampton buradaki amaç, çok sayıda eski cevabı karıştırmayan ve kilitli olan kesin bir sürüm oluşturmaktı, böylece kimse bu konuda hiçbir şey yapamaz. Katılıma katılmıyorum.
Craig Ringer

Neden, bu yakında modası geçmiş ve kilitli olacak, böylece kimse bu konuda hiçbir şey yapamazdı.
Michael Hampton

2
@MichaelHampton Eğer endişeleniyorsanız, belki bağlandığınız kişiyi işaretleyebilir ve kilidinin temizlenmesini isteyebilirsiniz, o zaman bunu birleştirebiliriz. Sadece açık olan tek yakınlıktan bıktım upert için böyle kafa karıştırıcı ve yanlış bir karışıklık olduğu gibi.
Craig Ringer

1
Soru-Cevap kilitli değil!
Michael Hampton

Yanıtlar:


396

9.5 ve daha yenisi:

PostgreSQL 9.5 ve daha yeni destek INSERT ... ON CONFLICT UPDATE(ve ON CONFLICT DO NOTHING), yani upert.

İle karşılaştırmaON DUPLICATE KEY UPDATE .

Hızlı açıklama .

Kullanım için el kitabına bakın - özellikle sözdizimi diyagramındaki çakışma_etlemi yan tümcesine ve açıklayıcı metne .

Aşağıda verilen 9.4 ve daha eski çözümlerden farklı olarak, bu özellik birden çok çakışan satırla çalışır ve özel kilitleme veya yeniden deneme döngüsü gerektirmez.

Özelliği ekleme taahhüdü burada ve geliştirilmesiyle ilgili tartışma burada .


9.5 kullanıyorsanız ve geriye dönük uyumlu olmanız gerekmiyorsa, şimdi okumayı durdurabilirsiniz .


9.4 ve üstü:

PostgreSQL'in yerleşik UPSERT(veya MERGE) bir tesisi yoktur ve eşzamanlı kullanım karşısında verimli bir şekilde yapmak çok zordur.

Bu makalede, sorunu ayrıntılı olarak açıklanmaktadır .

Genel olarak iki seçenek arasında seçim yapmanız gerekir:

  • Yeniden deneme döngüsünde tek tek ekleme / güncelleme işlemleri; veya
  • Masayı kilitleme ve toplu birleştirme yapıyor

Tek satır yeniden deneme döngüsü

Birden çok bağlantıyı aynı anda ekler yapmaya çalışırken istiyorsanız, yeniden deneme döngüsünde tek tek satır üstbilgileri kullanmak makul bir seçenektir.

PostgreSQL belgeleri, bunu veritabanının içindeki bir döngüde yapmanıza izin veren kullanışlı bir yordam içerir . Çoğu naif çözümün aksine, kayıp güncellemelere karşı koruma sağlar ve yarışları yerleştirir. Sadece READ COMMITTEDmodda çalışır ve ancak işlemde yaptığınız tek şey güvenli olduğunda güvenlidir. Tetikleyiciler veya ikincil benzersiz anahtarlar benzersiz ihlallere neden olursa işlev düzgün çalışmaz.

Bu strateji çok verimsiz. Ne zaman pratik olursanız olun, işi sıraya almalı ve bunun yerine aşağıda açıklandığı gibi bir toplu destek yapmalısınız.

Bu soruna yönelik birçok girişim çözümü geri alma işlemlerini dikkate almaz, bu nedenle eksik güncellemelerle sonuçlanır. İki işlem birbiriyle yarışır; bunlardan biri başarıyla INSERTs; diğeri yinelenen bir anahtar hatası alır ve UPDATEbunun yerine bir anahtar yapar . UPDATEİçin bloklar bekleyen INSERTgeri alma veya taahhüt. Geri döndüğünde, UPDATEkoşulun yeniden kontrolü sıfır satırla eşleşir, bu nedenle UPDATEtaahhütler gerçekte beklediğiniz yükselmeyi yapmamış olsa bile . Sonuç satırı sayılarını kontrol etmeniz ve gerektiğinde yeniden denemeniz gerekir.

Bazı denenmiş çözümler SELECT ırklarını dikkate almaz. Açık ve basit olanı denerseniz:

-- THIS IS WRONG. DO NOT COPY IT. It's an EXAMPLE.

BEGIN;

UPDATE testtable
SET somedata = 'blah'
WHERE id = 2;

-- Remember, this is WRONG. Do NOT COPY IT.

INSERT INTO testtable (id, somedata)
SELECT 2, 'blah'
WHERE NOT EXISTS (SELECT 1 FROM testtable WHERE testtable.id = 2);

COMMIT;

daha sonra iki seferde birden fazla hata modu olduğunda. Bunlardan biri, güncellemenin yeniden kontrol edilmesinde zaten tartışılan konudur. Bir diğeri, her ikisinin UPDATEde aynı anda sıfır satırı eşleştirmesi ve devam etmesidir. Sonra ikisini de EXISTSolur testini önceINSERT . Her ikisi de sıfır satır alır, her ikisi de INSERT. Biri yinelenen bir anahtar hatasıyla başarısız oluyor.

Bu yüzden bir yeniden deneme döngüsüne ihtiyacınız var. Akıllı SQL ile yinelenen anahtar hatalarını veya kayıp güncellemeleri önleyebileceğinizi düşünebilirsiniz, ancak yapamazsınız. Satır sayısını kontrol etmeniz veya yinelenen anahtar hatalarını (seçilen yaklaşıma bağlı olarak) ele almanız ve tekrar denemeniz gerekir.

Lütfen bunun için kendi çözümünüzü atmayın. Mesaj kuyruğunda olduğu gibi, muhtemelen yanlış.

Kilitli toplu upsert

Bazen, daha eski bir veri kümesiyle birleştirmek istediğiniz yeni bir veri kümesine sahip olduğunuz toplu upert yapmak istersiniz. Bu, bireysel sıra destekçilerinden çok daha verimlidir ve pratik olduğunda tercih edilmelidir.

Bu durumda, genellikle aşağıdaki işlemi izlersiniz:

  • CREATEbir TEMPORARYmasa

  • COPY veya yeni verileri geçici tabloya toplu olarak ekleyin

  • LOCKhedef tablo IN EXCLUSIVE MODE. Bu, başka işlemlere izin verir SELECT, ancak tabloda değişiklik yapmaz.

  • UPDATE ... FROMGeçici tablodaki değerleri kullanarak varolan kayıtlardan birini yapın ;

  • INSERTHedef tabloda henüz bulunmayan satırlardan birini yapın ;

  • COMMIT, kilidi serbest bırakır.

Örneğin, soruda verilen örnek için INSERT, geçici tabloyu doldurmak için çok değerli kullanma:

BEGIN;

CREATE TEMPORARY TABLE newvals(id integer, somedata text);

INSERT INTO newvals(id, somedata) VALUES (2, 'Joe'), (3, 'Alan');

LOCK TABLE testtable IN EXCLUSIVE MODE;

UPDATE testtable
SET somedata = newvals.somedata
FROM newvals
WHERE newvals.id = testtable.id;

INSERT INTO testtable
SELECT newvals.id, newvals.somedata
FROM newvals
LEFT OUTER JOIN testtable ON (testtable.id = newvals.id)
WHERE testtable.id IS NULL;

COMMIT;

İlgili okuma

Ne olmuş MERGE?

SQL standardı MERGEaslında kötü tanımlanmış eşzamanlılık semantiğine sahiptir ve önce bir tabloyu kilitlemeden yükseltme için uygun değildir.

Veri birleştirme için gerçekten yararlı bir OLAP ifadesi, ancak aslında eşzamanlı güvenli destek için yararlı bir çözüm değil. Diğer DBMS'leri kullanıcıları için kullanmak MERGEüzere birçok öneri var, ama aslında yanlış.

Diğer DB'ler:


Toplu upertte, INSERT'e filtre uygulamak yerine yeni değerlerden silme konusunda olası bir değer var mı? Örneğin upd AS İLE (GÜNCELLEME ... newvals.id) DÖNÜYOR newvals.id = upd.id = upd.id NEREDEN KULLANILAN newvals'DEN SİL, ardından test edilebilir SELECT * 'dan çıplak bir INSERT *? Bununla ilgili fikrim: INSERT'te (JOIN / WHERE ve benzersiz kısıtlama için) iki kez filtrelemek yerine, zaten RAM'de olan ve çok daha küçük olabilecek UPDATE'ten varlık kontrolü sonuçlarını yeniden kullanın. Birkaç sıra eşleşen ve / veya yeni değer test testinden çok daha küçükse bu bir kazanç olabilir.
Gunnlaugur Briem

1
Hala çözülmemiş sorunlar var ve diğer satıcılar için neyin işe yarayıp neyin yaramadığı belli değil. 1. Belirtildiği gibi Postgres döngü çözümü, birden fazla benzersiz anahtar durumunda çalışmaz. 2. mysql için yinelenen anahtar da birden çok benzersiz anahtar için çalışmaz. 3. Yukarıda yayınlanan MySQL, SQL Server ve Oracle için diğer çözümler işe yarıyor mu? Bu durumlarda istisnalar mümkün müdür ve bu döngüyü tekrarlamak zorunda mıyız?
dan b

@danb Bu sadece PostgreSQL ile ilgilidir. Çapraz satıcı çözümü yoktur. PostgreSQL çözümü birden çok satır için çalışmaz, ne yazık ki her satırda bir işlem yapmanız gerekir. MERGESQL Server ve Oracle için kullanılan "çözümler" yanlıştır ve yukarıda belirtildiği gibi yarış koşullarına yatkındır. Onlarla nasıl başa çıkacağınızı öğrenmek için özellikle her DBMS'ye bakmanız gerekecek, gerçekten sadece PostgreSQL hakkında tavsiyede bulunabilirim. PostgreSQL'de çok satırlı güvenli bir üst destek yapmanın tek yolu, yerel üst düzey desteğin çekirdek sunucuya eklenmesi olacaktır.
Craig Ringer

PostGresQL için bile, bir tablonun birden fazla benzersiz anahtarı olduğu durumda (sadece bir satırı güncelleyerek) çözüm çalışmaz. Bu durumda, hangi anahtarın güncelleneceğini belirtmeniz gerekir. Örneğin jdbc kullanan bir çapraz satıcı çözümü olabilir.
dan b

2
Postgres artık UPSERT'i
Chris

32

PostgreSQL'in 9.5 öncesi sürümleriyle tek ekleme sorunu için başka bir çözümle katkıda bulunmaya çalışıyorum. Fikir, ilk olarak ekleme işlemini gerçekleştirmeye çalışmak ve kaydın zaten mevcut olması durumunda, güncellemek:

do $$
begin 
  insert into testtable(id, somedata) values(2,'Joe');
exception when unique_violation then
  update testtable set somedata = 'Joe' where id = 2;
end $$;

Bu çözümün yalnızca tablonun satırlarının silinmesi yoksa uygulanabileceğini unutmayın .

Bu çözümün verimliliğini bilmiyorum, ama bana yeterince makul geliyor.


3
Teşekkür ederim, tam da aradığım şey buydu. Neden bu kadar zor olduğunu anlayamıyorum.
isapir

4
Evet. Bu sadeleştirme, yalnızca silme olmadığında çalışır.
Craig Ringer

@CraigRinger Silme olursa tam olarak ne olacağını açıklayabilir misiniz?
turbanoff

@turbanoff Kayıt zaten orada olduğu için ekleme başarısız olabilir, ardından eşzamanlı olarak silinir ve satır silindiği için güncelleme sıfır satırı etkiler.
Craig Ringer

@CraigRinger So. Silme aynı anda gerçekleşir . Bu mümkünse outways nelerdir olduğu eserler ince? Silme aynı anda çalışıyorsa - bloğumuzdan hemen sonra yürütülebilir. Söylemeye çalıştığım - eğer eşzamanlı silme varsa - o zaman bu kod uygun şekilde aynı şekilde çalışırinsert on update
türbanoff

28

İşte bazı örnekler insert ... on conflict ...( pg 9.5+ ):

  • Çatışma üzerine yerleştirin - hiçbir şey yapmayın .
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict do nothing;`  
    
  • Çakışmaya ekleme - güncelleme yapıldığında , çakışma hedefini sütunla belirtin .
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict(id)
    do update set name = 'new_name', size = 3;  
    
  • Çakışmaya ekleme - güncelleme yapın , sınırlama adı ile çakışma hedefi belirtin .
    insert into dummy(id, name, size) values(1, 'new_name', 3)
    on conflict on constraint dummy_pkey
    do update set name = 'new_name', size = 4;
    

mükemmel cevap - soru: neden veya hangi durumda sütun veya kısıtlama adı ile hedef belirtimi kullanılmalıdır? Çeşitli kullanım durumları için bir avantaj / dezavantaj var mı?
Nathan Benton

1
@NathanBenton En az 2 fark olduğunu düşünüyorum: (1) sütun adı programcı tarafından belirtilirken, kısıtlama adı programcı tarafından belirtilebilir veya tablo / sütun adlarına göre veritabanı tarafından oluşturulabilir. (2) her sütunda birden fazla kısıtlama olabilir. Bununla birlikte, hangisini kullanacağınızı seçmek sizin durumunuza bağlıdır.
Eric Wang

8

Postgres için SQLAlchemy upert> = 9.5

Yukarıdaki büyük yazı Postgres sürümleri için birçok farklı SQL yaklaşımını kapsadığından (yalnızca sorudaki gibi 9.5 değil), Postgres 9.5 kullanıyorsanız SQLAlchemy'de nasıl yapılacağını eklemek istiyorum. Kendi upert'inizi uygulamak yerine, SQLAlchemy'nin işlevlerini de kullanabilirsiniz (SQLAlchemy 1.1'e eklenmiştir). Şahsen, mümkünse bunları kullanmanızı tavsiye ederim. Sadece kolaylık nedeniyle değil, aynı zamanda PostgreSQL'in meydana gelebilecek yarış koşullarını ele almasına izin verdiği için.

Dün verdiğim başka bir cevaptan çapraz gönderi ( https://stackoverflow.com/a/44395983/2156909 )

SQLAlchemy destekler ON CONFLICTiki yöntem ile artık on_conflict_do_update()ve on_conflict_do_nothing():

Belgelerden kopyalama:

from sqlalchemy.dialects.postgresql import insert

stmt = insert(my_table).values(user_email='a@b.com', data='inserted data')
stmt = stmt.on_conflict_do_update(
    index_elements=[my_table.c.user_email],
    index_where=my_table.c.user_email.like('%@gmail.com'),
    set_=dict(data=stmt.excluded.data)
    )
conn.execute(stmt)

http://docs.sqlalchemy.org/en/latest/dialects/postgresql.html?highlight=conflict#insert-on-conflict-upsert


4
Soruda Python ve SQLAlchemy'den bahsedilmiyor.
Alexander Emelianov

Yazdığım çözümlerde sıklıkla Python kullanıyorum. Ama SQLAlchemy'ye bakmadım (ya da farkındaydım). Bu zarif bir seçenek gibi görünüyor. Teşekkür ederim. Kontrol ederse, bunu kuruluşuma sunacağım.
Robert

3
WITH UPD AS (UPDATE TEST_TABLE SET SOME_DATA = 'Joe' WHERE ID = 2 
RETURNING ID),
INS AS (SELECT '2', 'Joe' WHERE NOT EXISTS (SELECT * FROM UPD))
INSERT INTO TEST_TABLE(ID, SOME_DATA) SELECT * FROM INS

Postgresql 9.3 üzerinde test edildi


@CraigRinger: Bu konuyu biraz açıklayabilir misiniz? cte atomik değil mi?
parisni

2
@parisni Hayır. Her CTE terimi yazma işlemi gerçekleştirirse kendi anlık görüntüsünü alır. Ayrıca satırların üzerinde gerçekleştirilen yüklem kilitleme hiçbir tür yok değil onlar hala başka bir oturum tarafından eş zamanlı oluşturulabilir böylece bulundu. SERIALIZABLEYalıtım kullandıysanız , bir serileştirme hatası ile iptal edersiniz, aksi takdirde benzersiz bir ihlal görürsünüz. Üstünü yeniden icat etmeyin, yeniden icat yanlış olacaktır. Kullanın INSERT ... ON CONFLICT .... PostgreSQL'iniz çok eskiyse güncelleyin.
Craig Ringer

@CraigRinger INSERT ... ON CLONFLICT ...toplu yükleme için tasarlanmamıştır. Yayınınızdan, LOCK TABLE testtable IN EXCLUSIVE MODE;bir CTE içinde atom şeyler almak için bir çözümdür. Hayır ?
parisni

@parisni Toplu yüklemeye yönelik değil mi? Kim söylüyor? postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT . Elbette, upert benzeri davranışlar olmadan toplu yüklemeden çok daha yavaş, ancak bu açıktır ve ne yaparsanız yapın durum böyle olacaktır. Alt işlemleri kullanmaktan çok daha hızlı, bu kesin. En hızlı yaklaşım, hedef tabloyu kilitlemek ve sonra insert ... where not exists ...elbette bir veya benzeri yapmaktır .
Craig Ringer

1

Yana bu soru kapatıldı, ben bunu SQLAlchemy kullanarak bunu nasıl burada post ediyorum. Özyineleme yoluyla, yarış koşullarıyla ve doğrulama hatalarıyla mücadele etmek için toplu bir ekleme veya güncelleme yeniden dener .

İlk olarak ithalat

import itertools as it

from functools import partial
from operator import itemgetter

from sqlalchemy.exc import IntegrityError
from app import session
from models import Posts

Şimdi bir çift yardımcı fonksiyon

def chunk(content, chunksize=None):
    """Groups data into chunks each with (at most) `chunksize` items.
    https://stackoverflow.com/a/22919323/408556
    """
    if chunksize:
        i = iter(content)
        generator = (list(it.islice(i, chunksize)) for _ in it.count())
    else:
        generator = iter([content])

    return it.takewhile(bool, generator)


def gen_resources(records):
    """Yields a dictionary if the record's id already exists, a row object 
    otherwise.
    """
    ids = {item[0] for item in session.query(Posts.id)}

    for record in records:
        is_row = hasattr(record, 'to_dict')

        if is_row and record.id in ids:
            # It's a row but the id already exists, so we need to convert it 
            # to a dict that updates the existing record. Since it is duplicate,
            # also yield True
            yield record.to_dict(), True
        elif is_row:
            # It's a row and the id doesn't exist, so no conversion needed. 
            # Since it's not a duplicate, also yield False
            yield record, False
        elif record['id'] in ids:
            # It's a dict and the id already exists, so no conversion needed. 
            # Since it is duplicate, also yield True
            yield record, True
        else:
            # It's a dict and the id doesn't exist, so we need to convert it. 
            # Since it's not a duplicate, also yield False
            yield Posts(**record), False

Ve son olarak yukarı işlevi

def upsert(data, chunksize=None):
    for records in chunk(data, chunksize):
        resources = gen_resources(records)
        sorted_resources = sorted(resources, key=itemgetter(1))

        for dupe, group in it.groupby(sorted_resources, itemgetter(1)):
            items = [g[0] for g in group]

            if dupe:
                _upsert = partial(session.bulk_update_mappings, Posts)
            else:
                _upsert = session.add_all

            try:
                _upsert(items)
                session.commit()
            except IntegrityError:
                # A record was added or deleted after we checked, so retry
                # 
                # modify accordingly by adding additional exceptions, e.g.,
                # except (IntegrityError, ValidationError, ValueError)
                db.session.rollback()
                upsert(items)
            except Exception as e:
                # Some other error occurred so reduce chunksize to isolate the 
                # offending row(s)
                db.session.rollback()
                num_items = len(items)

                if num_items > 1:
                    upsert(items, num_items // 2)
                else:
                    print('Error adding record {}'.format(items[0]))

İşte nasıl kullanıyorsunuz

>>> data = [
...     {'id': 1, 'text': 'updated post1'}, 
...     {'id': 5, 'text': 'updated post5'}, 
...     {'id': 1000, 'text': 'new post1000'}]
... 
>>> upsert(data)

Bunun avantajı bulk_save_objects, ekleme (ilişkiler, hata kontrolü, vb .) ( Toplu işlemlerin aksine ) üzerinde işlem yapabilmesidir .


Bana da yanlış geliyor. Kimlik listenizi topladıktan sonra eşzamanlı bir oturum satır eklerse ne olur? Yoksa birini siler mi?
Craig Ringer

good point @CraigRinger Buna benzer bir şey yapıyorum ama sadece işi yapan 1 seansım var. O zaman birden fazla oturumu yönetmenin en iyi yolu nedir? Belki bir işlem?
reubano

İşlemler tüm eşzamanlılık sorunlarına sihirli bir çözüm değildir. SERIALIZABLE İşlemleri kullanabilir ve serileştirme hatalarını işleyebilirsiniz, ancak yavaştır. Hata işleme ve yeniden deneme döngüsüne ihtiyacınız var. Cevabımı ve içindeki "ilgili okuma" bölümüne bakın.
Craig Ringer

@CraigRinger yakaladım. Aslında diğer doğrulama hataları nedeniyle kendi durumumda bir yeniden deneme döngüsü uyguladı. Bu cevabı buna göre güncelleyeceğim.
reubano
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.