CTE đệ quy để tìm sên độc đáo


7

Tôi có một bảng bài viết nơi tôi muốn con sên là duy nhất.

CREATE TABLE article (
   title char(50) NOT NULL,
   slug  char(50) NOT NULL
);

Khi người dùng nhập một tiêu đề News on Apple, vd , tôi muốn kiểm tra cơ sở dữ liệu để xem có tồn tại một con sên tương ứng không news-on-apple. Nếu có, tôi sẽ có một giá trị số cho đến khi tôi tìm thấy một giá trị duy nhất, vd news-on-apple-1. Điều đó có thể đạt được với truy vấn CTE đệ quy thay vì thực hiện đệ quy trong ORM của tôi không. Có một số sân bóng tốt mà tôi nên dừng đệ quy và lỗi ra. Tôi có thể tưởng tượng mọi người sử dụng cùng một tiêu đề 1000 lần sẽ dẫn đến 1000 truy vấn chỉ để tạo 1 bài viết.

Có thể sự hiểu biết của tôi về CTE đệ quy là không chính xác và không có cách nào tốt hơn để tìm một con sên độc đáo. Xin đề nghị bất kỳ lựa chọn thay thế.

Câu trả lời:


7

Trước hết, bạn không muốn sử dụng char(50). Sử dụng varchar(50)hoặc chỉ text. Đọc thêm:

Giả sử các quy tắc sau:

  • Sên cơ bản không bao giờ kết thúc với một dấu gạch ngang.
  • Sên trùng lặp được thêm vào một dấu gạch ngang và một số liên tiếp ( -123).

Lưu ý rằng tất cả các phương pháp sau phải tuân theo điều kiện cuộc đua : các hoạt động đồng thời có thể xác định cùng tên "miễn phí" cho sên tiếp theo.
Để chống lại điều đó, bạn có thể áp đặt một ràng buộc KHÔNG GIỚI HẠN slugvà sẵn sàng lặp lại một CHỨNG CHỈ khi vi phạm khóa trùng lặp hoặc bạn lấy ra một khóa ghi trên bàn khi bắt đầu giao dịch.

Nếu bạn dán hậu tố vào tên sên cơ bản bằng dấu gạch ngang và cho phép các sên cơ bản kết thúc bằng các số riêng biệt, thì thông số kỹ thuật là một chút mơ hồ (xem bình luận). Tôi đề nghị một dấu phân cách duy nhất của sự lựa chọn của bạn thay vào đó (không được phép).

RCTE hiệu quả

WITH RECURSIVE
  input AS (SELECT 'news-on-apple'::text AS slug)  -- input basic slug here once
, cte   AS (
   SELECT slug || '-' AS slug  -- append '-' once, if basic slug exists
        , 1 as suffix          -- start with suffix 1
   FROM   article
   JOIN   input USING (slug)

   UNION ALL
   SELECT c.slug, c.suffix + 1  -- increment by 1 ...
   FROM   cte     c
   JOIN   article a ON a.slug = c.slug || c.suffix  -- ... if slug-n already exists
   )
(
SELECT slug || suffix AS slug
FROM   cte
ORDER  BY suffix DESC  -- pick the last (free) one
LIMIT  1
)  -- parentheses required
UNION  ALL  -- if the basic slug wasn't taken, fall back to that
SELECT slug FROM input
LIMIT  1;

Hiệu suất tốt hơn mà không cần rCTE

Nếu bạn lo lắng về hàng ngàn con sên cạnh tranh cho cùng một con sên hoặc thường muốn tối ưu hóa hiệu suất, tôi sẽ xem xét một cách tiếp cận khác, nhanh hơn.

WITH input AS (SELECT 'news-on-apple'::text  AS slug
                    , 'news-on-apple-'::text AS slug1)  -- input basic slug here
SELECT i.slug
FROM   input        i
LEFT   JOIN article a USING (slug)
WHERE  a.slug IS NULL  -- doesn't exist yet.

UNION ALL
(  -- parentheses required
SELECT i.slug1 || COALESCE(right(a.slug, length(i.slug1) * -1)::int + 1, 1)
FROM   input        i
LEFT   JOIN article a ON a.slug LIKE (i.slug1 || '%')  -- match up to last "-"
                     AND right(a.slug, length(i.slug1) * -1) ~ '^\d+$' -- suffix numbers only
ORDER  BY right(a.slug, length(i.slug1) * -1)::int DESC
)
LIMIT  1;
  • Nếu sên cơ bản là không được nêu ra, thứ hai đắt hơn SELECTkhông bao giờ thực hiện - tương tự như trên, nhưng nhiều hơn nữa quan trọng ở đây. Kiểm tra với EXPLAIN ANALYZE, Postgres là thông minh theo cách đó với LIMITcác truy vấn. Liên quan:

  • Kiểm tra riêng chuỗi đầu và hậu tố, vì vậy LIKEbiểu thức có thể sử dụng chỉ mục btree cơ bản với text_pattern_opslike

    CREATE INDEX article_slug_idx ON article (slug text_pattern_ops);

    Giải thích chi tiết:

  • Chuyển đổi hậu tố thành số nguyên trước khi bạn áp dụng max(). Các số trong biểu diễn văn bản không hoạt động.

Tối ưu hóa hiệu suất

Để có được sự tối ưu, hãy xem xét việc lưu trữ hậu tố tách biệt với sên cơ bản và nối sên khi cần: concat_ws('-' , slug, suffix::text) AS slug

CREATE TABLE article (
   article_id serial PRIMARY KEY
 , title text NOT NULL
 , slug  text NOT NULL
 , suffix int
);

Truy vấn cho một con sên mới sau đó trở thành:

SELECT slug
    || COALESCE((
          SELECT '-'::text || (max(suffix) + 1)::text
          FROM   article a
          WHERE  a.slug = i.slug), '') As slug
FROM  (SELECT 'news-on-apple'::text AS slug) i  -- input basic slug here

Hỗ trợ lý tưởng với một chỉ số duy nhất trên (slug, suffix).

Truy vấn danh sách sên

Trong bất kỳ phiên bản nào của Postgres, bạn có thể cung cấp các hàng trong một VALUESbiểu thức.

SELECT *
FROM   article
JOIN  (
   VALUES
     ('slug-foo'::text, 1)
     ('slug-bar',7)
   ) u(slug,suffix) USING (slug,suffix);

Bạn cũng có thể sử dụng INvới một tập hợp các biểu thức loại hàng ngắn hơn:

SELECT *
FROM   article
WHERE (slug,suffix) IN (('slug-foo', 1), ('slug-bar',7));

Chi tiết theo câu hỏi liên quan này (như bình luận bên dưới):

Đối với danh sách dài, JOINđến một VALUESbiểu hiện thường nhanh hơn.

Trong Postgres 9.4 (phát hành ngày hôm nay!) Bạn cũng có thể sử dụng biến thể mới của unnest()nhiều mảng không nhất quán song song.

Cho một mảng các sên cơ bản và một mảng các hậu tố tương ứng (theo nhận xét):

SELECT *
FROM   article
JOIN   unnest('{slug-foo,slug-bar}'::text[]
            , '{1,7}'::int[]) AS u(slug,suffix) USING (slug,suffix);

3
Cảm ơn bạn là một cách đánh giá thấp cho một câu trả lời đầy đủ thông tin và bằng văn bản như vậy! Tôi có một số câu hỏi tiếp theo. Đối với phương pháp thứ 2, nếu người dùng thiết lập 3 danh hiệu sau đây theo thứ tự, bmw, bmw 745, bmw, các maxchức năng sẽ dẫn đến sên con thứ ba bmw-746, mặc dù chúng tôi sẽ lý tưởng muốn bmw-1ở đây. Sử dụng countlà một tùy chọn nhưng điều đó cũng sẽ tạo ra sên bất ngờ trong một số trường hợp nhất định. Cụ thể khi có xung đột giữa các tiêu đề kết thúc bằng số.
dùng4150760

@ user4150760: Sự không rõ ràng giữa tên sên cơ bản và tên sên cộng với hậu tố được thêm vào được tích hợp vào thông số kỹ thuật của bạn. Đó không phải là thứ mà giải pháp của tôi thêm vào. Bạn phải định hướng chính mình. Ví dụ: sử dụng một ký tự không được phép trước hậu tố.
Erwin Brandstetter

Trong phương pháp thứ 3, cách hiệu quả để truy vấn danh sách các cặp sên & hậu tố là gì? Làm điều này: WHERE slug IN (<slug_list>) AND suffix IN (<suffix_list>)sẽ dẫn đến sản phẩm cartesian của tất cả các sên & hậu tố có trong danh sách. Tuy nhiên tôi muốn truy vấn được giới hạn trong các cặp sên & hậu tố tương ứng.
dùng4150760

@ user4150760: Câu hỏi hay. Có giải pháp thanh lịch. Nên là một câu hỏi khác. Nhưng dù sao tôi cũng đã thêm một câu trả lời.
Erwin Brandstetter

1
Cảm ơn bạn đã cập nhật. Tôi đã thêm một câu hỏi mới nhưng đã xóa nó sau khi @ypercube nhận xét rằng một câu hỏi tương tự đã được trả lời: dba.stackexchange.com/questions/86373/ Lỗi
user4150760
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.