Rekursiver CTE, um eine einzigartige Schnecke zu finden

7

Ich habe eine Artikeltabelle, in der die Schnecke eindeutig sein soll.

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

Wenn der Benutzer einen Titel eingibt, z. B. News on Applemöchte ich die Datenbank überprüfen, um festzustellen, ob ein entsprechender Slug vorhanden ist, z news-on-apple. Wenn dies der Fall ist, füge ich einen numerischen Wert hinzu, bis ich einen eindeutigen finde, z news-on-apple-1. Kann dies mit einer rekursiven CTE-Abfrage erreicht werden, anstatt eine Rekursion in meinem ORM durchzuführen? Gibt es eine gute Baseballnummer, bei der ich aufhören sollte zu rekursieren und Fehler machen sollte? Ich kann mir vorstellen, dass Leute 1000 Mal denselben Titel verwenden, was zu 1000 Abfragen führen würde, nur um 1 Artikel zu erstellen.

Es ist möglich, dass mein Verständnis von rekursivem CTE falsch ist und es keinen besseren Weg gibt, eine eindeutige Schnecke zu finden. Bitte schlagen Sie Alternativen vor.

user4150760
quelle

Antworten:

7

Zunächst einmal möchten Sie nicht verwenden char(50). Verwenden Sie varchar(50)oder nur text. Weiterlesen:

Unter Annahme der folgenden Regeln:

  • Grundlegende Schnecken enden nie mit einem Strich.
  • Doppelte Slugs werden mit einem Bindestrich und einer fortlaufenden Nummer ( -123) versehen.

Beachten Sie, dass für alle folgenden Methoden Rennbedingungen gelten : Gleichzeitige Vorgänge identifizieren möglicherweise denselben "freien" Namen für den nächsten Slug.
Um sich dagegen zu verteidigen, können Sie eine EINZIGARTIGE Einschränkung auferlegen slugund darauf vorbereitet sein, ein INSERT bei doppelter Schlüsselverletzung zu wiederholen oder zu Beginn der Transaktion eine Schreibsperre für die Tabelle aufzuheben.

Wenn Sie das Suffix mit einem Bindestrich auf den Namen des Basis-Slugs kleben und zulassen, dass Basic-Slugs in separaten Zahlen enden, ist die Spezifikation ein wenig mehrdeutig (siehe Kommentare). Ich schlage stattdessen ein eindeutiges Trennzeichen Ihrer Wahl vor (das ansonsten nicht zulässig ist).

Effizientes 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;

Bessere Leistung ohne rCTE

Wenn Sie sich Sorgen machen, dass Tausende von Slugs um denselben Slug konkurrieren oder generell die Leistung optimieren möchten, würde ich einen anderen, schnelleren Ansatz in Betracht ziehen.

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;
  • Wenn der Grund Slug noch nicht genommen wird, desto teurer zweite SELECTwird nie ausgeführt - das gleiche wie oben, aber viel wichtiger ist hier. Überprüfen Sie mit EXPLAIN ANALYZE, Postgres ist auf diese Weise bei LIMITAbfragen klug . Verbunden:

  • LIKESuchen Sie separat nach der führenden Zeichenfolge und dem Suffix, damit der Ausdruck einen grundlegenden btree-Index mit text_pattern_opslike verwenden kann

    CREATE INDEX article_slug_idx ON article (slug text_pattern_ops);

    Ausführliche Erklärung:

  • Konvertieren Sie das Suffix in eine Ganzzahl, bevor Sie es anwenden max(). Zahlen in der Textdarstellung funktionieren nicht.

Leistung optimieren

Um das Optimum zu erzielen, sollten Sie das vom Basis-Slug getrennte Suffix speichern und den Slug nach Bedarf verketten: 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
);

Die Abfrage nach einem neuen Slug lautet dann:

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

Idealerweise mit einem eindeutigen Index unterstützt (slug, suffix).

Abfrage nach Liste der Schnecken

In jeder Version von Postgres können Sie Zeilen in einem VALUESAusdruck bereitstellen .

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

Sie können auch INeine Reihe von Zeilenausdrücken verwenden, die kürzer sind:

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

Details unter dieser verwandten Frage (wie unten kommentiert):

Bei langen Listen ist der Ausdruck JOINzu einem VALUESAusdruck normalerweise schneller.

In Postgres 9.4 (heute veröffentlicht!) unnest()Können Sie auch die neue Variante verwenden , um mehrere Arrays parallel zu entfernen.

Bei einem Array von Basis-Slugs und einem entsprechenden Array von Suffixen (gemäß Kommentar):

SELECT *
FROM   article
JOIN   unnest('{slug-foo,slug-bar}'::text[]
            , '{1,7}'::int[]) AS u(slug,suffix) USING (slug,suffix);
Erwin Brandstetter
quelle
3
Vielen Dank ist eine Untertreibung für eine so informative und gut geschriebene Antwort! Ich habe einige Anschlussfragen. Für das zweite Verfahren, wenn ein Benutzer des folgenden 3 Titel setzt , um, bmw, bmw 745, bmw, die maxfunktionieren in dritten Slug Wesen führen bmw-746, obwohl wir im Idealfall wollen würden bmw-1hier. Die Verwendung countist eine Option, die jedoch in bestimmten Fällen auch zu unerwarteten Schnecken führen würde. Insbesondere, wenn es einen Konflikt zwischen Titeln gibt, die mit Zahlen enden.
user4150760
@ user4150760: Die Mehrdeutigkeit zwischen dem Basis-Slug-Namen und dem Slug-Namen sowie dem angehängten Suffix ist in Ihre Spezifikationen integriert. Es ist nicht etwas, was meine Lösung hinzugefügt hat. Sie müssen sich eindeutig machen. Beispiel: Verwenden Sie ein Zeichen, das vor dem Suffix nicht zulässig ist.
Erwin Brandstetter
Was ist bei der dritten Methode eine effiziente Methode, um eine Liste von Slug & Suffix-Paaren abzufragen? Wenn Sie dies tun, erhalten Sie ein WHERE slug IN (<slug_list>) AND suffix IN (<suffix_list>)kartesisches Produkt aller in der Liste enthaltenen Slugs & Suffixe. Ich möchte jedoch, dass die Abfrage auf entsprechende Paare von Slugs & Suffixen beschränkt wird.
user4150760
@ user4150760: Gute Frage. Es gibt elegante Lösungen. Sollte eine andere Frage sein. Aber ich habe trotzdem eine andere Antwort hinzugefügt.
Erwin Brandstetter
1
Danke für das Update. Ich habe eine neue Frage hinzugefügt, sie aber später gelöscht, nachdem @ypercube kommentiert hat, dass eine ähnliche Frage beantwortet wurde: dba.stackexchange.com/questions/86373/…
user4150760