Schnellere Abfrage mit Mustervergleich für mehrere Textfelder

9

Ich habe einen Postgres-Tisch mit mehr als 20 Millionen Tupeln:

first_name | last_name | email
-------------------------------------------
bat        | man       | batman@wayne.com
arya       | vidal     | foo@email.com
max        | joe       | bar@email.com

So filtern Sie die von mir verwendeten Datensätze:

SELECT *
  FROM people
WHERE (first_name || '' || last_name) ILIKE '%bat%man%' OR 
    first_name ILIKE '%bat%man%'  OR  
    last_name ILIKE '%bat%man%'   OR
    email ILIKE '%bat%man%'
    LIMIT 25 OFFSET 0

Selbst bei Indizes dauert die Suche fast eine Minute , um Ergebnisse zurückzugeben.
Es gibt Indizes für (first_name || '' || last_name), first_name, last_nameund email.

Was kann ich tun, um die Leistung dieser Abfrage zu verbessern?

Sieger
quelle
1
Kennen Sie varchar_pattern_ops ?
Sahap Asci
1
Ich nehme an, Sie verwenden gewöhnliche B-Tree-Indizes? Diese funktionieren nicht ilike '%something'(mit führendem Platzhalter). Sie müssen andere Indizes hinzufügen.
Colin 't Hart
1
Welche Version von PostgreSQL verwenden Sie? Bitte bearbeiten Sie dies (und die Tatsache, dass Sie B-Tree-Indizes verwenden) in Ihrer Frage, damit wir diese Kommentare löschen können.
Colin 't Hart
1
... und (wie immer) Ihre tatsächliche Tabellendefinition, die die tatsächlichen Datentypen und Einschränkungen anzeigt: CREATE TABLESkript oder was Sie \d peoplein psql erhalten.
Erwin Brandstetter
3
@SahapAsci: Die *_pattern_opsOperatorklassen sind nützlich für links verankerte Muster, aber für den demonstrierten Anwendungsfall machtlos.
Erwin Brandstetter

Antworten:

14

Verwenden Sie für Ihre Art der Mustererkennung am besten einen Trigrammindex. Lesen Sie dies zuerst:

Ich gehe davon aus, dass Ihr Ausdruck einen Tippfehler enthält (first_name || '' || last_name), der bei einer leeren Zeichenfolge keinen Sinn ergibt, und Sie möchten wirklich (first_name || ' ' || last_name)- mit einem Leerzeichen.

Unter der Annahme, dass jede Spalte NULL sein kann, benötigen Sie eine NULL-sichere Verkettung. Die einfache Lösung lautet concat_ws():

Diese Funktion ist jedoch nicht IMMUTABLE(Erklärung in der verknüpften Antwort), sodass Sie sie nicht direkt in einem Indexausdruck verwenden können. Sie könnten einen IMMUTABLEFunktions-Wrapper verwenden:

CREATE OR REPLACE FUNCTION f_immutable_concat_ws(s text, t1 text, t2 text)
  RETURNS text AS
$func$
SELECT concat_ws(s, t1, t2)
$func$ LANGUAGE sql IMMUTABLE;

Der Wrapper kann sein, IMMUTABLEweil er nur textParameter akzeptiert.
In beiden Fällen ist dies ausführlicher, hat jedoch weniger internen Aufwand und ist erheblich schneller:

CREATE OR REPLACE FUNCTION f_immutable_concat_ws1(s text, t1 text, t2 text)
  RETURNS text AS
$func$
SELECT CASE
         WHEN t1 IS NULL THEN t2
         WHEN t2 IS NULL THEN t1
         ELSE t1 || s || t2
       END
$func$ LANGUAGE sql IMMUTABLE;

Oder mit fest codiertem Leerzeichen:

CREATE OR REPLACE FUNCTION f_concat_space(t1 text, t2 text)
  RETURNS text AS
$func$
SELECT CASE
         WHEN t1 IS NULL THEN t2
         WHEN t2 IS NULL THEN t1
         ELSE t1 || ' ' || t2
       END
$func$ LANGUAGE sql IMMUTABLE;

Basierend auf dieser Funktion schlage ich vor:

CREATE INDEX people_special_gin_trgm_idx ON people
USING gin (f_concat_space(first_name, last_name) gin_trgm_ops, email gin_trgm_ops);

Ich habe emailals zweite Indexspalte für mehrere Überlegungen hinzugefügt .

Das Erstellen des Index dauert eine Weile für 20 Millionen Zeilen, am besten nicht während des Topladens oder möglicherweise bei Verwendung CREATE INDEX CONCURRENTLY .... Ein GIN-Index ist erheblich größer als ein einfacher Btree-Index und in der Wartung auch teurer. Stellen Sie sicher, dass Sie die neueste Version von Postgres ausführen. In den letzten Versionen wurden die GIN-Indizes erheblich verbessert.

Dann sollte Ihre leicht angepasste und vereinfachte Abfrage schnell und korrekt sein :

SELECT *
FROM   people
WHERE  f_concat_space(first_name, last_name) ILIKE '%bat%man%' OR
       email ILIKE '%bat%man%'
LIMIT  25;

Sie benötigen nur den einen Index für diese Abfrage.

Grundlagen für den Mustervergleich:

Erwin Brandstetter
quelle
1
Heh. Schnell (ish) und richtig. Infix-Suchen werden immer etwas langsam sein. Was concat_ws, Sie wissen , warum es nicht immutable? Ich würde vorschlagen, dies vor dem Einpacken zu überprüfen, da Sie sonst möglicherweise Indexinhalte erhalten, die nicht mit den aktuellen Ausdrucksergebnissen für dieselbe Eingabe übereinstimmen, was sich wie eine Indexbeschädigung verhält.
Craig Ringer
2
@CraigRinger: Ich habe nachgeforscht, ob ich concat_ws()nicht bin, IMMUTABLEund der oben genannten Antwort eine Erklärung hinzugefügt . Die Erklärung ist ziemlich einfach und mein Wrapper ist sicher, weil er nur textParameter akzeptiert.
Erwin Brandstetter
Danke, freut mich sehr. Ich habe es nur angehoben, weil ich die Leute wirklich nicht dazu ermutigen möchte, unveränderliche Flaggen mit Wrappern zu umgehen!
Craig Ringer
@CraigRinger: Da hast du vollkommen recht. Ich habe schon früher Verbrechen in dieser dunklen Gasse gesehen: stackoverflow.com/a/11007216/939860 . Ich habe oben einen Hinweis hinzugefügt, warum der Wrapper sicher ist.
Erwin Brandstetter