PostgreSQL에서 중복 업데이트에 삽입 하시겠습니까?
-
12-09-2019 - |
문제
몇 달 전에 나는 다음 구문을 사용하여 MySQL에서 한 번에 여러 업데이트를 한 번에 수행하는 방법에 대한 답변에서 답변했습니다.
INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);
나는 이제 PostgreSQL로 전환했는데 분명히 이것은 정확하지 않습니다. 그것은 모든 올바른 테이블을 언급하기 때문에 다른 키워드가 사용되는 문제라고 가정하지만 PostgreSQL 문서에서 어디에 있는지 잘 모르겠습니다.
명확히하기 위해, 나는 몇 가지를 삽입하고 이미 업데이트하기 위해 존재하는 경우를 삽입하고 싶습니다.
해결책
버전 9.5 이후 Postgresql 상향 구문 갈등에 절. 다음 구문 (MySQL과 유사)
INSERT INTO the_table (id, column_1, column_2)
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE
SET column_1 = excluded.column_1,
column_2 = excluded.column_2;
"Upsert"에 대한 PostgreSQL의 이메일 그룹 아카이브 검색 매뉴얼에서 원하는 일을하는 예:
예 38-2. 업데이트/인서트가있는 예외
이 예제는 예외 처리를 사용하여 업데이트 또는 삽입을 수행합니다.
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
-- note that "a" must be unique
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');
9.1 이상의 CTE를 사용하여 대량 으로이 작업을 수행하는 방법에 대한 예가있을 수 있습니다. 해커 메일 링리스트:
WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;
보다 a_horse_with_no_name의 답변 명확한 예를 위해.
다른 팁
경고 : 여러 세션에서 동시에 실행되는 경우 안전하지 않습니다. (아래의 경고 참조).
PostgreSQL에서 "Upsert"를 수행하는 또 다른 영리한 방법은 각각 성공하거나 영향을 미치지 않도록 설계된 두 개의 순차적 업데이트/삽입 문을 수행하는 것입니다.
UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
SELECT 3, 'C', 'Z'
WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);
"ID = 3"이 이미 존재하면 업데이트가 성공합니다. 그렇지 않으면 효과가 없습니다.
"ID = 3"이있는 행이 아직 존재하지 않는 경우에만 인서트가 성공합니다.
이 두 가지를 단일 문자열로 결합하여 응용 프로그램에서 단일 SQL 문으로 실행하여 둘 다 실행할 수 있습니다. 단일 거래에서 함께 실행하는 것이 좋습니다.
이것은 분리되어 있거나 잠긴 테이블에서 실행될 때 매우 잘 작동하지만 행이 동시에 삽입되거나 행이 동시에 삽입 될 때 행이 삽입되지 않은 상태에서 종료 될 수있는 경우 중복 키 오류로 여전히 실패 할 수 있음을 의미합니다. . ㅏ SERIALIZABLE
PostgreSQL 9.1 이상의 거래는 매우 높은 직렬화 실패율의 비용으로 안정적으로 처리 할 것이므로 많은 재 시도가 필요합니다. 보다 Upsert가 그렇게 복잡한 이유는 무엇입니까?, 이 경우에 대해 자세히 설명합니다.
이 접근법도 있습니다 업데이트 손실이 적용됩니다 read committed
응용 프로그램이 영향을받는 행 계산을 확인하고 insert
아니면 그 update
줄에 영향을 미쳤습니다.
PostgreSQL 9.1에서는 쓰기 가능한 CTE를 사용하여 달성 할 수 있습니다.일반적인 테이블 표현):
WITH new_values (id, field1, field2) as (
values
(1, 'A', 'X'),
(2, 'B', 'Y'),
(3, 'C', 'Z')
),
upsert as
(
update mytable m
set field1 = nv.field1,
field2 = nv.field2
FROM new_values nv
WHERE m.id = nv.id
RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1
FROM upsert up
WHERE up.id = new_values.id)
이 블로그 항목을 참조하십시오.
이 솔루션은 그렇습니다 ~ 아니다 고유 한 키 위반을 방지하지만 업데이트 손실에 취약하지 않습니다.
참조 DBA.stackexchange.com에서 Craig Ringer의 후속 조치
PostgreSQL 9.5 및 최신에서 사용할 수 있습니다 INSERT ... ON CONFLICT UPDATE
.
보다 문서.
MySQL INSERT ... ON DUPLICATE KEY UPDATE
직접적으로 a ON CONFLICT UPDATE
. SQL-Standard 구문도 마찬가지이며 둘 다 데이터베이스 별 확장자입니다. 좋은 이유가 있습니다 MERGE
이것에 사용되지 않았습니다, 새로운 구문은 재미를 위해 만들어진 것이 아닙니다. (MySQL의 구문에는 직접 채택되지 않은 문제도 있습니다).
예 : 주어진 설정 :
CREATE TABLE tablename (a integer primary key, b integer, c integer);
INSERT INTO tablename (a, b, c) values (1, 2, 3);
MySQL 쿼리 :
INSERT INTO tablename (a,b,c) VALUES (1,2,3)
ON DUPLICATE KEY UPDATE c=c+1;
:
INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;
차이점 :
너 ~ 해야 하다 고유성 검사에 사용할 열 이름 (또는 고유 한 제약 조건 이름)을 지정하십시오. 그게 그게
ON CONFLICT (columnname) DO
키워드
SET
이것이 정상인 것처럼 사용해야합니다.UPDATE
성명
좋은 기능도 있습니다.
당신은 가질 수 있습니다
WHERE
귀하의 조항UPDATE
(효과적으로 회전하게합니다ON CONFLICT UPDATE
~ 안으로ON CONFLICT IGNORE
특정 값에 대해)제안 된 삽입 값은 행 변수로 사용할 수 있습니다
EXCLUDED
, 대상 테이블과 동일한 구조를 갖습니다. 테이블 이름을 사용하여 테이블에서 원래 값을 얻을 수 있습니다. 이 경우EXCLUDED.c
될거야10
(우리가 삽입하려고했던 것이기 때문에)"table".c
될거야3
그것이 테이블의 현재 값이기 때문입니다. 당신은 두 가지 또는 둘 다를 사용할 수 있습니다SET
표현 및WHERE
절.
Upsert에 대한 배경은 참조하십시오 PostgreSQL에서 Upsert (병합, 삽입 ... 중복 업데이트) 방법은 무엇입니까?
나는 여기에 왔을 때 같은 것을 찾고 있었지만 일반적인 "Upsert"기능의 부족은 나에게 약간 귀찮게했기 때문에 업데이트를 전달하고 해당 기능에 대한 인수로 SQL을 삽입 할 수 있다고 생각했습니다.
그것은 다음과 같습니다.
CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
RETURNS VOID
LANGUAGE plpgsql
AS $$
BEGIN
LOOP
-- first try to update
EXECUTE sql_update;
-- check if the row is found
IF FOUND THEN
RETURN;
END IF;
-- not found so insert the row
BEGIN
EXECUTE sql_insert;
RETURN;
EXCEPTION WHEN unique_violation THEN
-- do nothing and loop
END;
END LOOP;
END;
$$;
아마도 처음에하고 싶은 일을하고 "Upsert"를 배치하려면 TCL을 사용하여 SQL_UPDATE를 분할하고 개별 업데이트를 루프 할 수 있습니다. 사전 형식 적중은 매우 작은 것입니다. http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php
가장 높은 비용은 코드에서 쿼리를 실행하는 것입니다. 데이터베이스 측면에서 실행 비용이 훨씬 적습니다.
그것을하는 간단한 명령은 없습니다.
가장 올바른 접근법은 기능을 사용하는 것입니다. 문서.
또 다른 솔루션 (안전하지는 않지만)은 리턴으로 업데이트하고 업데이트 한 행을 확인하고 나머지 부분을 삽입하는 것입니다.
라인을 따라 무언가 :
update table
set column = x.column
from (values (1,'aa'),(2,'bb'),(3,'cc')) as x (id, column)
where table.id = x.id
returning id;
ID : 2가 반환되었다고 가정합니다.
insert into table (id, column) values (1, 'aa'), (3, 'cc');
물론 여기에는 명확한 인종 조건이 있기 때문에 조만간 (동시 환경에서) 구제 할 것입니다. 그러나 일반적으로 작동합니다.
여기에 있습니다 주제에 대한 더 길고 포괄적 인 기사.
개인적으로 삽입 문에 첨부 된 "규칙"을 설정했습니다. 시간당 고객 당 DNS 히트를 녹음 한 "DNS"테이블이 있다고 가정 해 봅시다.
CREATE TABLE dns (
"time" timestamp without time zone NOT NULL,
customer_id integer NOT NULL,
hits integer
);
업데이트 된 값으로 행을 다시 삽입하거나 아직 존재하지 않으면 만들 수 있기를 원했습니다. Customer_id와 시간에 키가 표시됩니다. 이 같은:
CREATE RULE replace_dns AS
ON INSERT TO dns
WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = new."time")
AND (dns.customer_id = new.customer_id))))
DO INSTEAD UPDATE dns
SET hits = new.hits
WHERE ((dns."time" = new."time") AND (dns.customer_id = new.customer_id));
업데이트 : 동시 인서트가 발생하면 고유 한 예외를 생성하므로 이는 실패 할 가능성이 있습니다. 그러나 비 말단 거래는 계속되고 성공할 것이며 종료 된 거래를 반복하면됩니다.
그러나 항상 많은 삽입물이 발생하는 경우 삽입 문에 테이블 잠금 장치를 넣을 것입니다. 그러나 고유 한 키를 업데이트하지 않는 업데이트는 안전하므로 작업이 없으면 대신 자문 잠금 장치를 사용하십시오.
또한 사본 명령은 규칙을 사용하지 않으므로 사본을 삽입하는 경우 대신 트리거를 사용해야합니다.
삽입하고 교체하려는 경우 위의 "Upsert"기능을 사용자 정의하십시오.
`
CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)
RETURNS void AS
$BODY$
BEGIN
-- first try to insert and after to update. Note : insert has pk and update not...
EXECUTE sql_insert;
RETURN;
EXCEPTION WHEN unique_violation THEN
EXECUTE sql_update;
IF FOUND THEN
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
ALTER FUNCTION upsert(text, text)
OWNER TO postgres;`
그리고 실행 후 다음과 같은 일을하십시오.
SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)
컴파일러 오류를 피하기 위해 이중 달러 규모를 넣는 것이 중요합니다.
- 속도 확인 ...
대부분의 대답과 유사하지만 약간 더 빠르게 작동합니다.
WITH upsert AS (UPDATE spider_count SET tally=1 WHERE date='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)
계정 설정을 이름 값 쌍으로 관리하는 데 동일한 문제가 있습니다. 설계 기준은 다른 클라이언트마다 다른 설정 세트를 가질 수 있다는 것입니다.
JWP와 유사한 내 솔루션은 대량 지우고 교체하여 응용 프로그램 내에서 병합 레코드를 생성하는 것입니다.
이것은 방탄, 플랫폼 독립적이며 클라이언트 당 약 20 개 이상의 설정이 없기 때문에 3 개의 상당히 낮은 부하 DB 호출 일뿐입니다. 아마도 가장 빠른 방법 일 것입니다.
개별 행을 업데이트하는 대안 - 예외를 확인한 다음 삽입 - 또는 일부 조합은 끔찍한 코드입니다. (위에서 언급 한 바와 같이) 비 표준 SQL 예외 처리가 DB에서 DB로 변경되거나 릴리스를 해제하기 때문에 종종 깨지기 때문입니다.
#This is pseudo-code - within the application:
BEGIN TRANSACTION - get transaction lock
SELECT all current name value pairs where id = $id into a hash record
create a merge record from the current and update record
(set intersection where shared keys in new win, and empty values in new are deleted).
DELETE all name value pairs where id = $id
COPY/INSERT merged records
END TRANSACTION
에 따라 Postgresql 문서 INSERT
성명, 처리 ON DUPLICATE KEY
사례는 지원되지 않습니다. 구문의 일부는 독점적 인 MySQL 확장입니다.
CREATE OR REPLACE FUNCTION save_user(_id integer, _name character varying)
RETURNS boolean AS
$BODY$
BEGIN
UPDATE users SET name = _name WHERE id = _id;
IF FOUND THEN
RETURN true;
END IF;
BEGIN
INSERT INTO users (id, name) VALUES (_id, _name);
EXCEPTION WHEN OTHERS THEN
UPDATE users SET name = _name WHERE id = _id;
END;
RETURN TRUE;
END;
$BODY$
LANGUAGE plpgsql VOLATILE STRICT
이 기능 병합을 사용합니다
CREATE OR REPLACE FUNCTION merge_tabla(key INT, data TEXT)
RETURNS void AS
$BODY$
BEGIN
IF EXISTS(SELECT a FROM tabla WHERE a = key)
THEN
UPDATE tabla SET b = data WHERE a = key;
RETURN;
ELSE
INSERT INTO tabla(a,b) VALUES (key, data);
RETURN;
END IF;
END;
$BODY$
LANGUAGE plpgsql
작은 세트를 병합하려면 위의 기능을 사용하는 것이 좋습니다. 그러나 많은 양의 데이터를 병합하는 경우 조사하는 것이 좋습니다. http://mbk.projects.postgresql.org
내가 알고있는 현재 모범 사례는 다음과 같습니다.
- 새/업데이트 된 데이터를 온도 테이블에 복사하십시오 (물론, 비용이 정상인 경우 삽입 할 수 있습니다)
- 잠금 획득 [선택 사항] (자문은 테이블 잠금, IMO에 바람직합니다)
- 병합. (재미있는 부분)
업데이트는 수정 된 행의 수를 반환합니다. JDBC (Java)를 사용하는 경우 0에 대해이 값을 확인하고 행이 영향을받지 않은 경우 대신 Fire Insert를 확인할 수 있습니다. 다른 프로그래밍 언어를 사용하는 경우 수정 된 행의 수를 여전히 얻을 수 있습니다. 문서를 확인하십시오.
이것은 우아하지는 않지만 호출 코드에서 사용하기에 더 간단한 SQL이 훨씬 간단합니다. 다르게, PL/PSQL에 10 줄 스크립트를 작성하면 단지 하나 또는 다른 종류의 단위 테스트가 있어야 할 것입니다.
편집하다: 이것은 예상대로 작동하지 않습니다. 허용 된 답변과 달리 두 프로세스가 반복적으로 호출 할 때 고유 한 주요 위반이 발생합니다. upsert_foo
동시에.
유레카! 하나의 쿼리에서 수행하는 방법을 알아 냈습니다. UPDATE ... RETURNING
행이 영향을 받았는지 테스트하기 위해 :
CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);
CREATE FUNCTION update_foo(k INT, v TEXT)
RETURNS SETOF INT AS $$
UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
$$ LANGUAGE sql;
CREATE FUNCTION upsert_foo(k INT, v TEXT)
RETURNS VOID AS $$
INSERT INTO foo
SELECT $1, $2
WHERE NOT EXISTS (SELECT update_foo($1, $2))
$$ LANGUAGE sql;
그만큼 UPDATE
불행히도 이것은 구문 오류이므로 별도의 절차에서 수행해야합니다.
... WHERE NOT EXISTS (UPDATE ...)
이제 원하는대로 작동합니다.
SELECT upsert_foo(1, 'hi');
SELECT upsert_foo(1, 'bye');
SELECT upsert_foo(3, 'hi');
SELECT upsert_foo(3, 'bye');