PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

CHỌN hoặc CHÈN trong một hàm có khuynh hướng gặp phải các điều kiện về chủng tộc không?

Đâ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 SELECTINSERT 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ặc UPDATE trên bảng tag . 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ặc name . 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ới INSERT chứa EXCEPTION 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 trong UNION truy vấn).

  • LIMIT 1 sẽ không cần thiết (thử nghiệm trong trang 9.4). Postgres dẫn xuất LIMIT 1 từ INTO _tag_id và chỉ thực thi cho đến khi hàng đầu tiên được tìm thấy.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Đối phó với các truy vấn chậm với PostgreSQL

  2. Cách đạt được tính khả dụng cao của PostgreSQL với pgBouncer

  3. Mảng ban đầu trong chức năng tổng hợp mảng đa chiều

  4. Hàm tương tự trong Postgres với pg_trgm

  5. Cách tách các giao dịch chỉ đọc và đọc ghi với JPA và Hibernate