Postgres: INSERT jika belum ada

361

Saya menggunakan Python untuk menulis ke database postgres:

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

Tetapi karena beberapa baris saya identik, saya mendapatkan kesalahan berikut:

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

Bagaimana saya bisa menulis pernyataan SQL 'INSERT kecuali baris ini sudah ada'?

Saya telah melihat pernyataan kompleks seperti ini direkomendasikan:

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

Tetapi pertama-tama, apakah ini berlebihan untuk apa yang saya butuhkan, dan kedua, bagaimana saya bisa mengeksekusi salah satu dari mereka sebagai string sederhana?

AP257
sumber
56
Terlepas dari bagaimana Anda menyelesaikan masalah ini, Anda seharusnya tidak menghasilkan permintaan Anda seperti itu. Gunakan parameter dalam kueri Anda dan berikan nilai secara terpisah; lihat stackoverflow.com/questions/902408/…
Thomas Wouters
3
Mengapa tidak menangkap pengecualian dan mengabaikannya?
Matius Mitchell
5
Pada Posgres 9.5 (saat ini dalam versi beta2) ada fitur baru seperti upert, lihat: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno
2
Sudahkah Anda mempertimbangkan menerima jawaban untuk ini? =]
Relequestual

Jawaban:

513

Postgres 9.5 (dirilis sejak 2016-01-07) menawarkan perintah "upsert" , juga dikenal sebagai klausa ON CONFLICT ke INSERT :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Ini memecahkan banyak masalah halus yang bisa Anda hadapi ketika menggunakan operasi bersamaan, yang diajukan beberapa jawaban lainnya.

Arie
sumber
14
9.5 dibebaskan.
luckydonald
2
@TusharJain sebelum PostgreSQL 9.5 Anda dapat melakukan UPSERT "kuno" (dengan CTE) tetapi Anda mungkin mengalami masalah dengan kondisi balapan dan itu tidak akan tampil sebagai gaya 9,5. Ada detail bagus tentang upsert di blog ini (di area yang diperbarui di bagian bawah) termasuk beberapa tautan jika Anda ingin membaca lebih lanjut tentang detailnya.
Skyguard
17
Untuk yang dibutuhkan, inilah dua contoh sederhana. (1) MASUKKAN jika tidak ada yang lain TIDAK ADA - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) MASUKKAN jika tidak ada yang lain UPDATE - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Contoh-contoh ini dari manual - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan
13
Ada satu peringatan / efek samping. Dalam tabel dengan kolom urutan (serial atau bigserial), bahkan jika tidak ada baris yang dimasukkan urutannya akan bertambah pada setiap upaya memasukkan.
Grzegorz Luczywo
2
Akan lebih baik menautkan ke dokumentasi INSERT daripada menunjuk untuk melepaskan. Tautan
dokumen
379

Bagaimana saya bisa menulis pernyataan SQL 'INSERT kecuali baris ini sudah ada'?

Ada cara yang baik untuk melakukan INSERT bersyarat di PostgreSQL:

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

CAVEAT Pendekatan ini tidak 100% andal untuk operasi penulisan bersamaan . Ada kondisi ras yang sangat kecil antara SELECTdi dalam NOT EXISTSanti-semi-join dan INSERTitu sendiri. Ini bisa gagal dalam kondisi seperti itu.

John Doe
sumber
Seberapa amankah anggapan ini bahwa bidang "nama" memiliki batasan UNIK? Apakah ini akan gagal dengan pelanggaran unik?
agnsaft
2
Ini berfungsi dengan baik. Satu-satunya masalah adalah kopling saya kira: bagaimana jika seseorang memodifikasi tabel sehingga lebih banyak kolom yang unik. Dalam hal ini semua skrip harus dimodifikasi. Akan lebih baik jika ada cara yang lebih umum untuk melakukan ini ...
Willem Van Onsem
1
Apakah mungkin untuk menggunakannya dengan RETURNS idmisalnya untuk mendapatkan idapakah sudah dimasukkan atau belum?
Olivier Pons
2
@ OlivierPons ya, itu mungkin. Tambahkan RETURNING idpada dan dari kueri dan itu akan mengembalikan id baris baru atau tidak sama sekali, jika tidak ada baris yang dimasukkan.
AlexM
4
Saya menemukan ini tidak dapat diandalkan. Tampaknya Postgres kadang-kadang mengeksekusi sisipan sebelum mengeksekusi pemilihan dan saya berakhir dengan pelanggaran kunci duplikat meskipun catatan belum dimasukkan. Coba gunakan versi => 9.5 dengan ON CONFLICT.
Michael Silver
51

Salah satu pendekatan akan membuat tabel non-kendala (tidak ada indeks unik) untuk memasukkan semua data Anda ke dalam dan melakukan pilih berbeda dari itu untuk melakukan memasukkan Anda ke dalam seratus tabel Anda.

Jadi level tinggi akan. Saya berasumsi ketiga kolom berbeda dalam contoh saya jadi untuk langkah 3 ubah BUKAN EXIT bergabung dengan hanya bergabung pada kolom unik dalam tabel seratus.

  1. Buat tabel sementara. Lihat dokumen di sini .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. Masukkan data ke tabel temp.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Tambahkan indeks ke tabel temp.

  4. Apakah memasukkan tabel utama.

    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
        );
Kuberchaun
sumber
3
Ini adalah cara tercepat yang saya temukan untuk melakukan sisipan massa ketika saya tidak tahu apakah barisnya sudah ada.
nate c
pilih 'X'? dapatkah seseorang mengklarifikasi? Ini hanyalah pernyataan pilih kanan: SELECT name,name_slug,statusatau*
roberthuttinger
3
Cari subquery berkorelasi. 'X' dapat diubah menjadi 1 atau bahkan 'SadClown'. SQL mengharuskan ada sesuatu dan 'X' adalah hal yang umum untuk digunakan. Ini kecil dan membuatnya jelas bahwa subquery berkorelasi sedang digunakan dan memenuhi persyaratan apa yang dibutuhkan SQL.
Kuberchaun
Anda menyebutkan "masukkan semua data Anda ke dalam (dengan asumsi tabel temp) dan pilih yang berbeda dari itu". Dalam hal itu, bukankah seharusnya demikian SELECT DISTINCT name, name_slug, status FROM temp_data?
gibbz00
17

Sayangnya, PostgreSQLtidak mendukung MERGEjuga ON DUPLICATE KEY UPDATE, jadi Anda harus melakukannya dalam dua pernyataan:

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

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

Anda dapat membungkusnya menjadi fungsi:

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';

dan sebut saja:

SELECT  fn_upd_invoices('12345', 'TRUE')
Quassnoi
sumber
1
Sebenarnya, ini tidak berhasil: Saya bisa menelepon INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);beberapa kali, dan itu tetap memasukkan baris.
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. Ada satu catatan.
Quassnoi
12

Anda dapat menggunakan VALUES - tersedia di Postgres:

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;
kristisme
sumber
12
SELECT name FROM Person <--- bagaimana jika ada satu miliar baris secara pribadi?
Henley Chiu
1
Saya pikir ini adalah cara cepat yang bagus untuk menyelesaikan masalah, tetapi hanya ketika Anda yakin tabel sumber tidak akan pernah tumbuh besar. Saya punya tabel yang tidak akan pernah memiliki lebih dari 1000 baris, jadi saya bisa menggunakan solusi ini.
Leonard
WOW, inilah tepatnya yang saya butuhkan. Saya khawatir saya harus membuat fungsi atau tabel temp, tetapi ini menghalangi semua itu - terima kasih!
Amalgovinus
8

Saya tahu pertanyaan ini dari beberapa waktu yang lalu, tetapi berpikir ini mungkin membantu seseorang. Saya pikir cara termudah untuk melakukan ini adalah melalui pemicu. Misalnya:

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();

Jalankan kode ini dari prompt psql (atau bagaimanapun Anda ingin mengeksekusi query langsung pada database). Kemudian Anda dapat memasukkan seperti biasa dari Python. Misalnya:

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

Perhatikan bahwa seperti yang sudah disebutkan oleh @Thomas_Wouters, kode di atas memanfaatkan parameter daripada menyatukan string.

ktr
sumber
Jika ada orang lain yang bertanya-tanya juga, dari dokumen : "Pemicu tingkat-baris yang dipecat SEBELUM dapat mengembalikan nol untuk memberi sinyal pada manajer pemicu untuk melewati sisa operasi untuk baris ini (yaitu, pemicu berikutnya tidak dipecat, dan INSERT / PEMBARUAN) / HAPUS tidak terjadi untuk baris ini). Jika nilai nonnull dikembalikan maka operasi dilanjutkan dengan nilai baris itu. "
Pete
Persis jawaban ini yang saya cari. Bersihkan kode, menggunakan fungsi + pemicu alih-alih pernyataan pilihan. +1
Jacek Krawczyk
Saya suka jawaban ini, gunakan fungsi dan pemicu. Sekarang saya menemukan cara lain untuk memecahkan kebuntuan menggunakan fungsi dan pemicu ...
Sukma Saputra
7

Ada cara yang baik untuk melakukan INSERT bersyarat di PostgreSQL menggunakan DENGAN query: Seperti:

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 
Ritesh Jha
sumber
7

Ini persis masalah yang saya hadapi dan versi saya 9.5

Dan saya menyelesaikannya dengan query SQL di bawah ini.

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;

Semoga itu akan membantu seseorang yang memiliki masalah yang sama dengan versi> = 9.5.

Terima kasih sudah membaca.

tuanngocptn
sumber
5

MASUK .. TIDAK ADA pendekatan yang bagus. Dan kondisi lomba dapat dihindari dengan "amplop" transaksi:

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;
Pavel Francírek
sumber
2

Mudah dengan aturan:

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

Tetapi gagal dengan bersamaan menulis ...


sumber
1

Pendekatan dengan sebagian besar upvotes (dari John Doe) entah bagaimana bekerja untuk saya tetapi dalam kasus saya dari yang diharapkan 422 baris saya hanya mendapatkan 180. Saya tidak dapat menemukan sesuatu yang salah dan tidak ada kesalahan sama sekali, jadi saya mencari yang berbeda pendekatan sederhana.

Menggunakan IF NOT FOUND THENsetelah SELECThanya berfungsi dengan baik untuk saya.

(dijelaskan dalam Dokumentasi PostgreSQL )

Contoh dari dokumentasi:

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

kelas kursor psycopgs memiliki atribut rowcount .

Atribut read-only ini menentukan jumlah baris yang dieksekusi * () terakhir diproduksi (untuk pernyataan DQL seperti SELECT) atau terpengaruh (untuk pernyataan DML seperti UPDATE atau INSERT).

Jadi, Anda dapat mencoba PEMBARUAN terlebih dahulu dan MASUKKAN hanya jika rowcount adalah 0.

Tetapi tergantung pada tingkat aktivitas dalam basis data Anda, Anda dapat mencapai kondisi balapan antara UPDATE dan INSERT tempat proses lain dapat membuat catatan itu untuk sementara.

Johnbaum
sumber
Agaknya membungkus pertanyaan ini dalam suatu transaksi akan meringankan kondisi balapan.
Daniel Lyons
Terima kasih, solusi yang sangat sederhana dan bersih
Alexander Malfait
1

Kolom Anda "ratus" tampaknya didefinisikan sebagai kunci utama dan oleh karena itu harus unik yang tidak demikian. Masalahnya bukan dengan, itu dengan data Anda.

Saya sarankan Anda memasukkan id sebagai tipe serial untuk menangani kunci utama

Boodoo
sumber
1

Jika Anda mengatakan bahwa banyak baris Anda identik, Anda akan berakhir memeriksa berulang kali. Anda dapat mengirimnya dan database akan menentukan apakah memasukkannya atau tidak dengan klausa ON CONFLICT sebagai berikut

  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);
Buka sebuah
sumber
0

Saya sedang mencari solusi yang serupa, berusaha menemukan SQL yang berfungsi di PostgreSQL dan juga HSQLDB. (HSQLDB adalah apa yang membuat ini sulit.) Menggunakan contoh Anda sebagai dasar, ini adalah format yang saya temukan di tempat lain.

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 );"
Jeff Fairley
sumber
-1

Berikut adalah fungsi python generik yang memberikan nama tab, kolom, dan nilai, menghasilkan ekuivalen upsert untuk postgresql.

impor 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)
Patrick
sumber
-8

Solusinya sederhana, tetapi tidak dengan segera.
Jika Anda ingin menggunakan instruksi ini, Anda harus melakukan satu perubahan pada db:

ALTER USER user SET search_path to 'name_of_schema';

setelah perubahan ini "INSERT" akan berfungsi dengan benar.

el fuser
sumber