递归 CTE 以找到独特的 slug

use*_*760 7 postgresql database-design cte recursive

我有一个文章表,我希望 slug 是独一无二的。

CREATE TABLE article (
   title char(50) NOT NULL,
   slug  char(50) NOT NULL
);
Run Code Online (Sandbox Code Playgroud)

当用户输入标题时,例如News on Apple,我想检查数据库以查看是否存在相应的 slug,例如news-on-apple。如果是这样,我将给一个数值添加后缀,直到找到一个唯一的值,例如news-on-apple-1. 可以通过递归 CTE 查询而不是在我的 ORM 中进行递归来实现。是否有一个很好的大概数字,我应该停止递归和出错。我可以想象人们使用相同的标题 1000 次,这将导致 1000 次查询只是为了创建 1 篇文章。

我对递归 CTE 的理解可能是不正确的,并且没有更好的方法来找到唯一的 slug。请提出任何替代方案。

Erw*_*ter 7

首先,你希望使用char(50)。使用varchar(50)或只是text. 阅读更多:

假设以下规则:

  • 基本的slug永远不会以破折号结束。
  • 重复的 slug 以破折号和序列号 ( -123)为后缀。

请注意,以下所有方法都受到竞争条件的影响:并发操作可能会为下一个 slug 标识相同的“空闲”名称。
为了防止它,您可以施加一个 UNIQUE 约束slug并准备在重复键违规时重复 INSERT,或者您在事务开始时在表上取出写锁。

如果您用破折号将后缀粘贴到基本 slug 名称并允许基本 slug 以单独的数字结尾,则规范有点含糊不清(请参阅评论)。我建议您选择一个唯一的分隔符(否则是不允许的)。

高效的 rCTE

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;
Run Code Online (Sandbox Code Playgroud)

没有 rCTE 的更好性能

如果您担心数以千计的 slug 争夺同一个 slug 或通常想要优化性能,我会考虑一种不同的、更快的方法。

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;
Run Code Online (Sandbox Code Playgroud)

优化性能

要获得最佳效果,请考虑将后缀与基本 slug 分开存储,并根据需要连接 slug: 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
);
Run Code Online (Sandbox Code Playgroud)

然后对新 slug 的查询变为:

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
Run Code Online (Sandbox Code Playgroud)

理想情况下由 上的唯一索引支持(slug, suffix)

查询 slug 列表

任何版本的 Postgres 中,您都可以在VALUES表达式中提供行。

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

您还可以使用IN一组较短的行类型表达式:

SELECT *
FROM   article
WHERE (slug,suffix) IN (('slug-foo', 1), ('slug-bar',7));
Run Code Online (Sandbox Code Playgroud)

此相关问题下的详细信息(如下所述):

对于长列表,JOINtoVALUES表达式通常更快。

在 Postgres 9.4(今天发布!)中,您还可以使用 的新变体unnest()来并行取消嵌套多个数组。

给定一组基本 slug 和相应的后缀数组(根据评论):

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

  • 谢谢你对这样一个内容丰富且写得很好的答案是轻描淡写!我有一些后续问题。对于第二种方法,如果用户按顺序设置以下 3 个标题,`bmw`、`bmw 745`、`bmw`,`max` 函数将导致第三个 slug 为 `bmw-746`,尽管我们理想情况下想要‘bmw-1’在这里。使用 `count` 是一种选择,但在某些情况下也会出现意外的 slug。特别是当以数字结尾的标题之间存在冲突时。 (3认同)