Postgres: INSERT หากไม่มีอยู่แล้ว


361

ฉันใช้ Python เพื่อเขียนไปยังฐานข้อมูล postgres:

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

แต่เนื่องจากแถวบางแถวของฉันเหมือนกันฉันได้รับข้อผิดพลาดต่อไปนี้:

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

ฉันจะเขียนคำว่า 'INSERT ได้อย่างไรถ้าไม่มีคำสั่ง SQL อยู่แล้ว?

ฉันเคยเห็นข้อความที่ซับซ้อนเช่นนี้แนะนำ:

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

แต่อย่างแรกคือ overkill นี้สำหรับสิ่งที่ฉันต้องการและอย่างที่สองฉันจะรันหนึ่งในนั้นเป็นสตริงแบบง่ายได้อย่างไร


56
ไม่ว่าคุณจะแก้ปัญหานี้อย่างไรคุณไม่ควรสร้างข้อความค้นหาเช่นนั้น ใช้พารามิเตอร์ในคิวรีของคุณและส่งค่าแยกกัน ดูstackoverflow.com/questions/902408/…
Thomas Wouters

3
ทำไมไม่ลองดูข้อยกเว้นแล้วเพิกเฉยล่ะ?
Matthew Mitchell

5
ในฐานะของ Posgres 9.5 (ปัจจุบันอยู่บน beta2) มีฟีเจอร์ใหม่ที่เพิ่มขึ้นเช่นดู: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno

2
คุณคิดว่าจะยอมรับคำตอบสำหรับสิ่งนี้หรือไม่? =]
ทะเบียน

คำตอบ:


512

Postgres 9.5 (เปิดตัวตั้งแต่ 2016-01-07) มีคำสั่ง"upsert"หรือที่เรียกว่าON ON CONERTICT clause :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

มันแก้ปัญหาที่ละเอียดอ่อนหลายอย่างที่คุณสามารถพบเจอได้เมื่อใช้การทำงานพร้อมกันซึ่งบางคำตอบอื่น ๆ เสนอ


14
9.5 ได้เปิดตัวแล้ว
luckydonald

2
@TusharJain ก่อน PostgreSQL 9.5 คุณสามารถทำ UPSERT "แบบเก่า" (กับ CTE) แต่คุณอาจพบปัญหาเกี่ยวกับสภาพการแข่งขันและมันจะไม่เป็นสไตล์ 9.5 มีรายละเอียดที่ดีเกี่ยวกับ upsert ในบล็อกนี้(ในพื้นที่อัปเดตที่ด้านล่าง) รวมถึงการเชื่อมโยงบางส่วนถ้าคุณต้องการอ่านเพิ่มเติมเกี่ยวกับรายละเอียด
Skyguard

16
สำหรับผู้ที่ต้องการนี่คือตัวอย่างง่ายๆสองตัวอย่าง (1) INSERT หากไม่ได้อยู่ที่อื่นไม่มีอะไร - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) INSERT หากไม่ได้อยู่ที่อื่น UPDATE - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;ตัวอย่างเหล่านี้มาจากคู่มือ - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan

13
มีหนึ่งคำเตือน / ผลข้างเคียง ในตารางที่มีคอลัมน์ลำดับ (อนุกรมหรือใหญ่) แม้ว่าจะไม่มีการแทรกแถวลำดับจะเพิ่มขึ้นทุกครั้งที่พยายามแทรก
Grzegorz Luczywo

2
มันจะเป็นการเชื่อมโยงกับเอกสาร INSERT ที่ดีกว่าแทนที่จะชี้ไปที่ปล่อย ลิงก์ Doc: postgresql.org/docs/9.5/static/sql-insert.html
borjagvo

379

ฉันจะเขียนคำว่า 'INSERT ได้อย่างไรถ้าไม่มีคำสั่ง SQL อยู่แล้ว?

มีวิธีที่ดีในการทำ INSERT แบบมีเงื่อนไขใน PostgreSQL:

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

ถ้ำวิธีนี้ไม่น่าเชื่อถือ 100% สำหรับการดำเนินการเขียนพร้อมกัน มีสภาพการแข่งขันที่เล็กมากระหว่างSELECTการNOT EXISTSต่อต้านกึ่งเข้าร่วมกับINSERTตัวมันเอง มันอาจล้มเหลวภายใต้เงื่อนไขดังกล่าว


สิ่งนี้ปลอดภัยแค่ไหนถ้าสมมติว่า "ชื่อ" - ฟิลด์มีข้อ จำกัด ที่ไม่ซ้ำใคร มันจะล้มเหลวด้วยการละเมิดที่ไม่ซ้ำกันหรือไม่?
agnsaft

2
ใช้งานได้ดี ปัญหาเดียวคือข้อต่อที่ฉันเดาว่าจะเกิดอะไรขึ้นถ้ามีการปรับเปลี่ยนตารางเพื่อให้คอลัมน์มีจำนวนมากขึ้น ในกรณีนั้นสคริปต์ทั้งหมดต้องถูกแก้ไข มันคงจะดีถ้ามีวิธีทั่วไปมากขึ้นการทำเช่นนี้ ...
วิลเล็มแวน Onsem

1
มันเป็นไปได้ที่จะใช้กับRETURNS idตัวอย่างเพื่อให้ได้idว่ามีการแทรกหรือไม่?
Olivier Pons

2
@ OlivierPons ใช่มันเป็นไปได้ เพิ่มRETURNING idที่และของแบบสอบถามและจะส่งกลับรหัสแถวใหม่หรือไม่มีอะไรเลยหากไม่มีการแทรกแถว
AlexM

4
ฉันพบว่าสิ่งนี้ไม่น่าเชื่อถือ ดูเหมือนว่าบางครั้ง Postgres จะดำเนินการแทรกก่อนที่จะทำการเลือกและฉันจบลงด้วยการละเมิดคีย์ที่ซ้ำกันแม้ว่าจะยังไม่ได้แทรกบันทึก ลองใช้ version => 9.5 ด้วย ON CONFLICT
Michael Silver

51

วิธีหนึ่งคือการสร้างตารางที่ไม่มีข้อ จำกัด (ไม่มีดัชนีที่ไม่ซ้ำกัน) เพื่อแทรกข้อมูลทั้งหมดของคุณลงในและทำการเลือกที่แตกต่างจากที่จะแทรกของคุณลงในตารางร้อยของคุณ

ดังนั้นระดับสูงจะเป็น ฉันคิดว่าทั้งสามคอลัมน์มีความแตกต่างในตัวอย่างของฉันดังนั้นสำหรับขั้นตอนที่ 3 เปลี่ยนการเข้าร่วม NOT EXITS เพื่อเข้าร่วมเฉพาะคอลัมน์ที่ไม่ซ้ำกันในตารางร้อย

  1. สร้างตารางชั่วคราว ดูเอกสารที่นี่

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. แทรกข้อมูลลงในตารางอุณหภูมิ

    INSERT INTO temp_data(name, name_slug, status); 
  3. เพิ่มดัชนีใด ๆ ลงในตารางชั่วคราว

  4. ทำการแทรกตารางหลัก

    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
นี่เป็นวิธีที่เร็วที่สุดที่ฉันพบว่าจะต้องเพิ่มเม็ดมีดเมื่อไม่ทราบว่ามีแถวอยู่หรือไม่
c

เลือก 'X' หรือไม่ บางคนสามารถอธิบายได้หรือไม่ นี่เป็นเพียงข้อความสั่งที่ถูกต้องSELECT name,name_slug,statusหรือ:*
roberthuttinger

3
ค้นหาแบบสอบถามย่อยที่มีความสัมพันธ์กัน 'X' อาจเปลี่ยนเป็น 1 หรือแม้แต่ 'SadClown' SQL ต้องมีบางสิ่งบางอย่างและ 'X' เป็นสิ่งที่ใช้กันทั่วไป มีขนาดเล็กและทำให้เห็นได้ชัดว่ามีการใช้คิวรีย่อยที่มีความสัมพันธ์และตรงตามข้อกำหนดของ SQL ที่ต้องการ
Kuberchaun

คุณกล่าวถึง "แทรกข้อมูลทั้งหมดของคุณลงใน (สมมติว่า temp table) และทำการเลือกที่แตกต่างจากนั้น" ในกรณีนั้นไม่ควรเป็นเช่นนั้นSELECT DISTINCT name, name_slug, status FROM temp_data?
gibbz00

17

แต่น่าเสียดายที่PostgreSQLการสนับสนุนค่าMERGEมิได้ON DUPLICATE KEY UPDATEดังนั้นคุณจะต้องทำมันในสองงบ:

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

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

คุณสามารถห่อมันเป็นฟังก์ชั่น:

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

และเพียงเรียกว่า:

SELECT  fn_upd_invoices('12345', 'TRUE')

1
จริงๆแล้วมันใช้งานไม่ได้: ฉันสามารถโทรได้INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);หลายครั้งและมันจะแทรกแถวเข้าไป
AP257

1
@ 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 hundredAP257: มีอยู่หนึ่งระเบียน
Quassnoi

12

คุณสามารถใช้ประโยชน์จาก VALUES ได้ที่ Postgres:

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

12
เลือกชื่อจากบุคคล <--- ถ้ามีคนเป็นพันล้านแถวล่ะ?
Henley Chiu

1
ฉันคิดว่านี่เป็นวิธีที่รวดเร็วในการแก้ปัญหา แต่เมื่อคุณแน่ใจว่าตารางต้นฉบับจะไม่เติบโตมาก ฉันมีตารางที่จะไม่มีแถวมากกว่า 1,000 แถวดังนั้นฉันจึงสามารถใช้โซลูชันนี้ได้
Leonard

ว้าวนี่คือสิ่งที่ฉันต้องการ ฉันกังวลว่าฉันจะต้องสร้างฟังก์ชั่นหรือตารางชั่วคราว แต่สิ่งนี้ทำให้ทุกอย่างหมดไป - ขอบคุณ!
Amalgovinus

8

ฉันรู้ว่าคำถามนี้เกิดขึ้นเมื่อไม่นานมานี้ แต่คิดว่าอาจช่วยได้ ฉันคิดว่าวิธีที่ง่ายที่สุดในการทำเช่นนี้คือผ่านทริกเกอร์ เช่น:

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

ดำเนินการรหัสนี้จากพรอมต์ psql (หรือคุณต้องการรันการสืบค้นโดยตรงบนฐานข้อมูล) จากนั้นคุณสามารถแทรกตามปกติจาก Python เช่น:

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

โปรดทราบว่าตามที่ @Thomas_Wouters ได้กล่าวถึงแล้วโค้ดด้านบนใช้ประโยชน์จากพารามิเตอร์มากกว่าการต่อสตริง


หากมีใครสงสัยเช่นกันจากเอกสาร : "ทริกเกอร์ระดับแถวที่ถูกยิงก่อนหน้านี้สามารถส่งคืน null เพื่อส่งสัญญาณตัวจัดการทริกเกอร์เพื่อข้ามส่วนที่เหลือของการดำเนินการสำหรับแถวนี้ (เช่นทริกเกอร์ที่ตามมาจะไม่ถูกไล่ออก / DELETE จะไม่เกิดขึ้นสำหรับแถวนี้) หากส่งคืนค่า nonnull การดำเนินการจะดำเนินการกับค่าแถวนั้น "
Pete

คำตอบนี้ฉันกำลังมองหา ทำความสะอาดโค้ดโดยใช้ฟังก์ชั่น + ทริกเกอร์แทนการเลือกคำสั่ง +1
Jacek Krawczyk

ฉันชอบคำตอบนี้ใช้ฟังก์ชั่นและทริกเกอร์ ตอนนี้ฉันหาวิธีอื่นเพื่อหยุดการหยุดชะงักโดยใช้ฟังก์ชั่นและทริกเกอร์ ...
Sukma Saputra

7

มีวิธีที่ดีในการทำ INSERT แบบมีเงื่อนไขใน PostgreSQL โดยใช้ WITH query: Like:

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

นี่เป็นปัญหาที่ฉันเผชิญและรุ่นของฉันคือ 9.5

และฉันแก้มันด้วยแบบสอบถาม SQL ด้านล่าง

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;

หวังว่าจะช่วยให้คนที่มีปัญหาเดียวกันกับรุ่น> = 9.5

ขอบคุณที่อ่าน.


5

ใส่ .. ที่ซึ่งไม่มีอยู่เป็นวิธีการที่ดี และเงื่อนไขการแข่งขันสามารถหลีกเลี่ยงได้โดยการทำธุรกรรม "ซองจดหมาย":

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

2

มันง่ายกับกฎ:

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

แต่มันล้มเหลวพร้อมเขียน ...


1

วิธีการที่มี upvotes มากที่สุด (จาก John Doe) ทำงานได้ดีสำหรับฉัน แต่ในกรณีของฉันจากแถว 422 ที่คาดหวังฉันได้รับเพียง 180 ฉันไม่พบสิ่งผิดปกติและไม่มีข้อผิดพลาดเลยดังนั้นฉันจึงมองหาวิธีอื่น วิธีการง่ายๆ

ใช้IF NOT FOUND THENหลังจากSELECTเพียงแค่ทำงานได้อย่างสมบูรณ์แบบสำหรับฉัน

(อธิบายไว้ในเอกสารประกอบ PostgreSQL )

ตัวอย่างจากเอกสาร:

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

1

psycopgs ระดับเคอร์เซอร์มีแอตทริบิวต์rowcount

แอ็ตทริบิวต์อ่านอย่างเดียวนี้ระบุจำนวนของแถวที่ execute * () ที่สร้างล่าสุด (สำหรับคำสั่ง DQL เช่น SELECT) หรือได้รับผลกระทบ (สำหรับคำสั่ง DML เช่น UPDATE หรือ INSERT)

ดังนั้นคุณสามารถลองอัปเดตก่อนและใส่เฉพาะเมื่อ rowcount เป็น 0

แต่ขึ้นอยู่กับระดับกิจกรรมในฐานข้อมูลของคุณคุณอาจพบกับสภาพการแข่งขันระหว่าง UPDATE และ INSERT โดยที่กระบวนการอื่นอาจสร้างระเบียนนั้นขึ้นในระหว่างกาล


การห่อแบบสอบถามเหล่านี้ในการทำธุรกรรมน่าจะช่วยบรรเทาสภาพการแข่งขัน
Daniel Lyons

ขอบคุณวิธีแก้ปัญหาที่ง่ายและสะอาดจริงๆ
Alexander Malfait

1

ดูเหมือนว่าคอลัมน์ของคุณ "ร้อย" จะถูกกำหนดเป็นคีย์หลักและดังนั้นจึงต้องไม่ซ้ำกันซึ่งไม่ใช่กรณี ปัญหาไม่ได้เกิดจากข้อมูลของคุณ

ฉันขอแนะนำให้คุณใส่รหัสเป็นประเภทอนุกรมเพื่อใช้คีย์หลักในมือ


1

หากคุณบอกว่าหลายแถวเหมือนกันคุณจะสิ้นสุดการตรวจสอบหลายครั้ง คุณสามารถส่งพวกเขาและฐานข้อมูลจะกำหนดว่าจะแทรกหรือไม่ด้วยประโยค ON CONFLICT ดังต่อไปนี้

  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

ฉันกำลังมองหาโซลูชันที่คล้ายกันพยายามค้นหา SQL ที่ทำงานใน PostgreSQL เช่นเดียวกับ HSQLDB (HSQLDB เป็นสิ่งที่ทำให้สิ่งนี้ยาก) โดยใช้ตัวอย่างของคุณเป็นพื้นฐานนี่เป็นรูปแบบที่ฉันพบที่อื่น

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

นี่คือฟังก์ชั่นหลามทั่วไปที่ได้รับ tablename คอลัมน์และค่าสร้างสุดยอดเทียบเท่าสำหรับ postgresql

นำเข้า 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

การแก้ปัญหาในง่าย ๆ แต่ไม่ใช่ในทันที
หากคุณต้องการใช้คำสั่งนี้คุณต้องทำการเปลี่ยนแปลงหนึ่งครั้งกับ db:

ALTER USER user SET search_path to 'name_of_schema';

หลังจากการเปลี่ยนแปลง "INSERT" เหล่านี้จะทำงานได้อย่างถูกต้อง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.