Postgres: INSERT nếu chưa tồn tại


361

Tôi đang sử dụng Python để ghi vào cơ sở dữ liệu postgres:

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

Nhưng vì một số hàng của tôi giống hệt nhau, tôi gặp lỗi sau:

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

Làm cách nào tôi có thể viết câu lệnh 'INSERT trừ khi hàng này đã tồn tại' câu lệnh SQL?

Tôi đã thấy những tuyên bố phức tạp như thế này được đề xuất:

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

Nhưng thứ nhất, đây có phải là quá mức cho những gì tôi cần không, và thứ hai, làm thế nào tôi có thể thực thi một trong những chuỗi đó như một chuỗi đơn giản?


56
Bất kể bạn giải quyết vấn đề này như thế nào, bạn không nên tạo truy vấn của mình như thế. Sử dụng các tham số trong truy vấn của bạn và chuyển các giá trị riêng biệt; xem stackoverflow.com/questions/902408/ Mạnh
Thomas Wouters

3
Tại sao không bắt ngoại lệ và bỏ qua nó?
Matthew Mitchell

5
Kể từ Posgres 9.5 (hiện đang có phiên bản beta2), có một tính năng mới như upert, xem: postgresql.org/docs/9.5/static/sql-insert.html#Query-ON-CONFLICT
Ezequiel Moreno

2
Bạn đã xem xét chấp nhận một câu trả lời cho điều này? =]
Relequestual

Câu trả lời:


513

Postgres 9.5 (được phát hành từ 2016-01-07) cung cấp lệnh "upsert" , còn được gọi là mệnh đề ON CONFLICT cho INSERT :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Nó giải quyết nhiều vấn đề tinh tế mà bạn có thể gặp phải khi sử dụng thao tác đồng thời, một số câu trả lời khác đề xuất.


14
9,5 đã được phát hành.
luckydonald

2
@TusharJain trước PostgreQuery 9.5, bạn có thể thực hiện một UPSERT "lỗi thời" (với CTE) nhưng bạn có thể gặp sự cố với điều kiện cuộc đua và nó sẽ không hoạt động theo kiểu 9.5. Có một chi tiết tốt về upsert trên blog này (trong khu vực được cập nhật ở phía dưới) bao gồm một số liên kết nếu bạn muốn đọc thêm về các chi tiết.
Skyguard

17
Đối với những người cần thiết, đây là hai ví dụ đơn giản. (1) INSERT nếu không tồn tại NOTHING khác - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) INSERT nếu không tồn tại CẬP NHẬT khác - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Những ví dụ này từ thủ công - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan

13
Có một cảnh báo / tác dụng phụ. Trong một bảng có cột trình tự (nối tiếp hoặc bigserial), ngay cả khi không có hàng nào được chèn, trình tự được tăng lên ở mỗi lần thử chèn.
Grzegorz Luczywo

2
Nó sẽ được liên kết tốt hơn với tài liệu INSERT thay vì chỉ để phát hành. Liên kết tài liệu: postgresql.org/docs/9.5/static/sql-insert.html
borjagvo

379

Làm cách nào tôi có thể viết câu lệnh 'INSERT trừ khi hàng này đã tồn tại' câu lệnh SQL?

Có một cách hay để thực hiện INSERT có điều kiện trong PostgreSQL:

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

Nên biết trước Cách tiếp cận này không phải là 100% đáng tin cậy cho đồng thời các hoạt động ghi, mặc dù. Có một điều kiện cuộc đua rất nhỏ giữa SELECTviệc NOT EXISTSchống bán tham gia và INSERTchính nó. Nó có thể thất bại trong điều kiện như vậy.


Làm thế nào an toàn là giả định rằng trường "tên" có một ràng buộc ĐỘC ĐÁO? Nó sẽ không bao giờ thất bại với vi phạm duy nhất?
agnsaft

2
Điều này hoạt động tốt. Vấn đề duy nhất là khớp nối tôi đoán: nếu một người sửa đổi bảng sao cho nhiều cột là duy nhất. Trong trường hợp đó, tất cả các tập lệnh phải được sửa đổi. Sẽ thật tuyệt nếu có một cách chung chung hơn để làm điều này ...
Willem Van Onsem

1
Có thể sử dụng nó với RETURNS idví dụ để có được idliệu đã được chèn hay không?
Olivier Pons

2
@OlivierPons có, nó có thể. Thêm RETURNING idtại và của truy vấn và nó sẽ trả về id hàng mới hoặc không có gì, nếu không có hàng nào được chèn.
AlexM

4
Tôi đã tìm thấy điều này là không đáng tin cậy. Có vẻ như Postgres đôi khi thực thi thao tác chèn trước khi nó thực thi lệnh chọn và tôi kết thúc bằng một vi phạm khóa trùng lặp mặc dù bản ghi chưa được chèn. Hãy thử sử dụng phiên bản => 9.5 với ON CONFLICT.
Michael Silver

51

Một cách tiếp cận là tạo một bảng không bị ràng buộc (không có chỉ mục duy nhất) để chèn tất cả dữ liệu của bạn vào và thực hiện một lựa chọn khác biệt để thực hiện việc chèn vào bảng trăm của bạn.

Vì vậy, mức độ cao sẽ được. Tôi giả sử cả ba cột đều khác biệt trong ví dụ của tôi vì vậy đối với bước 3, thay đổi tham gia KHÔNG EXITS để chỉ tham gia vào các cột duy nhất trong bảng trăm.

  1. Tạo bảng tạm thời. Xem tài liệu ở đây .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. Dữ liệu INSERT vào bảng tạm thời.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Thêm bất kỳ chỉ mục vào bảng tạm thời.

  4. Làm bảng chính chè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
Đây là cách nhanh nhất tôi đã tìm thấy để thực hiện chèn hàng loạt khi tôi không biết hàng đã tồn tại chưa.
nate c

chọn 'X'? ai đó có thể làm rõ? Đây chỉ đơn giản là một tuyên bố chọn đúng: SELECT name,name_slug,statushoặc*
roberthuttinger

3
Tra cứu truy vấn con tương quan. 'X' có thể được thay đổi thành 1 hoặc thậm chí 'SadClown'. SQL yêu cầu phải có một cái gì đó và 'X' là một thứ phổ biến để sử dụng. Nó nhỏ và rõ ràng là một truy vấn con tương quan đang được sử dụng và đáp ứng các yêu cầu của những gì SQL yêu cầu.
Kuberchaun

Bạn đã đề cập "chèn tất cả dữ liệu của bạn vào (giả sử bảng tạm thời) và chọn một phân biệt từ đó". Trong trường hợp đó, nó có nên không SELECT DISTINCT name, name_slug, status FROM temp_data?
gibbz00

17

Thật không may, PostgreSQLhỗ trợ cũng không MERGEphải ON DUPLICATE KEY UPDATE, vì vậy bạn sẽ phải làm điều đó trong hai tuyên bố:

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

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

Bạn có thể gói nó thành một chức năng:

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

và chỉ cần gọi nó:

SELECT  fn_upd_invoices('12345', 'TRUE')

1
Trên thực tế, điều này không hoạt động: Tôi có thể gọi INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);bất kỳ số lần nào và nó tiếp tục chèn hàng.
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. Có một kỷ lục.
Quassnoi

12

Bạn có thể sử dụng GIÁ TRỊ - có sẵn trong Postgres:

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

12
Tên CHỌN TỪ người <--- nếu có một tỷ hàng trong người thì sao?
Henley Chiu

1
Tôi nghĩ rằng đây là một cách nhanh chóng tốt để giải quyết vấn đề, nhưng chỉ khi bạn chắc chắn bảng nguồn sẽ không bao giờ phát triển lớn. Tôi đã có một bảng sẽ không bao giờ có hơn 1000 hàng, vì vậy tôi có thể sử dụng giải pháp này.
Leonard

WOW, đây chính xác là những gì tôi cần. Tôi đã lo lắng tôi cần phải tạo một hàm hoặc một bảng tạm thời, nhưng điều này loại trừ tất cả những điều đó - cảm ơn bạn!
Amalgovinus

8

Tôi biết câu hỏi này là từ một thời gian trước, nhưng nghĩ rằng điều này có thể giúp đỡ một ai đó. Tôi nghĩ rằng cách dễ nhất để làm điều này là thông qua một kích hoạt. Ví dụ:

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

Thực thi mã này từ một dấu nhắc psql (hoặc tuy nhiên bạn muốn thực hiện các truy vấn trực tiếp trên cơ sở dữ liệu). Sau đó, bạn có thể chèn như bình thường từ Python. Ví dụ:

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

Lưu ý rằng như @Thomas_Wouters đã đề cập, đoạn mã trên tận dụng lợi thế của các tham số thay vì nối chuỗi.


Nếu bất kỳ ai khác cũng thắc mắc, từ các tài liệu : "Các trình kích hoạt cấp hàng được bắn TRƯỚC có thể trả về null để báo hiệu cho trình quản lý kích hoạt bỏ qua phần còn lại của hoạt động cho hàng này (nghĩa là các trình kích hoạt tiếp theo không được kích hoạt và CHERTN / CẬP NHẬT / XÓA không xảy ra đối với hàng này). Nếu một giá trị không hoàn trả được trả về thì thao tác tiến hành với giá trị hàng đó. "
Pete

Chính xác câu trả lời này tôi đang tìm kiếm. Làm sạch mã, sử dụng hàm + kích hoạt thay vì chọn câu lệnh. +1
Jacek Krawchot

Tôi thích câu trả lời này, sử dụng chức năng và kích hoạt. Bây giờ tôi tìm một cách khác để phá vỡ bế tắc bằng cách sử dụng các chức năng và trình kích hoạt ...
Sukma Saputra

7

Có một cách hay để thực hiện INSERT có điều kiện trong PostgreSQL bằng truy vấn VỚI: Thích:

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

Đây chính xác là vấn đề tôi gặp phải và phiên bản của tôi là 9.5

Và tôi giải quyết nó bằng truy vấn SQL bên dưới.

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;

Hy vọng rằng sẽ giúp được ai đó có cùng vấn đề với phiên bản> = 9.5.

Cảm ơn vì đã đọc.


5

CHERTN .. Ở ĐÂU KHÔNG HIỆN TẠI là cách tiếp cận tốt. Và điều kiện cuộc đua có thể tránh được bằng "phong bì" giao dịch:

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

2

Thật dễ dàng với các quy tắc:

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

Nhưng nó thất bại với việc viết đồng thời ...


1

Cách tiếp cận với hầu hết các upvote (từ John Doe) bằng cách nào đó có hiệu quả với tôi nhưng trong trường hợp của tôi từ 422 hàng dự kiến ​​tôi chỉ nhận được 180. Tôi không thể tìm thấy bất cứ điều gì sai và không có lỗi nào cả, vì vậy tôi đã tìm một cách khác cách tiếp cận đơn giản.

Sử dụng IF NOT FOUND THENsau khi SELECTchỉ hoạt động hoàn hảo cho tôi.

(được mô tả trong Tài liệu PostgreSQL )

Ví dụ từ tài liệu:

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

1

psycopgs lớp con trỏ có thuộc tính rowcount .

Thuộc tính chỉ đọc này chỉ định số lượng hàng mà lần thực thi cuối cùng * () được tạo ra (đối với các câu lệnh DQL như SELECT) hoặc bị ảnh hưởng (đối với các câu lệnh DML như UPDATE hoặc INSERT).

Vì vậy, bạn có thể thử CẬP NHẬT trước và CHỈ khi hàng đếm bằng 0.

Nhưng tùy thuộc vào cấp độ hoạt động trong cơ sở dữ liệu của bạn, bạn có thể gặp tình trạng chạy đua giữa CẬP NHẬT và CHERTN trong đó một quy trình khác có thể tạo bản ghi đó trong thời gian tạm thời.


Có lẽ gói các truy vấn này trong một giao dịch sẽ làm giảm bớt điều kiện cuộc đua.
Daniel Lyons

Cảm ơn, giải pháp thực sự đơn giản và sạch sẽ
Alexander Malfait

1

Cột "trăm" của bạn dường như được xác định là khóa chính và do đó phải là duy nhất không phải là trường hợp. Vấn đề không phải là, đó là với dữ liệu của bạn.

Tôi đề nghị bạn chèn một id dưới dạng loại nối tiếp để sử dụng khóa chính


1

Nếu bạn nói rằng nhiều hàng của bạn giống hệt nhau, bạn sẽ kết thúc kiểm tra nhiều lần. Bạn có thể gửi chúng và cơ sở dữ liệu sẽ xác định xem có chèn nó hay không với mệnh đề ON CONFLICT như sau

  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

Tôi đang tìm kiếm một giải pháp tương tự, cố gắng tìm SQL hoạt động trong PostgreSQL cũng như HSQLDB. (HSQLDB là điều gây khó khăn cho việc này.) Sử dụng ví dụ của bạn làm cơ sở, đây là định dạng mà tôi tìm thấy ở nơi khác.

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

Dưới đây là một hàm python chung cung cấp một tablename, cột và giá trị, tạo ra mức tương đương cho postgresql.

nhập khẩu 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

Các giải pháp trong đơn giản, nhưng không phải ngay lập tức.
Nếu bạn muốn sử dụng hướng dẫn này, bạn phải thực hiện một thay đổi đối với db:

ALTER USER user SET search_path to 'name_of_schema';

sau những thay đổi này, "INSERT" sẽ hoạt động chính xác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.