web-dev-qa-db-ja.com

ユニークなスラグを見つけるための再帰的なCTE

スラグを一意にしたい記事の表があります。

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

ユーザーがタイトルを入力したとき。 News on Apple、データベースをチェックして、対応するスラッグが存在するかどうかを確認します。 news-on-Apple。一致する場合は、一意の数値が見つかるまで数値のサフィックスを付けます。 news-on-Apple-1。 ORMで再帰を行う代わりに、再帰的なCTEクエリでそれを実現できますか?再帰を停止してエラーを出すべき良い球場番号はありますか?同じタイトルを1000回使用していて、1つの記事を作成するだけで1000のクエリが発生することを想像できます。

再帰CTEについての私の理解が正しくなく、ユニークなスラッグを見つけるためのより良い方法がない可能性があります。代替案があれば提案してください。

7
user4150760

まず、あなたはnot使いたい char(50)varchar(50)または単にtextを使用します。続きを読む:

次のルールを想定しています。

  • 基本 slugs ダッシュで終わることはありません。
  • 重複するスラッグには、ダッシュと連続番号(_-123_)がサフィックスとして付加されます。

以下のすべてのメソッドは競合状態の影響を受けることに注意してください。同時操作は次のスラグに対して同じ「フリー」名を識別する可能性があります。
これを防ぐには、slugにUNIQUE制約を課し、重複キー違反時にINSERTを繰り返す準備をするか、またはテーブルの開始時にテーブルの書き込みロックを解除しますトランザクション。

接尾辞をダッシュ​​で基本的なスラッグの名前に接着し、基本的なスラッグが別の数字で終わることを許可する場合、仕様はごくわずかですあいまい(コメントを参照)。代わりに、独自のdelimiterをお勧めします(それ以外の場合は許可されません)。

効率的な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;
_

RCTEを使用しない場合のパフォーマンスの向上

何千ものスラグが同じスラグで競合することを心配している場合、または一般的にパフォーマンスを最適化したい場合は、別の高速なアプローチを検討します。

_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;
_
  • 基本的なスラッグがまだ取得されていない場合、より高価な2番目のSELECT実行されない-上記と同じですが、ここでははるかに重要です。 _EXPLAIN ANALYZE_で確認してください。PostgresはLIMITクエリでそのようにスマートです。関連:

  • 先頭の文字列とサフィックスを個別にチェックして、LIKE式が_text_pattern_ops_のような基本的なbtreeインデックスを使用できるようにします

    _CREATE INDEX article_slug_idx ON article (slug text_pattern_ops);
    _

    詳細な説明:

  • max()を適用する前に、サフィックスを整数に変換してください。テキスト表現の数値は機能しません。

パフォーマンスを最適化

最適な状態にするには、基本的なスラッグから分離されたサフィックスを保存し、必要に応じてスラッグを連結することを検討してください: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
);
_

新しいスラッグのクエリは次のようになります。

_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
_

理想的には、_(slug, suffix)_の一意のインデックスでサポートされます。

ナメクジのリストのクエリ

Postgresのany versionでは、VALUES式で行を提供できます。

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

INを行タイプの式のセットで使用することもできます。

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

この関連質問の詳細(以下でコメント):

長いリストの場合、JOINからVALUESへの式の方が通常は高速です。

Postgresでは9.4(本日リリース!) unnest() の新しいバリアントを使用して、複数の配列を並列にネスト解除することもできます。

基本的なスラッグの配列と対応するサフィックスの配列(コメントどおり)を指定します。

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