Langsame Abfragezeiten für Ähnlichkeitssuchen mit pg_trgm-Indizes

9

Wir haben einer Tabelle zwei pg_trgm-Indizes hinzugefügt, um die Fuzzy-Suche nach E-Mail-Adresse oder Name zu ermöglichen, da wir Benutzer nach Namen oder E-Mail-Adressen suchen müssen, die bei der Anmeldung falsch geschrieben wurden (z. B. "@ gmail.con"). ANALYZEwurde nach der Indexerstellung ausgeführt.

In den allermeisten Fällen ist die Suche nach einem dieser Indizes jedoch nur sehr langsam. Bei einem erhöhten Zeitlimit wird eine Abfrage möglicherweise innerhalb von 60 Sekunden zurückgegeben, in sehr seltenen Fällen bis zu 15 Sekunden. In der Regel tritt jedoch eine Zeitüberschreitung bei Abfragen auf.

pg_trgm.similarity_thresholdist der Standardwert von 0.3, aber dies zu 0.8erhöhen schien keinen Unterschied zu machen.

Diese bestimmte Tabelle hat über 25 Millionen Zeilen und wird ständig abgefragt, aktualisiert und eingefügt (die durchschnittliche Zeit für jede Tabelle liegt unter 2 ms). Das Setup ist PostgreSQL 9.6.6, das auf einer RDS-Instanz db.m4.large mit allgemeinem SSD-Speicher und mehr oder weniger Standardparametern ausgeführt wird. Die Erweiterung pg_trgm ist Version 1.3.

Fragen:

  • SELECT *
    FROM users
    WHERE email % '[email protected]'
    ORDER BY email <-> '[email protected]' LIMIT 10;
  • SELECT *
    FROM users
    WHERE (first_name || ' ' || last_name) % 'chris orr'
    ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;

Diese Abfragen müssen nicht sehr oft ausgeführt werden (dutzende Male am Tag), sollten jedoch auf dem aktuellen Tabellenstatus basieren und idealerweise innerhalb von etwa 10 Sekunden zurückgegeben werden.


Schema:

=> \d+ users
                                          Table "public.users"
          Column   |            Type             | Collation | Nullable | Default | Storage  
-------------------+-----------------------------+-----------+----------+---------+----------
 id                | uuid                        |           | not null |         | plain    
 email             | citext                      |           | not null |         | extended 
 email_is_verified | boolean                     |           | not null |         | plain    
 first_name        | text                        |           | not null |         | extended 
 last_name         | text                        |           | not null |         | extended 
 created_at        | timestamp without time zone |           |          | now()   | plain    
 updated_at        | timestamp without time zone |           |          | now()   | plain    
                  | boolean                     |           | not null | false   | plain    
                  | character varying(60)       |           |          |         | extended 
                  | character varying(6)        |           |          |         | extended 
                  | character varying(6)        |           |          |         | extended 
                  | boolean                     |           |          |         | plain    
Indexes:
  "users_pkey" PRIMARY KEY, btree (id)
  "users_email_key" UNIQUE, btree (email)
  "users_search_email_idx" gist (email gist_trgm_ops)
  "users_search_name_idx" gist (((first_name || ' '::text) || last_name) gist_trgm_ops)
  "users_updated_at_idx" btree (updated_at)
Triggers:
  update_users BEFORE UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE update_modified_column()
Options: autovacuum_analyze_scale_factor=0.01, autovacuum_vacuum_scale_factor=0.05

(Ich bin mir bewusst , dass wir wahrscheinlich auch hinzufügen unaccent()zu users_search_name_idxund die Namensabfrage ...)


Erklärt:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE (first_name || ' ' || last_name) % 'chris orr' ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;::

Limit  (cost=0.42..40.28 rows=10 width=152) (actual time=58671.973..58676.193 rows=10 loops=1)
  Buffers: shared hit=66227 read=231821
  ->  Index Scan using users_search_name_idx on users  (cost=0.42..100264.13 rows=25153 width=152) (actual time=58671.970..58676.180 rows=10 loops=1)
        Index Cond: (((first_name || ' '::text) || last_name) % 'chris orr'::text)
        Order By: (((first_name || ' '::text) || last_name) <-> 'chris orr'::text"
        Buffers: shared hit=66227 read=231821
Planning time: 0.125 ms
Execution time: 58676.265 ms

Bei der E-Mail-Suche tritt mit größerer Wahrscheinlichkeit eine Zeitüberschreitung auf als bei der Namenssuche. Dies liegt jedoch vermutlich daran, dass die E-Mail-Adressen so ähnlich sind (z. B. viele @ gmail.com-Adressen).

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email % '[email protected]' ORDER BY email <-> '[email protected]' LIMIT 10;::

Limit  (cost=0.42..40.43 rows=10 width=152) (actual time=58851.719..62181.128 rows=10 loops=1)
  Buffers: shared hit=83 read=428918
  ->  Index Scan using users_search_email_idx on users  (cost=0.42..100646.36 rows=25153 width=152) (actual time=58851.716..62181.113 rows=10 loops=1)
        Index Cond: ((email)::text % '[email protected]'::text)
        Order By: ((email)::text <-> '[email protected]'::text)
        Buffers: shared hit=83 read=428918
Planning time: 0.100 ms
Execution time: 62181.186 ms

Was könnte ein Grund für die langsamen Abfragezeiten sein? Hat das etwas mit der Anzahl der gelesenen Puffer zu tun? Ich konnte nicht viele Informationen zur Optimierung dieser bestimmten Art von Abfrage finden, und die Abfragen sind denen in der Dokumentation zu pg_trgm ohnehin sehr ähnlich.

Ist dies etwas, das wir in Postgres optimieren oder besser implementieren könnten, oder würde ein Blick auf Elasticsearch besser zu diesem speziellen Anwendungsfall passen?

Christopher Orr
quelle
1
Ist Ihre Version von pg_trgmmindestens 1.3? Sie können mit "\ dx" einchecken psql.
Jjanes
Konnten Sie eine Top-n-Abfrage reproduzieren, die mit dem <->Operator bewertet wurde, der einen Index verwendet?
Colin 't Hart
Unter der Annahme, dass die Standardeinstellungen sind, würde ich mit dem Ähnlichkeitsschwellenwert spielen. Auf diese Weise können Sie ein kleineres Ergebnis erzielen, sodass die Gesamtkosten möglicherweise sinken können ...
Michał Zaborowski
@jjanes Danke für den Zeiger. Ja, die Version ist 1.3.
Christopher Orr
1
@ MichałZaborowski Wie in der Frage erwähnt, habe ich das versucht, aber leider keine Verbesserung festgestellt.
Christopher Orr

Antworten:

1

Möglicherweise können Sie eine bessere Leistung erzielen gin_trgm_opsals mit gist_trgm_ops. Was besser ist, ist ziemlich unvorhersehbar, es reagiert empfindlich auf die Verteilung von Textmustern und -längen in Ihren Daten und in Ihren Abfragebegriffen. Sie müssen es so ziemlich nur ausprobieren und sehen, wie es für Sie funktioniert. Eine Sache ist, dass die GIN-Methode im pg_trgm.similarity_thresholdGegensatz zur GiST-Methode sehr empfindlich ist . Es hängt auch davon ab, welche Version von pg_trgm Sie haben. Wenn Sie mit einer älteren Version von PostgreSQL begonnen, diese jedoch aktualisiert haben pg_upgrade, verfügen Sie möglicherweise nicht über die neueste Version. Der Planer kann nicht besser vorhersagen, welcher Indextyp überlegen ist als wir. Um es zu testen, können Sie nicht einfach beide erstellen, sondern müssen das andere löschen, um den Planer zu zwingen, das gewünschte zu verwenden.

Im speziellen Fall der E-Mail-Spalte ist es möglicherweise besser, sie in Benutzername und Domain aufzuteilen und dann nach einem ähnlichen Benutzernamen mit genauer Domain abzufragen und umgekehrt. Dann ist es weniger wahrscheinlich, dass die extreme Verbreitung der großen Cloud-E-Mail-Anbieter die Indizes mit Trigrammen verschmutzt, die nur wenige Informationen hinzufügen.

Was ist der Anwendungsfall dafür? Wenn Sie wissen, warum Sie diese Abfragen ausführen müssen, können Sie bessere Vorschläge machen. Warum sollten Sie insbesondere eine Ähnlichkeitssuche für E-Mails durchführen, wenn diese als zustellbar bestätigt wurden und an die richtige Person weitergeleitet werden? Vielleicht könnten Sie einen Teilindex nur für die Teilmenge der E-Mails erstellen, die noch nicht überprüft wurden?

jjanes
quelle
Danke für die Information. Ich werde stattdessen einen GIN-Index ausprobieren und mit dem Schwellenwert spielen. Ja, das ist auch ein guter Punkt, wenn es einen Teilindex für nicht überprüfte Adressen gibt. Selbst für verifizierte E-Mail-Adressen können unscharfe Übereinstimmungen erforderlich sein (z. B. Personen, die die Punkte in @ gmail.com-Adressen vergessen). Dies ist jedoch wahrscheinlich der Fall, wenn Sie, wie bereits erwähnt, eine separate Tabelle mit normalisierten Spalten für lokale Teile und Domänen haben.
Christopher Orr