Postgres : 존재하지 않는 경우 INSERT


361

파이썬을 사용하여 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

그러나 첫째, 이것은 내가 필요로하는 과잉이며, 둘째, 그중 하나를 간단한 문자열로 어떻게 실행할 수 있습니까?


56
이 문제를 해결하는 방법에 관계없이 그런 식으로 쿼리를 생성해서는 안됩니다. 쿼리에 매개 변수를 사용하고 값을 개별적으로 전달하십시오. 참조 stackoverflow.com/questions/902408/...
토마스 Wouters

3
왜 예외를 잡아서 무시하지 않습니까?
Matthew Mitchell

5
Posgres 9.5 (현재 베타 2)부터 새로운 upsert와 같은 기능이 있습니다. postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno

2
이에 대한 답변을 고려한 적이 있습니까? =]
Relequestual

답변:


512

Postgres 9.5 (2016-01-07 이후 출시)는 INSERTON CONFLICT 절 이라고도 하는 "upsert" 명령을 제공합니다 .

INSERT ... ON CONFLICT DO NOTHING/UPDATE

그것은 동시 작업을 사용할 때 발생할 수있는 많은 미묘한 문제를 해결합니다.


14
9.5가 릴리스되었습니다.
luckydonald

2
PostgreSQL 9.5 이전의 @TusharJain에서는 "구식"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 문서에 연결하는 것이 좋습니다. 문서 링크 : postgresql.org/docs/9.5/static/sql-insert.html
borjagvo

379

'이 행이 존재하지 않는 한'INSERT 'SQL 문을 작성하려면 어떻게해야합니까?

PostgreSQL에서 조건부 INSERT를 수행하는 좋은 방법이 있습니다.

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

주의 사항 이 방법은 동시 쓰기 작업에 100 % 신뢰할 수 없습니다 . 사이 아주 작은 경쟁 조건이 SELECTNOT EXISTS반 반이-가입하고 INSERT자체. 그것은 와 같은 조건에서 실패합니다.


"이름"필드에 UNIQUE 제약 조건이 있다고 가정하면 이것이 얼마나 안전합니까? 고유 한 위반으로 실패 할 수 있습니까?
agnsaft

2
이것은 잘 작동합니다. 유일한 문제는 내가 생각하는 결합입니다 : 더 많은 열이 고유하도록 테이블을 수정하면 어떻게 될까요? 이 경우 모든 스크립트를 수정해야합니다. 이것을하는 더 일반적인 방법이 있다면 좋을 것입니다 ...
Willem Van Onsem

1
RETURNS id예를 들어 id삽입 여부 를 알기 위해 사용할 수 있습니까?
Olivier Pons

2
@OlivierPons 예, 가능합니다. RETURNING id및에 쿼리를 추가 하면 행이 삽입되지 않은 경우 새 행 ID를 반환하거나 아무것도 반환하지 않습니다.
AlexM

4
나는 이것이 신뢰할 수 없다는 것을 발견했다. Postgres는 선택을 실행하기 전에 때때로 삽입을 실행하고 레코드가 아직 삽입되지 않은 경우에도 중복 키 위반으로 나타납니다. ON CONFLICT와 함께 버전 => 9.5를 사용하십시오.
마이클 실버

51

한 가지 방법은 제한되지 않은 (고유 인덱스가없는) 테이블을 만들어 모든 데이터를 삽입하고 그와 다른 선택을 수행하여 백 테이블에 삽입하는 것입니다.

따라서 높은 수준입니다. 필자의 예제에서는 세 열이 모두 고유하다고 가정하므로 step3에서는 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
이것은 행이 이미 존재하는지 알 수 없을 때 대량 삽입을 수행하는 가장 빠른 방법입니다.
nate c

'X'를 선택 하시겠습니까? 누군가가 명확히 할 수 있습니까? 이것은 단순히 엄선 된 진술입니다. SELECT name,name_slug,status또는*
roberthuttinger

3
상관 된 하위 쿼리를 조회합니다. 'X'는 1 또는 'SadClown'으로 변경 될 수 있습니다. SQL에는 무언가가 필요하며 'X'는 일반적으로 사용됩니다. 크기가 작고 상관 된 하위 쿼리가 사용되고 있으며 SQL에 필요한 요구 사항을 충족시키는 것이 분명합니다.
Kuberchaun

"모든 데이터를 (임시 테이블 가정)에 삽입하고 그와 다른 선택을하십시오"라고 언급했습니다. 이 경우, 그렇지 않아야 SELECT DISTINCT name, name_slug, status FROM temp_data합니까?
gibbz00 2016 년

17

불행히도 nor도 PostgreSQL지원하지 않으므로 두 가지 진술로해야합니다.MERGEON 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
@ 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. 하나의 기록이 있습니다.
Quassnoi

12

Postgres에서 사용할 수있는 값을 사용할 수 있습니다.

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

12
SELECT name FROM Person <--- 10 억 개의 행이 있다면 어떻게합니까?
Henley Chiu

1
이것이 문제를 해결하는 좋은 빠른 방법이라고 생각하지만 소스 테이블이 절대 커지지 않을 때에 만 가능합니다. 1000 행을 넘지 않는 테이블이 있으므로이 솔루션을 사용할 수 있습니다.
레너드

와우, 이것이 바로 내가 필요한 것입니다. 함수 또는 임시 테이블을 만들어야 할까봐 걱정했지만이 모든 것이 불가능합니다. 감사합니다!
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가 이미 언급했듯이 위의 코드는 문자열을 연결하는 대신 매개 변수를 활용합니다.


다른 누군가가 궁금해하는 경우 문서에서 : "행하기 전에 트리거 된 행 레벨 트리거는 트리거 관리자에게이 행의 나머지 조작을 건너 뛰도록 신호를 보내기 위해 널을 리턴 할 수 있습니다 (즉, 후속 트리거가 실행되지 않고 INSERT / UPDATE 이 행에 대해서는 / DELETE가 발생하지 않습니다.) null이 아닌 값이 반환되면 해당 행 값으로 작업이 진행됩니다. "
Pete

정확히이 대답은 내가 찾고있었습니다. select 문 대신 function + trigger를 사용하여 코드를 정리하십시오. +1
Jacek Krawczyk

나는이 답변을 좋아하고 기능과 트리거를 사용합니다. 지금은 ... 기능과 트리거를 사용하여 교착 상태를 깰 수있는 또 다른 방법을 찾아
Sukma Saputra

7

WITH 쿼리를 사용하여 PostgreSQL에서 조건부 INSERT를 수행하는 좋은 방법이 있습니다.

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

(Doo Doe의) 가장 많은 upvotes를 가진 접근 방식은 어떻게 든 나를 위해 작동하지만 예상되는 422 행에서 나는 단지 180을 얻습니다. 나는 잘못된 것을 찾을 수 없으며 전혀 오류가 없으므로 다른 것을 찾았습니다. 간단한 접근.

사용 IF NOT FOUND THENSELECT바로 나를 위해 완벽하게 작동합니다.

(에 설명 된 PostgreSQL 설명서에 )

설명서의 예 :

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

1

psycopgs 커서 클래스에는 속성 rowcount가 있습니다.

이 읽기 전용 속성은 마지막 실행 * ()이 생성했거나 (SELECT와 같은 DQL 문의 경우) 영향을받는 (UPDATE 또는 INSERT와 같은 DML 문의 경우) 행 수를 지정합니다.

따라서 행 개수가 0 인 경우에만 UPDATE를 먼저 시도하고 INSERT를 시도 할 수 있습니다.

그러나 데이터베이스의 활동 레벨에 따라 다른 프로세스가 중간에 해당 레코드를 작성할 수있는 UPDATE와 INSERT 간의 경쟁 조건에 도달 할 수 있습니다.


이러한 쿼리를 트랜잭션에 래핑하면 경쟁 조건이 완화 될 수 있습니다.
다니엘 리용

감사합니다. 정말 간단하고 깨끗한 솔루션
Alexander Malfait

1

"백"열이 기본 키로 정의 된 것 같으므로 고유하지 않아야합니다. 문제는 데이터와 관련이 없습니다.

기본 키를 처리하기 위해 직렬 유형으로 ID를 삽입하는 것이 좋습니다.


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

비슷한 솔루션을 찾고 있었고 PostgreSQL과 HSQLDB에서 작동하는 SQL을 찾으려고했습니다. (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

다음은 테이블 이름, 열 및 값이 주어지면 postgresql에 해당하는 upsert를 생성하는 일반적인 파이썬 함수입니다.

수입 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.