Postgres: Henüz yoksa INSERT


363

Bir postgres veritabanına yazmak için Python kullanıyorum:

sql_string = "INSERT INTO hundred (name,name_slug,status) VALUES ("
sql_string += hundred + ", '" + hundred_slug + "', " + status + ");"
cursor.execute(sql_string)

Ancak bazı satırlarım aynı olduğundan, aşağıdaki hatayı alıyorum:

psycopg2.IntegrityError: duplicate key value  
  violates unique constraint "hundred_pkey"

'Bu satır zaten yoksa' INSERT 'SQL deyimini nasıl yazabilirim?

Önerilen böyle karmaşık ifadeler gördüm:

IF EXISTS (SELECT * FROM invoices WHERE invoiceid = '12345')
UPDATE invoices SET billed = 'TRUE' WHERE invoiceid = '12345'
ELSE
INSERT INTO invoices (invoiceid, billed) VALUES ('12345', 'TRUE')
END IF

Ama öncelikle, bu ihtiyacım olan şey için aşırıya kaçıyor mu ve ikincisi, bunlardan birini basit bir dize olarak nasıl yürütebilirim?


56
Bu sorunu nasıl çözeceğinize bakılmaksızın, sorgunuzu böyle oluşturmamalısınız. Sorgunuzda parametreleri kullanın ve değerleri ayrı ayrı iletin; bkz. stackoverflow.com/questions/902408/…
Thomas Wouters

3
Neden istisnayı yakalayıp görmezden gelmiyorsun?
Matthew Mitchell

5
Posgres 9.5'ten itibaren (şu anda beta2'de) yeni bir upert benzeri özellik var, bkz: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno

2
Bunun için bir cevap almayı düşündün mü? =]
Relequestual

Yanıtlar:


515

Postgres 9.5 (2016-01-07 tarihinden beri piyasaya sürüldü) INSERT için ON CONFLICT yan tümcesi olarak da bilinen bir "upert" komutu sunuyor :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Eşzamanlı işlemi kullanırken karşılaşabileceğiniz ince sorunların birçoğunu çözer, diğer bazı cevaplar önermektedir.


14
9.5 yayınlandı.
luckydonald

2
@TusharJain PostgreSQL 9.5'ten önce bir "eski moda" UPSERT (CTE ile) yapabilirsiniz, ancak yarış koşullarında sorun yaşayabilirsiniz ve 9.5 tarzı olarak performans göstermeyecektir. Ayrıntılar hakkında daha fazla bilgi edinmek isterseniz, bazı bağlantılar da dahil olmak üzere bu blogda (altta güncellenen alanda) upsert hakkında iyi bir ayrıntı var .
Skyguard

17
İhtiyaç duyanlar için iki basit örnek. (1) Başka bir şey yoksa EKLEME - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) Başka bir güncelleme yoksa EKLE - GÜNCELLEME - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Bu örnekler el kitabından - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan

13
Bir uyarı / yan etki var. Dizi sütununa sahip bir tabloda (seri veya bigserial), hiçbir satır eklenmemiş olsa bile dizi her ekleme girişiminde artırılır.
Grzegorz Luczywo

3
Gerekirse ON CONFLICT DO NOTHING RETURNING idbu yanıtı okuyun stackoverflow.com/a/42217872/368691 .
Gajus

379

'Bu satır zaten yoksa' INSERT 'SQL deyimini nasıl yazabilirim?

PostgreSQL'de koşullu INSERT yapmanın güzel bir yolu var:

INSERT INTO example_table
    (id, name)
SELECT 1, 'John'
WHERE
    NOT EXISTS (
        SELECT id FROM example_table WHERE id = 1
    );

CAVEAT Bu yaklaşım eş zamanlı yazma işlemleri için% 100 güvenilir değildir . Arasında çok küçük bir yarış durumu vardır SELECTiçinde NOT EXISTSanti-yarı katılmak ve INSERTkendisi. Bu koşullar altında başarısız olabilir .


Bu, "isim" alanının UNIQUE sınırlaması olduğunu ne kadar güvenli? Eşsiz ihlal ile başarısız olur mu?
agnsaft

2
Bu iyi çalışıyor. Tek sorun sanırım kuplaj: bir tablo daha fazla sütun benzersiz olacak şekilde değiştirirse. Bu durumda, tüm komut dosyalarının değiştirilmesi gerekir. Bunu yapmak için daha genel bir yol olsaydı iyi olurdu ...
Willem Van Onsem

1
O ile kullanmak mümkün mü RETURNS idalmak için örneğin idtakıldı olup olmadığını?
Olivier Pons

2
@OlivierPons evet, bu mümkün. RETURNING idSorgunun ve yönergelerine ekleyin ; yeni bir satır kimliği veya hiç satır eklenmediyse hiçbir şey döndürmez.
AlexM

4
Bunu güvenilmez buldum. Görünüşe göre Postgres bazen seçimi gerçekleştirmeden önce eki yürütür ve kayıt henüz eklenmemiş olsa bile yinelenen bir anahtar ihlali ile sonuçlanır. ON CONFLICT ile sürüm => 9.5'i kullanmayı deneyin.
Michael Silver

51

Bir yaklaşım, tüm verilerinizi eklemek için kısıtlanmamış (benzersiz dizinler olmadan) bir tablo oluşturmak ve ekinizi yüz tablonuza yapmak için bundan farklı bir seçim yapmak olacaktır.

Yüksek seviye olurdu. Örnekte üç sütunun da farklı olduğunu varsayıyorum, bu nedenle adım3 için NOT EXITS birleşimini yalnızca yüz tablosundaki benzersiz sütunlara katılmak için değiştirin.

  1. Geçici tablo oluştur. Dokümanlara buradan bakın .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. Geçici tabloya veri ekle.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Geçici tabloya herhangi bir dizin ekleyin.

  4. Ana tabla parçasını yapın.

    INSERT INTO hundred(name, name_slug, status) 
        SELECT DISTINCT name, name_slug, status
        FROM hundred
        WHERE NOT EXISTS (
            SELECT 'X' 
            FROM temp_data
            WHERE 
                temp_data.name          = hundred.name
                AND temp_data.name_slug = hundred.name_slug
                AND temp_data.status    = status
        );

3
Bu, satırın zaten var olup olmadığını bilmediğimde toplu ekler yapmanın en hızlı yoludur.
nate c

'X' seçilsin mi? Birisi açıklığa kavuşturabilir mi? Bu sadece bir seçme ifadesidir: SELECT name,name_slug,statusveya*
roberthuttinger

3
İlişkili alt sorguyu arama. 'X' 1 veya hatta 'SadClown' olarak değiştirilebilir. SQL bir şey olmasını gerektirir ve 'X' yaygın bir şeydir. Küçüktür ve ilişkili bir alt sorgunun kullanıldığını açıkça gösterir ve SQL'in gerektirdiği şeylerin gereksinimlerini karşılar.
Kuberchaun

"Tüm verilerinizi (geçici tablo varsa) içine ekleyin ve bundan farklı bir seçim yapın" ifadesini kullandınız. Bu durumda olmamalı SELECT DISTINCT name, name_slug, status FROM temp_datamı?
gibbz00

17

Ne yazık ki, PostgreSQLne destekliyor ne MERGEde ON DUPLICATE KEY UPDATE, bu yüzden iki ifadeyle yapmanız gerekecek:

UPDATE  invoices
SET     billed = 'TRUE'
WHERE   invoices = '12345'

INSERT
INTO    invoices (invoiceid, billed)
SELECT  '12345', 'TRUE'
WHERE   '12345' NOT IN
        (
        SELECT  invoiceid
        FROM    invoices
        )

Bir işleve sarabilirsiniz:

CREATE OR REPLACE FUNCTION fn_upd_invoices(id VARCHAR(32), billed VARCHAR(32))
RETURNS VOID
AS
$$
        UPDATE  invoices
        SET     billed = $2
        WHERE   invoices = $1;

        INSERT
        INTO    invoices (invoiceid, billed)
        SELECT  $1, $2
        WHERE   $1 NOT IN
                (
                SELECT  invoiceid
                FROM    invoices
                );
$$
LANGUAGE 'sql';

ve sadece ara:

SELECT  fn_upd_invoices('12345', 'TRUE')

1
Aslında bu işe yaramıyor: İstediğim kadar arayabiliyorum INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);ve satır eklemeye devam ediyor.
AP257

1
@ AP257: CREATE TABLE hundred (name TEXT, name_slug TEXT, status INT); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); SELECT * FROM hundred. Bir kayıt var.
Quassnoi

12

Değerleri - Postgres'te kullanabilirsiniz:

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;

12
Kişiden isim seçin <--- bizzat milyarlarca satır varsa ne olur?
Henley Chiu

1
Bunun sorunu çözmenin güzel ve hızlı bir yolu olduğunu düşünüyorum, ancak yalnızca kaynak tablonun asla büyük olmayacağından eminseniz. Asla 1000'den fazla satırı olmayacak bir masam var, bu yüzden bu çözümü kullanabilirim.
Leonard

WOW, tam da ihtiyacım olan şey bu. Bir işlev veya geçici tablo oluşturmam gerekeceğinden endişeliydim, ancak bu tüm bunları engelliyor - teşekkür ederim!
Amalgovinus

8

Bu sorunun bir süre önce olduğunu biliyorum, ama bunun birisine yardımcı olabileceğini düşündüm. Bunu yapmanın en kolay yolunun bir tetikleyici olduğunu düşünüyorum. Örneğin:

Create Function ignore_dups() Returns Trigger
As $$
Begin
    If Exists (
        Select
            *
        From
            hundred h
        Where
            -- Assuming all three fields are primary key
            h.name = NEW.name
            And h.hundred_slug = NEW.hundred_slug
            And h.status = NEW.status
    ) Then
        Return NULL;
    End If;
    Return NEW;
End;
$$ Language plpgsql;

Create Trigger ignore_dups
    Before Insert On hundred
    For Each Row
    Execute Procedure ignore_dups();

Bu kodu bir psql isteminden yürütün (veya sorguları doğrudan veritabanında yürütmek istiyorsunuz). Ardından Python'dan normal şekilde ekleyebilirsiniz. Örneğin:

sql = "Insert Into hundreds (name, name_slug, status) Values (%s, %s, %s)"
cursor.execute(sql, (hundred, hundred_slug, status))

@Thomas_Wouters'ın daha önce de belirtildiği gibi, yukarıdaki kodun dizeyi birleştirmek yerine parametrelerden yararlandığını unutmayın.


Başka biri de merak ediyorsa, dokümanlardan : "ÖNCE tetiklenen satır düzeyinde tetikleyiciler, bu satırdaki işlemin geri kalanını atlaması için tetikleyici yöneticisine sinyal göndermek üzere null değerini döndürebilir (yani, sonraki tetikleyiciler tetiklenmez ve INSERT / UPDATE / DELETE bu satır için gerçekleşmez). Boş olmayan bir değer döndürülürse, işlem bu satır değeriyle devam eder. "
Pete

Tam olarak aradığım bu cevap. Select deyimi yerine function + trigger kullanarak kodu temizleyin. +1
Jacek Krawczyk

Bu cevabı seviyorum, fonksiyonu kullan ve tetikle. Şimdi işlevleri ve tetikleyicileri kullanarak çıkmazı kırmak için başka bir yol bulmak ...
Sukma Saputra

7

WITH sorgu kullanarak kullanarak PostgreSQL koşullu INSERT yapmanın güzel bir yolu var:

WITH a as(
select 
 id 
from 
 schema.table_name 
where 
 column_name = your_identical_column_value
)
INSERT into 
 schema.table_name
(col_name1, col_name2)
SELECT
    (col_name1, col_name2)
WHERE NOT EXISTS (
     SELECT
         id
     FROM
         a
        )
  RETURNING id 

7

Bu tam olarak karşılaştığım problem ve versiyonum 9.5

Ve aşağıdaki SQL sorgusu ile çözdüm.

INSERT INTO example_table (id, name)
SELECT 1 AS id, 'John' AS name FROM example_table
WHERE NOT EXISTS(
            SELECT id FROM example_table WHERE id = 1
    )
LIMIT 1;

Bu sürüm> = 9.5 ile aynı sorunu olan birine yardımcı olacağını umuyoruz.

Okuduğunuz için teşekkürler.


5

EKLE .. VAR OLMAYAN YERLER iyi bir yaklaşımdır. Ve yarış koşulları "zarf" işlemiyle önlenebilir:

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;

2

Kurallarla kolaydır:

CREATE RULE file_insert_defer AS ON INSERT TO file
WHERE (EXISTS ( SELECT * FROM file WHERE file.id = new.id)) DO INSTEAD NOTHING

Ama eşzamanlı yazmalarla başarısız oluyor ...


1

En upvotes ile yaklaşım (John Doe'dan) bir şekilde benim için çalışıyor ama benim durumumda beklenen 422 satırdan sadece 180 alıyorum. Yanlış bir şey bulamadım ve hiç hata yok, bu yüzden farklı bir şey aradım basit yaklaşım.

IF NOT FOUND THENSonra kullanmak SELECTbenim için mükemmel çalışıyor.

( PostgreSQL Belgelerinde açıklanmıştır )

Belgelerden örnek:

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
  RAISE EXCEPTION 'employee % not found', myname;
END IF;

1

psycopgs imleç sınıfı rowcount özelliğine sahiptir .

Bu salt okunur öznitelik, son yürütme * () tarafından üretilen (SELECT gibi DQL ifadeleri için) veya etkilenen (UPDATE veya INSERT gibi DML ifadeleri için) satır sayısını belirtir.

Böylece önce UPDATE ve INSERT komutlarını ancak rowcount 0 ise deneyebilirsiniz.

Ancak, veritabanınızdaki etkinlik düzeylerine bağlı olarak, UPDATE ve INSERT arasında bir yarış koşuluna varabilirsiniz; burada başka bir işlem geçici olarak bu kaydı oluşturabilir.


Muhtemelen bu sorguları bir işleme sarmak yarış durumunu hafifletir.
Daniel Lyons

Teşekkürler, gerçekten basit ve temiz bir çözüm
Alexander Malfait

1

"Yüz" sütununuz birincil anahtar olarak tanımlanmış görünüyor ve bu nedenle durum benzersiz değil benzersiz olmalıdır. Sorun, verilerinizle değil.

Birincil anahtarı kullanmak için seri türü olarak bir kimlik eklemenizi öneririm


1

Satırlarınızın çoğunun aynı olduğunu söylerseniz, birçok kez kontrol etmeye son vereceksiniz. Onları gönderebilirsiniz ve veritabanı ON CONFLICT yan tümcesi ile ekleyip eklemeyeceğini aşağıdaki gibi belirler

  INSERT INTO Hundred (name,name_slug,status) VALUES ("sql_string += hundred  
  +",'" + hundred_slug + "', " + status + ") ON CONFLICT ON CONSTRAINT
  hundred_pkey DO NOTHING;" cursor.execute(sql_string);

0

PostgreSQL ve HSQLDB'de çalışan SQL bulmaya çalışırken benzer bir çözüm arıyordum. (HSQLDB bunu zor yapan şeydi.) Örneğinizi temel alarak, başka yerde bulduğum biçim budur.

sql = "INSERT INTO hundred (name,name_slug,status)"
sql += " ( SELECT " + hundred + ", '" + hundred_slug + "', " + status
sql += " FROM hundred"
sql += " WHERE name = " + hundred + " AND name_slug = '" + hundred_slug + "' AND status = " + status
sql += " HAVING COUNT(*) = 0 );"

-1

Burada bir tablename, sütunlar ve değerler verilen, postgresql için upert eşdeğerini üreten genel bir python işlevi var.

ithalat json

def upsert(table_name, id_column, other_columns, values_hash):

    template = """
    WITH new_values ($$ALL_COLUMNS$$) as (
      values
         ($$VALUES_LIST$$)
    ),
    upsert as
    (
        update $$TABLE_NAME$$ m
            set
                $$SET_MAPPINGS$$
        FROM new_values nv
        WHERE m.$$ID_COLUMN$$ = nv.$$ID_COLUMN$$
        RETURNING m.*
    )
    INSERT INTO $$TABLE_NAME$$ ($$ALL_COLUMNS$$)
    SELECT $$ALL_COLUMNS$$
    FROM new_values
    WHERE NOT EXISTS (SELECT 1
                      FROM upsert up
                      WHERE up.$$ID_COLUMN$$ = new_values.$$ID_COLUMN$$)
    """

    all_columns = [id_column] + other_columns
    all_columns_csv = ",".join(all_columns)
    all_values_csv = ','.join([query_value(values_hash[column_name]) for column_name in all_columns])
    set_mappings = ",".join([ c+ " = nv." +c for c in other_columns])

    q = template
    q = q.replace("$$TABLE_NAME$$", table_name)
    q = q.replace("$$ID_COLUMN$$", id_column)
    q = q.replace("$$ALL_COLUMNS$$", all_columns_csv)
    q = q.replace("$$VALUES_LIST$$", all_values_csv)
    q = q.replace("$$SET_MAPPINGS$$", set_mappings)

    return q


def query_value(value):
    if value is None:
        return "NULL"
    if type(value) in [str, unicode]:
        return "'%s'" % value.replace("'", "''")
    if type(value) == dict:
        return "'%s'" % json.dumps(value).replace("'", "''")
    if type(value) == bool:
        return "%s" % value
    if type(value) == int:
        return "%s" % value
    return value


if __name__ == "__main__":

    my_table_name = 'mytable'
    my_id_column = 'id'
    my_other_columns = ['field1', 'field2']
    my_values_hash = {
        'id': 123,
        'field1': "john",
        'field2': "doe"
    }
    print upsert(my_table_name, my_id_column, my_other_columns, my_values_hash)

-8

Çözüm basit, ama hemen değil.
Bu talimatı kullanmak istiyorsanız, db'de bir değişiklik yapmanız gerekir:

ALTER USER user SET search_path to 'name_of_schema';

bu değişikliklerden sonra "INSERT" doğru şekilde çalışacaktır.

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.