Đây là sự cố lặp lại của SELECT
hoặc INSERT
dưới tải ghi đồng thời có thể, liên quan đến (nhưng khác với) UPSERT
(là INSERT
hoặc UPDATE
).
Hàm PL / pgSQL này sử dụng UPSERT (INSERT ... ON CONFLICT .. DO UPDATE
) thành INSERT
hoặc SELECT
a hàng đơn :
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql 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$;
Vẫn còn một cửa sổ nhỏ cho tình trạng cuộc đua. Để hoàn toàn chắc chắn chúng tôi nhận được một ID:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql 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$;
db <> fiddle here
Điều này tiếp tục lặp lại cho đến khi INSERT
hoặc SELECT
thành công. Gọi:
SELECT f_tag_id('possibly_new_tag');
Nếu các lệnh tiếp theo trong cùng một giao dịch dựa vào sự tồn tại của hàng và thực tế có thể các giao dịch khác cập nhật hoặc xóa nó đồng thời, bạn có thể khóa một hàng hiện có trong SELECT
câu lệnh với FOR SHARE
.
Nếu thay vào đó, hàng này sẽ bị khóa (hoặc không hiển thị đối với các giao dịch khác) cho đến khi kết thúc giao dịch.
Bắt đầu với trường hợp chung (INSERT
so với SELECT
) để làm cho nó nhanh hơn.
Có liên quan:
- Nhận Id từ INSERT có điều kiện
- Cách bao gồm các hàng bị loại trừ trong QUAY LẠI khỏi CHÈN ... BẬT MẶC BẤT NGỜ
Giải pháp (thuần SQL) có liên quan đến INSERT
hoặc SELECT
nhiều hàng (một bộ) cùng một lúc:
- Làm thế nào để sử dụng RETURNING với ON CONFLICT trong PostgreSQL?
Có chuyện gì với cái này giải pháp SQL thuần túy?
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE sql 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$;
Không hoàn toàn sai, nhưng nó không thể bịt được lỗ hổng, giống như @FunctorSalad đã làm. Hàm có thể đưa ra kết quả trống nếu một giao dịch đồng thời cố gắng thực hiện cùng một lúc. Hướng dẫn sử dụng:
Tất cả các câu lệnh được thực thi với cùng một ảnh chụp nhanh
Nếu một giao dịch đồng thời chèn cùng một thẻ mới trước đó nhưng chưa được cam kết:
-
Phần UPSERT trống, sau khi đợi giao dịch đồng thời kết thúc. (Nếu giao dịch đồng thời quay trở lại, nó vẫn chèn thẻ mới và trả về một ID mới.)
-
Phần CHỌN cũng trống vì nó dựa trên cùng một ảnh chụp nhanh, trong đó thẻ mới từ giao dịch đồng thời (chưa được cam kết) không hiển thị.
Chúng tôi không nhận được gì . Không như dự định. Điều đó phản trực giác so với logic ngây thơ (và tôi đã hiểu ở đó), nhưng đó là cách hoạt động của mô hình MVCC của Postgres - phải hoạt động.
Vì vậy, không sử dụng điều này nếu nhiều giao dịch có thể cố gắng chèn cùng một thẻ cùng một lúc. Hoặc lặp lại cho đến khi bạn thực sự nhận được một hàng. Dù sao thì vòng lặp sẽ hiếm khi được kích hoạt trong các tải công việc thông thường.
Postgres 9.4 trở lên
Với bảng này (đơn giản hơn một chút):
CREATE table tag (
tag_id serial PRIMARY KEY
, tag text UNIQUE
);
An ninh gần như 100% chức năng chèn thẻ mới / chọn thẻ hiện có, có thể giống như thế này.
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT tag_id int)
LANGUAGE plpgsql 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$;
db <> fiddle here
Sqlfiddle cũ
Tại sao không phải là 100%? Xem xét các ghi chú trong sách hướng dẫn cho UPSERT
liên quan ví dụ:
- https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
Giải thích
-
Hãy thử
SELECT
đầu tiên . Bằng cách này, bạn tránh được đắt hơn đáng kể 99,99% thời gian xử lý ngoại lệ. -
Sử dụng CTE để giảm thiểu khoảng thời gian (vốn đã nhỏ) cho điều kiện cuộc đua.
-
Khoảng thời gian giữa
SELECT
vàINSERT
trong một truy vấn là siêu nhỏ. Nếu bạn không có tải nặng đồng thời hoặc nếu bạn có thể sống với ngoại lệ mỗi năm một lần, bạn có thể bỏ qua trường hợp này và sử dụng câu lệnh SQL, câu lệnh này nhanh hơn. -
Không cần
FETCH FIRST ROW ONLY
(=LIMIT 1
). Tên thẻ rõ ràng làUNIQUE
. -
Xóa
FOR SHARE
trong ví dụ của tôi nếu bạn thường không cóDELETE
đồng thời hoặcUPDATE
trên bảngtag
. Tốn một chút hiệu suất. -
Không bao giờ trích dẫn tên ngôn ngữ:
'plpgsql'.plpgsql
là một số nhận dạng . Trích dẫn có thể gây ra sự cố và chỉ được chấp nhận để tương thích ngược. -
Không sử dụng các tên cột không mang tính mô tả như
id
hoặcname
. Khi tham gia một vài bảng ( đó là những gì bạn làm trong DB quan hệ), bạn có nhiều tên giống nhau và phải sử dụng bí danh.
Được tích hợp vào chức năng của bạn
Sử dụng chức năng này, bạn có thể đơn giản hóa phần lớn FOREACH LOOP
của mình tới:
...
FOREACH TagName IN ARRAY $3
LOOP
INSERT INTO taggings (PostId, TagId)
VALUES (InsertedPostId, f_tag_id(TagName));
END LOOP;
...
Tuy nhiên, nhanh hơn như một câu lệnh SQL đơn với unnest()
:
INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM unnest($3) tag;
Thay thế toàn bộ vòng lặp.
Giải pháp thay thế
Biến thể này được xây dựng dựa trên hành vi của UNION ALL
với LIMIT
mệnh đề:ngay khi tìm thấy đủ hàng, phần còn lại sẽ không bao giờ được thực thi:
- Cách để thử nhiều lần CHỌN cho đến khi có kết quả?
Dựa trên điều này, chúng tôi có thể thuê ngoài INSERT
thành một chức năng riêng biệt. Chỉ ở đó chúng tôi cần xử lý ngoại lệ. Cũng an toàn như giải pháp đầu tiên.
CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int)
RETURNS int
LANGUAGE plpgsql 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$;
Được sử dụng trong chức năng chính:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql 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$;
-
Giá này rẻ hơn một chút nếu hầu hết các cuộc gọi chỉ cần
SELECT
, vì khối đắt hơn vớiINSERT
chứaEXCEPTION
mệnh đề hiếm khi được nhập. Truy vấn cũng đơn giản hơn. -
FOR SHARE
không thể thực hiện được ở đây (không được phép trongUNION
truy vấn). -
LIMIT 1
sẽ không cần thiết (thử nghiệm trong trang 9.4). Postgres dẫn xuấtLIMIT 1
từINTO _tag_id
và chỉ thực thi cho đến khi hàng đầu tiên được tìm thấy.