Musterabgleich mit LIKE, SIMILAR TO oder regulären Ausdrücken in PostgreSQL

94

Ich musste eine einfache Abfrage schreiben, in der ich nach Namen von Personen suche, die mit einem B oder einem D beginnen:

SELECT s.name 
FROM spelers s 
WHERE s.name LIKE 'B%' OR s.name LIKE 'D%'
ORDER BY 1

Ich habe mich gefragt, ob es eine Möglichkeit gibt, dies neu zu schreiben, um performanter zu werden. Also kann ich das vermeiden orund / oder like?

Lucas Kauffman
quelle
Warum versuchst du umzuschreiben? Performance? Ordentlichkeit? Ist s.nameindiziert?
Martin Smith
Ich möchte aus Performancegründen schreiben, ab Name wird nicht indiziert.
Lucas Kauffman
8
nameWenn Sie ohne führende Platzhalter suchen und keine zusätzlichen Spalten auswählen, kann ein Index hier hilfreich sein, wenn Sie Wert auf Leistung legen.
Martin Smith

Antworten:

161

Ihre Anfrage ist so ziemlich das Optimum. Die Syntax wird nicht viel kürzer, die Abfrage wird nicht viel schneller:

SELECT name
FROM   spelers
WHERE  name LIKE 'B%' OR name LIKE 'D%'
ORDER  BY 1;

Wenn Sie die Syntax wirklich verkürzen möchten , verwenden Sie einen regulären Ausdruck mit Verzweigungen :

...
WHERE  name ~ '^(B|D).*'

Oder etwas schneller mit einer Charakterklasse :

...
WHERE  name ~ '^[BD].*'

Ein schneller Test ohne Index liefert SIMILAR TOfür mich in beiden Fällen schnellere Ergebnisse als für .
Mit einem geeigneten B-Tree-Index LIKEgewinnt dieses Rennen um Größenordnungen.

Lesen Sie die Grundlagen zum Pattern Matching im Handbuch .

Index für überlegene Leistung

Wenn Sie sich mit der Leistung befassen, erstellen Sie einen Index für größere Tabellen:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

Beschleunigt diese Art der Abfrage um Größenordnungen. Besondere Überlegungen gelten für die länderspezifische Sortierreihenfolge. Weitere Informationen zu Operator-Klassen finden Sie im Handbuch . Wenn Sie das Standardgebietsschema "C" verwenden (die meisten Benutzer tun dies nicht), reicht ein einfacher Index (mit Standardoperatorklasse) aus.

Ein solcher Index eignet sich nur für links verankerte Muster (Abgleich ab dem Anfang der Zeichenfolge).

SIMILAR TOAuch reguläre Ausdrücke mit einfachen links verankerten Ausdrücken können diesen Index verwenden. Aber nicht mit Zweigen (B|D)oder Zeichenklassen [BD](zumindest in meinen Tests unter PostgreSQL 9.0).

Trigramm-Übereinstimmungen oder Textsuche verwenden spezielle GIN- oder GiST-Indizes.

Übersicht der Mustervergleichsoperatoren

  • LIKE( ~~) ist einfach und schnell, aber in seinen Fähigkeiten begrenzt.
    ILIKE( ~~*) die case insensitive Variante.
    pg_trgm erweitert die Indexunterstützung für beide.

  • ~ (Übereinstimmung mit regulären Ausdrücken) ist leistungsfähig, aber komplexer und kann für mehr als nur grundlegende Ausdrücke langsam sein.

  • SIMILAR TOist einfach sinnlos . Eine eigenartige Mischform von LIKEund regulären Ausdrücken. Ich benutze es nie. Siehe unten.

  • % ist der "Ähnlichkeits" -Operator, der vom Zusatzmodul bereitgestellt wirdpg_trgm. Siehe unten.

  • @@ist der Textsuchoperator. Siehe unten.

pg_trgm - Trigrammabgleich

Ab PostgreSQL 9.1 können Sie die Erweiterung vereinfachen pg_trgm, um Indexunterstützung für any LIKE / ILIKEpattern (und einfache Regexp-Muster mit ~) mithilfe eines GIN- oder GiST-Index bereitzustellen.

Details, Beispiel und Links:

pg_trgmbietet auch diese Operatoren :

  • % - der Operator "Ähnlichkeit"
  • <%(Kommutator %>:) - Der Operator "word_similarity" in Postgres 9.6 oder höher
  • <<%(Kommutator %>>:) - Der Operator "strict_word_similarity" in Postgres 11 oder höher

Textsuche

Ist eine spezielle Art des Mustervergleichs mit separaten Infrastruktur- und Indextypen. Es verwendet Wörterbücher und Stemming und ist ein großartiges Werkzeug, um Wörter in Dokumenten zu finden, insbesondere für natürliche Sprachen.

Der Präfixabgleich wird ebenfalls unterstützt:

Sowie die Phrasensuche seit Postgres 9.6:

Beachten Sie die Einführung im Handbuch und die Übersicht der Bediener und Funktionen .

Zusätzliche Tools für den Fuzzy-String-Abgleich

Das Zusatzmodul fuzzystrmatch bietet einige weitere Optionen, die Leistung ist jedoch generell schlechter als die oben genannten.

Insbesondere können verschiedene Implementierungen der levenshtein()Funktion hilfreich sein.

Warum sind reguläre Ausdrücke ( ~) immer schneller als SIMILAR TO?

Die Antwort ist einfach. SIMILAR TOAusdrücke werden intern in reguläre Ausdrücke umgeschrieben. Daher gibt es für jeden SIMILAR TOAusdruck mindestens einen schnelleren regulären Ausdruck (der den Aufwand für das Neuschreiben des Ausdrucks erspart). Es gibt keinen Leistungsgewinn bei der Verwendung SIMILAR TO jemals .

Und einfache Ausdrücke, die mit LIKE( ~~) gemacht werden können, sind LIKEsowieso schneller .

SIMILAR TOwird nur in PostgreSQL unterstützt, weil es zu frühen Entwürfen des SQL-Standards geführt hat. Sie haben es immer noch nicht losgeworden. Aber es gibt Pläne, es zu entfernen und stattdessen reguläre Ausdrücke einzuschließen - oder wie ich gehört habe.

EXPLAIN ANALYZEoffenbart es. Probieren Sie es einfach mit einem Tisch aus!

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name SIMILAR TO 'B%';

Enthüllt:

...  
Seq Scan on spelers  (cost= ...  
  Filter: (name ~ '^(?:B.*)$'::text)

SIMILAR TOwurde mit einem regulären Ausdruck ( ~) umgeschrieben .

Ultimative Leistung für diesen speziellen Fall

Aber EXPLAIN ANALYZEverrät mehr. Versuchen Sie es mit dem oben genannten Index:

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name ~ '^B.*;

Enthüllt:

...
 ->  Bitmap Heap Scan on spelers  (cost= ...
       Filter: (name ~ '^B.*'::text)
        ->  Bitmap Index Scan on spelers_name_text_pattern_ops_idx (cost= ...
              Index Cond: ((prod ~>=~ 'B'::text) AND (prod ~<~ 'C'::text))

Intern mit einem Index, der nicht locale-aware (wird text_pattern_opsoder mit locale C) einfach links verankerte Ausdrücke sind mit diesen Textmustern Operatoren neu geschrieben: ~>=~, ~<=~, ~>~, ~<~. Dies ist der Fall für ~, ~~oder SIMILAR TOgleichermaßen.

Gleiches gilt für Indizes zu varcharTypen mit varchar_pattern_opsoder charmit bpchar_pattern_ops.

Auf die ursprüngliche Frage angewendet ist dies der schnellste Weg :

SELECT name
FROM   spelers  
WHERE  name ~>=~ 'B' AND name ~<~ 'C'
    OR name ~>=~ 'D' AND name ~<~ 'E'
ORDER  BY 1;

Sollten Sie einmal nach benachbarten Initialen suchen , können Sie dies natürlich weiter vereinfachen:

WHERE  name ~>=~ 'B' AND name ~<~ 'D'   -- strings starting with B or C

Der Gewinn gegenüber dem normalen Gebrauch von ~oder ~~ist winzig. Wenn Leistung nicht Ihre oberste Anforderung ist, sollten Sie sich einfach an die Standardoperatoren halten und zu dem gelangen, was Sie bereits in der Frage haben.

Erwin Brandstetter
quelle
Das OP hat keinen Index für den Namen, aber wissen Sie zufällig, ob die ursprüngliche Abfrage zwei Bereichssuchen und similareinen Scan umfassen würde, wenn dies der Fall wäre ?
Martin Smith
2
@MartinSmith: Ein schneller Test mit EXPLAIN ANALYZE2 Bitmap-Index-Scans. Mehrere Bitmap-Index-Scans können relativ schnell kombiniert werden.
Erwin Brandstetter
Vielen Dank. So gäbe es jede milage sein mit dem Ersetzen der ORmit UNION ALLoder ersetzt name LIKE 'B%'mit name >= 'B' AND name <'C'in Postgres?
Martin Smith
1
@MartinSmith: Ich UNIONwerde es nicht tun , aber wenn Sie die Bereiche in einer WHEREKlausel kombinieren , wird die Abfrage beschleunigt. Ich habe meiner Antwort mehr hinzugefügt. Natürlich müssen Sie Ihr Gebietsschema berücksichtigen. Die Suche nach Gebietsschemas ist immer langsamer.
Erwin Brandstetter
2
@a_horse_with_no_name: Ich erwarte nicht. Die neuen Funktionen von pg_tgrm mit GIN-Indizes sind eine Wohltat für die allgemeine Textsuche. Eine am Start verankerte Suche geht schon schneller.
Erwin Brandstetter
11

Wie wäre es mit dem Hinzufügen einer Spalte zur Tabelle? Abhängig von Ihren tatsächlichen Anforderungen:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

PostgreSQL unterstützt keine berechneten Spalten in Basistabellen nach SQL Server , die neue Spalte kann jedoch über einen Trigger verwaltet werden. Offensichtlich würde diese neue Spalte indiziert werden.

Alternativ würde ein Index für einen Ausdruck dasselbe ergeben, das billiger ist. Z.B:

CREATE INDEX spelers_name_initial_idx ON spelers (left(name, 1)); 

Abfragen, die dem Ausdruck in ihren Bedingungen entsprechen, können diesen Index verwenden.

Auf diese Weise wird der Leistungstreffer beim Erstellen oder Ändern der Daten erfasst, sodass er möglicherweise nur für Umgebungen mit geringer Aktivität geeignet ist (dh viel weniger Schreibvorgänge als Lesevorgänge).

eines Tages, wenn
quelle
8

Du könntest es versuchen

SELECT s.name
FROM   spelers s
WHERE  s.name SIMILAR TO '(B|D)%' 
ORDER  BY s.name

Ich habe keine Ahnung, ob die oben genannten oder Ihre ursprünglichen Ausdrücke in Postgres wiedergegeben werden können.

Wenn Sie den vorgeschlagenen Index erstellen, wären Sie auch interessiert zu erfahren, wie sich dieser mit den anderen Optionen vergleicht.

SELECT name
FROM   spelers
WHERE  name >= 'B' AND name < 'C'
UNION ALL
SELECT name
FROM   spelers
WHERE  name >= 'D' AND name < 'E'
ORDER  BY name
Martin Smith
quelle
1
Es hat funktioniert und ich habe einen Preis von 1,19 bekommen, wo ich 1,25 hatte. Vielen Dank !
Lucas Kauffman
2

Was ich in der Vergangenheit getan habe, ist angesichts eines ähnlichen Leistungsproblems, das ASCII-Zeichen des letzten Buchstabens zu erhöhen und ein ZWISCHEN auszuführen. Sie erhalten dann die beste Leistung für eine Teilmenge der LIKE-Funktionalität. Natürlich funktioniert es nur in bestimmten Situationen, aber bei extrem großen Datenmengen, in denen Sie beispielsweise nach einem Namen suchen, wird die Leistung von miserabel auf akzeptabel erhöht.

Mel Padden
quelle
2

Sehr alte Frage, aber ich habe eine andere schnelle Lösung für dieses Problem gefunden:

SELECT s.name 
FROM spelers s 
WHERE ascii(s.name) in (ascii('B'),ascii('D'))
ORDER BY 1

Da die Funktion ascii () nur das erste Zeichen der Zeichenkette betrachtet.

Sole021
quelle
1
Verwendet dies einen Index für (name)?
Ypercubeᵀᴹ
2

Zur Überprüfung der Initialen verwende ich oft das Casting to "char"(mit den doppelten Anführungszeichen). Es ist nicht tragbar, aber sehr schnell. Intern wird der Text einfach entfremdet und das erste Zeichen zurückgegeben, und "char" -Vergleichsoperationen sind sehr schnell, da der Typ eine feste Länge von 1 Byte hat:

SELECT s.name 
FROM spelers s 
WHERE s.name::"char" =ANY( ARRAY[ "char" 'B', 'D' ] )
ORDER BY 1

Beachten Sie, dass das Casting in "char"schneller ist als die ascii()Slution von @ Sole021, aber nicht UTF8-kompatibel ist (oder eine andere Codierung). Es wird lediglich das erste Byte zurückgegeben. Daher sollte es nur in Fällen verwendet werden, in denen der Vergleich mit der einfachen alten 7 erfolgt -Bit ASCII-Zeichen.

Ziggy Crueltyfree Zeitgeister
quelle
1

Es gibt zwei Methoden, die noch nicht erwähnt wurden, um mit solchen Fällen umzugehen:

  1. partieller (oder partitionierter - wenn für den gesamten Bereich manuell erstellt) Index - am nützlichsten, wenn nur eine Teilmenge der Daten erforderlich ist (z. B. während einer Wartung oder vorübergehend für einige Berichte):

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
  2. Partitionierung der Tabelle selbst (unter Verwendung des ersten Zeichens als Partitionierungsschlüssel) - Diese Technik ist besonders in PostgreSQL 10+ (weniger schmerzhafte Partitionierung) und 11+ (Partitionsbereinigung während der Abfrageausführung) erwägenswert.

Wenn die Daten in einer Tabelle sortiert sind, kann der BRIN-Index (über dem ersten Zeichen) verwendet werden.

Tomasz Pala
quelle
-4

Wahrscheinlich schneller, um einen Einzelzeichenvergleich durchzuführen:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'
user2653985
quelle
1
Nicht wirklich. column LIKE 'B%'Dies ist effizienter als die Verwendung der Teilzeichenfolgenfunktion für die Spalte.
Ypercubeᵀᴹ