[SQL] SELECT 또는 INSERT는 경쟁 조건에 발생하기 쉬운 함수에서인가?
SQLSELECT 또는 INSERT는 경쟁 조건에 발생하기 쉬운 함수에서인가?
나는 간단한 블로그 엔진에 대한 게시물을 작성하는 기능을 썼다 :
CREATE FUNCTION CreatePost(VARCHAR, TEXT, VARCHAR[])
RETURNS INTEGER AS $$
DECLARE
InsertedPostId INTEGER;
TagName VARCHAR;
BEGIN
INSERT INTO Posts (Title, Body)
VALUES ($1, $2)
RETURNING Id INTO InsertedPostId;
FOREACH TagName IN ARRAY $3 LOOP
DECLARE
InsertedTagId INTEGER;
BEGIN
-- I am concerned about this part.
BEGIN
INSERT INTO Tags (Name)
VALUES (TagName)
RETURNING Id INTO InsertedTagId;
EXCEPTION WHEN UNIQUE_VIOLATION THEN
SELECT INTO InsertedTagId Id
FROM Tags
WHERE Name = TagName
FETCH FIRST ROW ONLY;
END;
INSERT INTO Taggings (PostId, TagId)
VALUES (InsertedPostId, InsertedTagId);
END;
END LOOP;
RETURN InsertedPostId;
END;
$$ LANGUAGE 'plpgsql';
이것은 동시에 할 때 여러 사용자가 삭제 태그 및 작성 게시물 경쟁 조건하는 경향이 있습니까? 특히, 거래 (및 기능) 발생에서 인종 조건 방지합니까? 나는 PostgreSQL을 9.2.3을 사용하고 있습니다.
해결법
-
==============================
1.그것은 (INSERT 또는 UPDATE 임)에 관한 반복 가능한 동시 기록 하중 SELECT 또는 INSERT의 문제 (그러나 다른)을 UPSERT이다.
그것은 (INSERT 또는 UPDATE 임)에 관한 반복 가능한 동시 기록 하중 SELECT 또는 INSERT의 문제 (그러나 다른)을 UPSERT이다.
갈등 .. DO UPDATE ON ... 새로운 UPSERT 구현 INSERT를 사용하여, 우리는 크게 단순화 할 수 있습니다. INSERT에 PL / pgSQL의 기능 또는 하나의 행 (태그)를 선택 :
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int) AS $func$ BEGIN SELECT tag_id -- only if row existed before FROM tag WHERE tag = _tag INTO _tag_id; IF NOT FOUND THEN INSERT INTO tag AS t (tag) VALUES (_tag) ON CONFLICT (tag) DO NOTHING RETURNING t.tag_id INTO _tag_id; END IF; END $func$ LANGUAGE plpgsql;
경쟁 조건에 대한 작은 창은 여전히있다. 절대적으로하려면 확실히 당신은 ID를 얻을 :
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int) AS $func$ BEGIN LOOP SELECT tag_id FROM tag WHERE tag = _tag INTO _tag_id; EXIT WHEN FOUND; INSERT INTO tag AS t (tag) VALUES (_tag) ON CONFLICT (tag) DO NOTHING RETURNING t.tag_id INTO _tag_id; EXIT WHEN FOUND; END LOOP; END $func$ LANGUAGE plpgsql;
이것은 INSERT 또는 SELECT 중 하나가 성공할 때까지 반복 유지합니다. 요구:
SELECT f_tag_id('possibly_new_tag');
동일한 트랜잭션의 후속 명령이 행의 존재에 의존하고 다른 트랜잭션의 업데이트가 동시에 삭제하거나하는 것이 실제로 가능하다면, 당신은 공유에 대한과 SELECT 문에 기존 행을 잠글 수 있습니다. 행 대신 삽입됩니다 경우, 어쨌든 트랜잭션이 끝날 때까지 잠겨 있습니다.
새로운 행이 대부분의 시간을 삽입하면 빨리를 만들기 위해 INSERT로 시작합니다.
관련 :
INSERT에 관련 (순수 SQL) 솔루션 또는 한 번에 여러 행 (세트)을 선택합니다 :
나는 이전에이 SQL 함수를 제안했다 :
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int) AS $func$ WITH ins AS ( INSERT INTO tag AS t (tag) VALUES (_tag) ON CONFLICT (tag) DO NOTHING RETURNING t.tag_id ) SELECT tag_id FROM ins UNION ALL SELECT tag_id FROM tag WHERE tag = _tag LIMIT 1 $func$ LANGUAGE sql;
어떤 전적으로 잘못이지만, @FunctorSalad 그의 추가 답변에서 일처럼 허점을 밀봉하는 데 실패합니다. 동시 트랜잭션이 동시에 동일한 작업을 수행하려고하면이 함수는 빈 결과와 함께 올 수 있습니다. CTE를 사용하여 쿼리의 모든 문을 거의 동시에 실행됩니다. 수동 :
동시 트랜잭션이 순간 이전 같은 새 태그를 삽입, 아직 커밋되지 않은 경우 :
우리는 아무것도 얻을 수 없다. 로 적당하지 않습니다. 즉, (그리고 내가 거기에 잡힌) 순진한 논리에 반 직관적하지만 포스트 그레스의 MVCC 모델이 어떻게 작동하는지 그의는 - 일에 있습니다.
여러 트랜잭션이 동시에 같은 태그를 삽입 할 수있는 경우에 따라서이를 사용하지 마십시오. 또는 루프 실제로 행을 얻을 때까지. 루프는 좀처럼 일반적인 작업 부하에 트리거되지 않습니다.
이 (약간 단순화) 테이블을 감안할 때 :
CREATE table tag ( tag_id serial PRIMARY KEY , tag text UNIQUE );
... 실질적으로 100 % 보안 기능은 다음과 같을 수, 기존 하나를 선택 / 새 태그를 삽입합니다. 왜 100 %? 관련 UPSERT 예에 대한 설명서의주의 사항을 고려 :
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT tag_id int) AS $func$ BEGIN LOOP BEGIN WITH sel AS (SELECT t.tag_id FROM tag t WHERE t.tag = _tag FOR SHARE) , ins AS (INSERT INTO tag(tag) SELECT _tag WHERE NOT EXISTS (SELECT 1 FROM sel) -- only if not found RETURNING tag.tag_id) -- qualified so no conflict with param SELECT sel.tag_id FROM sel UNION ALL SELECT ins.tag_id FROM ins INTO tag_id; EXCEPTION WHEN UNIQUE_VIOLATION THEN -- insert in concurrent session? RAISE NOTICE 'It actually happened!'; -- hardly ever happens END; EXIT WHEN tag_id IS NOT NULL; -- else keep looping END LOOP; END $func$ LANGUAGE plpgsql;
SQL 바이올린.
당신이 대부분으로 Foreach 루프를 단순화 할 수있는이 기능을 사용 :
... FOREACH TagName IN ARRAY $3 LOOP INSERT INTO taggings (PostId, TagId) VALUES (InsertedPostId, f_tag_id(TagName)); END LOOP; ...
빠른하지만) (unnest 단일 SQL 문으로 :
INSERT INTO taggings (PostId, TagId) SELECT InsertedPostId, f_tag_id(tag) FROM unnest($3) tag;
전체 루프를 대체합니다.
이 변종은 LIMIT 절을 UNION ALL의 동작을 기반으로 : 충분한 행이 발견하는 즉시로, 나머지는 실행되지 않습니다 :
이 바탕, 우리는 별도의 기능에 INSERT를 아웃소싱 할 수 있습니다. 만 우리는 예외 처리가 필요합니다. 그냥 첫 번째 해결 방법으로 안전뿐만.
CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int) RETURNS int AS $func$ BEGIN INSERT INTO tag(tag) VALUES (_tag) RETURNING tag.tag_id INTO tag_id; EXCEPTION WHEN UNIQUE_VIOLATION THEN -- catch exception, NULL is returned END $func$ LANGUAGE plpgsql;
주요 기능에 사용되는 :
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int) AS $func$ BEGIN LOOP SELECT tag_id FROM tag WHERE tag = _tag UNION ALL SELECT f_insert_tag(_tag) -- only executed if tag not found LIMIT 1 -- not strictly necessary, just to be clear INTO _tag_id; EXIT WHEN _tag_id IS NOT NULL; -- else keep looping END LOOP; END $func$ LANGUAGE plpgsql;
-
==============================
2.포스트 그레스 9.5에 소개 된 ON 충돌 절을 사용하는 경우에도 조심하는 것이 여전히있다. 우리가 할 경우, @Erwin Brandstetter 응답에서와 동일한 기능과 예를 들어 테이블을 사용 :
포스트 그레스 9.5에 소개 된 ON 충돌 절을 사용하는 경우에도 조심하는 것이 여전히있다. 우리가 할 경우, @Erwin Brandstetter 응답에서와 동일한 기능과 예를 들어 테이블을 사용 :
Session 1: begin; Session 2: begin; Session 1: select f_tag_id('a'); f_tag_id ---------- 11 (1 row) Session 2: select f_tag_id('a'); [Session 2 blocks] Session 1: commit; [Session 2 returns:] f_tag_id ---------- NULL (1 row)
그래서 단일 스레드 세계에서 불가능한 것이다, 세션 2에서 반환 NULL을 f_tag_id!
우리는 반복 읽기 (또는 강한 직렬화)에 대한 트랜잭션 격리 수준을 올릴 경우, 세션 2는 오류가 발생합니다 : 직렬화 액세스 할 수 없었다 인해 동시 갱신을 대신. 적어도 아니오 "불가능"결과 그래서,하지만 불행히도 우리는 이제 트랜잭션을 다시 시도 할 준비를해야합니다.
편집 : 세션 1 삽입 태그 A는, 다음 세션이 삽입 B, 다음 세션 1 시도가를 삽입이 개 시도를 B와 세션을 삽입 할 경우 반복 읽기 또는 직렬화으로는, 하나 개의 세션은 교착 상태를 감지 :
ERROR: deadlock detected DETAIL: Process 14377 waits for ShareLock on transaction 1795501; blocked by process 14363. Process 14363 waits for ShareLock on transaction 1795503; blocked by process 14377. HINT: See server log for query details. CONTEXT: while inserting index tuple (0,3) in relation "tag" SQL function "f_tag_id" statement 1
다시 교착 상태 오류 롤을받은 세션 후, 다른 세션이 계속됩니다. 나는 우리가이 같은 상황에서, 단지 serialization_failure 및 재시도 같은 교착 상태 처리해야 추측 그래서?
또한, 일관된 순서로 태그를 삽입하지만, 그들은 모두 한 곳에서 추가되지 않는 경우이 쉽지 않다.
-
==============================
3.나는 태그가 이미 존재 때 트랜잭션이 그것을 발견 한 후 다른 트랜잭션에 의해 삭제 될 수있는 약간의 기회가있다 생각합니다. 는 SELECT FOR 업데이트를 사용하여 그 해결해야한다.
나는 태그가 이미 존재 때 트랜잭션이 그것을 발견 한 후 다른 트랜잭션에 의해 삭제 될 수있는 약간의 기회가있다 생각합니다. 는 SELECT FOR 업데이트를 사용하여 그 해결해야한다.
from https://stackoverflow.com/questions/15939902/is-select-or-insert-in-a-function-prone-to-race-conditions by cc-by-sa and MIT license
'SQL' 카테고리의 다른 글
[SQL] 차이 사이의 가입 및 INNER는 가입 (0) | 2020.03.12 |
---|---|
[SQL] 준비된 문을 사용하여 변수 열 이름 (0) | 2020.03.12 |
[SQL] ERROR 1452 : 추가 또는 자식 행을 업데이트 할 수 없습니다 : 외래 키 제약 조건이 실패 (0) | 2020.03.12 |
[SQL] 내부는 어디 대 가입 (0) | 2020.03.12 |
[SQL] SQL 쿼리가 매일 실행하는 방법 작업을 예약하려면? (0) | 2020.03.11 |