Медленное время запроса на поиск сходства с индексами pg_trgm

9

Мы добавили два индекса pg_trgm в таблицу, чтобы включить нечеткий поиск по адресу электронной почты или имени, так как нам нужно найти пользователей по имени или адресам электронной почты, которые были написаны с ошибками при регистрации (например, «@ gmail.con»). ANALYZEбыл запущен после создания индекса.

Однако выполнение ранжированного поиска по любому из этих индексов в большинстве случаев происходит очень медленно. то есть с увеличенным временем ожидания, запрос может вернуться через 60 секунд, в очень редких случаях - до 15 секунд, но обычно запросы будут задерживаться.

pg_trgm.similarity_thresholdзначение по умолчанию 0.3, но увеличение 0.8этого значения, похоже, не имеет значения.

Эта конкретная таблица содержит более 25 миллионов строк и постоянно запрашивается, обновляется и вставляется в нее (среднее время для каждой из них составляет менее 2 мс). Это PostgreSQL 9.6.6, работающий на экземпляре RDS db.m4.large с общим хранилищем SSD и более или менее стандартными параметрами. Расширение pg_trgm - версия 1.3.

Запросы:

  • SELECT *
    FROM users
    WHERE email % 'chris@example.com'
    ORDER BY email <-> 'chris@example.com' LIMIT 10;
  • SELECT *
    FROM users
    WHERE (first_name || ' ' || last_name) % 'chris orr'
    ORDER BY (first_name || ' ' || last_name) <-> 'chris orr' LIMIT 10;

Эти запросы не нужно выполнять очень часто (десятки раз в день), но они должны основываться на текущем состоянии таблицы и в идеале возвращаться в течение 10 секунд.


Схема:

=> \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

(Я знаю , что мы , вероятно , следует добавить unaccent()к users_search_name_idxи запрос имени ...)


Объясняет:

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

Поиск по электронной почте, скорее всего, будет более длительным, чем поиск по имени, но, вероятно, это связано с тем, что адреса электронной почты очень похожи (например, множество адресов @ gmail.com).

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email % 'chris@example.com' ORDER BY email <-> 'chris@example.com' 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 % 'chris@example.com'::text)
        Order By: ((email)::text <-> 'chris@example.com'::text)
        Buffers: shared hit=83 read=428918
Planning time: 0.100 ms
Execution time: 62181.186 ms

Что может быть причиной медленного времени запроса? Что-то делать с количеством читаемых буферов? Я не смог найти много информации об оптимизации этого конкретного типа запросов, и запросы в любом случае очень похожи на те, что описаны в документации по pg_trgm.

Это то, что мы могли бы оптимизировать или лучше реализовать в Postgres, или мы хотели бы, чтобы что-то вроде Elasticsearch лучше подходило для этого конкретного варианта использования?

Кристофер Орр
источник
1
Ваша версия pg_trgmхотя бы 1.3? Вы можете проверить с "\ dx" в psql.
Джанес
Удалось ли вам воспроизвести любой топ-запрос, ранжированный с помощью <->оператора, который использует индекс?
Colin 't Hart
Предполагая, что настройки по умолчанию, я бы играл с порогом сходства. Таким образом, вы можете получить меньший результат, так что, возможно, общая стоимость может снизиться ...
Михал Заборовски
@jjanes Спасибо за указатель. Да, версия 1.3.
Кристофер Орр
1
@ MichałZaborowski Как уже упоминалось в вопросе, я пытался это сделать, но, к сожалению, улучшения не увидел.
Кристофер Орр

Ответы:

1

Вы могли бы быть в состоянии получить лучшую производительность, gin_trgm_opsа не gist_trgm_ops. Что лучше, это довольно непредсказуемо, оно чувствительно к распределению текстовых шаблонов и длины в ваших данных и в ваших условиях запроса. Вы просто должны попробовать это и посмотреть, как это работает для вас. Одна вещь заключается в том, что метод GIN будет весьма чувствительным pg_trgm.similarity_threshold, в отличие от метода GiST. Это также будет зависеть от того, какая у вас версия pg_trgm. Если вы начали со старой версии PostgreSQL, но обновили ее pg_upgrade, возможно, у вас не самая последняя версия. Планировщик не лучше предсказывает, какой тип индекса лучше, чем мы. Таким образом, чтобы проверить это, вы не можете просто создать оба, вы должны отбросить другой, чтобы заставить планировщика использовать тот, который вы хотите.

В конкретном случае столбца электронной почты может быть лучше разделить их на имя пользователя и домен, а затем запросить аналогичное имя пользователя с точным доменом и наоборот. Тогда крайняя распространенность основных поставщиков облачной электронной почты с меньшей вероятностью будет загрязнять индексы триграммами, которые добавляют мало информации.

Наконец, каков вариант использования для этого? Знание, почему вам нужно выполнить эти запросы, может привести к лучшим предложениям. В частности, зачем вам нужно выполнять поиск сходства по электронным письмам, если они были проверены на предмет доставки и отправки нужному человеку? Возможно, вы могли бы построить частичный индекс только для тех электронных писем, которые еще не были проверены?

jjanes
источник
Спасибо за информацию. Вместо этого я попробую индекс GIN и поиграю с порогом. Кроме того, да, это отличный момент, если иметь частичный индекс для непроверенных адресов. Однако даже для проверенных адресов электронной почты могут потребоваться нечеткие совпадения (например, люди, забывающие точки в адресах @ gmail.com), но это, вероятно, тот случай, когда, как вы упоминаете, есть отдельная таблица с нормализованными столбцами локальной части и домена.
Кристофер Орр