PostgreSQL에서 UPSERT를 구현하는 관용적 방법


40

UPSERTPostgreSQL의 다양한 구현에 대해 읽었 지만이 모든 솔루션은 비교적 오래되었거나 비교적 이국적입니다 ( 예 : 쓰기 가능한 CTE 사용 ).

그리고 나는이 솔루션이 오래되어 잘 권장되는지 또는 거의 모든 제품이 생산 용도에 적합하지 않은 장난감 예제인지 여부를 즉시 알아내는 psql 전문가는 아닙니다.

PostgreSQL에서 UPSERT를 구현하는 가장 안전한 스레드 안전 방법은 무엇입니까?

답변:


23

PostgreSQL에는 이제 UPSERT가 있습니다.


비슷한 StackOverflow 질문 에 따른 선호되는 방법 은 현재 다음과 같습니다.

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

7
차라리 쓰기 가능한 CTE를 사용하고 싶습니다 : stackoverflow.com/a/8702291/330315
a_horse_with_no_name

쓰기 가능한 CTE와 함수의 장점은 무엇입니까?
François Beausoleil

1
@ François 한 가지, 속도. CTE를 사용하면 데이터베이스에 한 번 도달했습니다. 이 방법으로 두 번 이상 칠 수 있습니다. 또한 옵티마이 저는 순수 SQL 코드만큼 효율적으로 pl / pgsql 프로 시저를 최적화 할 수 없습니다.
Adam Mackler

1
@ François 또 다른 것은 동시성입니다. 위의 예에는 여러 개의 SQL 문이 있으므로 경쟁 조건 (klugey 루프의 이유)에 대해 걱정해야합니다. 단일 SQL 문은 원자 적입니다. 이 링크
Adam Mackler

1
@ FrançoisBeausoleil 왜 여기여기 를 참조 하십시오 . 기본적으로 재시도 루프가 없으면 직렬화해야하거나 내재 된 경쟁 조건으로 인해 실패 할 가능성이 있습니다.
Jack Douglas

27

업데이트 (2015-08-20) :

ON CONFLICT DO UPDATE(공식 문서)를 사용하여 업 서트를 처리하기위한 공식적인 구현이 있습니다 . 이 글을 쓰는 시점에서이 기능은 현재 PostgreSQL 9.5 Alpha 2에 있으며 Postgres 소스 디렉토리 에서 다운로드 할 수 있습니다 .

다음은 item_id기본 키 라고 가정하는 예입니다 .

INSERT INTO my_table
    (item_id, price)
VALUES
    (123456, 10.99)
ON
    CONFLICT (item_id)
DO UPDATE SET
    price = EXCLUDED.price

원본 게시물 ...

다음은 삽입 또는 업데이트 발생 여부에 대한 가시성을 얻고 자 할 때 달성 한 구현입니다.

정의는 upsert_data가격과 item_id를 두 번 지정하지 않고 값을 단일 자원으로 통합하는 것입니다. 업데이트에 대해 한 번, 삽입에 대해 다시 한 번.

WITH upsert_data AS (
    SELECT
    '19.99'::numeric(10,2) AS price,
    'abcdefg'::character varying AS item_id
),
update_outcome AS (
    UPDATE pricing_tbl
    SET price = upsert_data.price
    FROM upsert_data
    WHERE pricing_tbl.item_id = upsert_data.item_id
    RETURNING 'update'::text AS action, item_id
),
insert_outcome AS (
    INSERT INTO
        pricing_tbl
    (price, item_id)
    SELECT
        upsert_data.price AS price,
        upsert_data.item_id AS item_id
    FROM upsert_data
    WHERE NOT EXISTS (SELECT item_id FROM update_outcome LIMIT 1)
    RETURNING 'insert'::text AS action, item_id
)
SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome

의 사용이 마음에 들지 않으면 다음과 같은 upsert_data대체 구현이 있습니다.

WITH update_outcome AS (
    UPDATE pricing_tbl
    SET price = '19.99'
    WHERE pricing_tbl.item_id = 'abcdefg'
    RETURNING 'update'::text AS action, item_id
),
insert_outcome AS (
    INSERT INTO
        pricing_tbl
    (price, item_id)
    SELECT
        '19.99' AS price,
        'abcdefg' AS item_id
    WHERE NOT EXISTS (SELECT item_id FROM update_outcome LIMIT 1)
    RETURNING 'insert'::text AS action, item_id
)
SELECT * FROM update_outcome UNION ALL SELECT * FROM insert_outcome

어떻게 수행합니까?
jb.

1
@jb. 내가 원하는 것뿐만 아니라 직선 인서트를 수행하는 것과 비교하여 성능이 저하 될 수 있습니다. 그러나 더 작은 배치 (예 : 1000 이하)의 경우이 예제는 잘 수행됩니다.
Joshua Burns

0

삽입 또는 업데이트가 발생했는지 여부를 알려줍니다.

with "update_items" as (
  -- Update statement here
  update items set price = 3499, name = 'Uncle Bob'
  where id = 1 returning *
)
-- Insert statement here
insert into items (price, name)
-- But make sure you put your values like so
select 3499, 'Uncle Bob'
where not exists ( select * from "update_items" );

업데이트가 발생하면 삽입물 0이 표시되고 그렇지 않으면 삽입물 1 또는 오류가 발생합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.