在多个文本字段上使用模式匹配进行更快的查询

Vic*_*tor 12 postgresql performance index full-text-search postgresql-performance

我有一个包含超过 20M 元组的 Postgres 表:

first_name | last_name | email
-------------------------------------------
bat        | man       | batman@wayne.com
arya       | vidal     | foo@email.com
max        | joe       | bar@email.com
Run Code Online (Sandbox Code Playgroud)

要过滤我正在使用的记录:

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
Run Code Online (Sandbox Code Playgroud)

即使使用索引,搜索也需要将近一分钟才能返回结果。
有索引的(first_name || '' || last_name)first_namelast_nameemail

我可以做些什么来提高此查询的性能?

Erw*_*ter 17

对于您的模式匹配,您最好使用三元组索引。先读这个:

我假设您的 expression 中存在拼写错误(first_name || '' || last_name),这对于空字符串毫无意义,而您确实想要(first_name || ' ' || last_name)- 带有空格字符。

假设任一列都可以为 NULL,您将需要 NULL 安全连接,简单的解决方案是concat_ws()

但该函数不是IMMUTABLE(链接答案中的解释),因此您不能直接在索引表达式中使用它。您可以使用IMMUTABLE函数包装器:

CREATE OR REPLACE FUNCTION f_immutable_concat_ws(s text, t1 text, t2 text)
  RETURNS text
  LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
'SELECT concat_ws(s, t1, t2)';
Run Code Online (Sandbox Code Playgroud)

包装器可以是IMMUTABLE因为它只需要text参数。(但不要试图在调用中强制使用文本表示不是不可变的数据类型!)

无论哪种方式,这都更冗长,但内部开销更少,而且速度要快得多:

CREATE OR REPLACE FUNCTION f_immutable_concat_ws(s text, t1 text, t2 text)
  RETURNS text
  LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
$func$
SELECT CASE
         WHEN t1 IS NULL THEN t2
         WHEN t2 IS NULL THEN t1
         ELSE t1 || s || t2
       END
$func$;
Run Code Online (Sandbox Code Playgroud)

或者,使用硬编码的空格字符:

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

使其PARALLEL SAFE(在 Postgres 9.6 或更高版本中)不妨碍并行性。(而且因为它符合条件!)

基于最后一个函数的索引,我建议:

CREATE INDEX people_gin_trgm_idx ON people
USING gin (f_concat_space(first_name, last_name) gin_trgm_ops, email gin_trgm_ops);
Run Code Online (Sandbox Code Playgroud)

我添加email为第二个索引列,因为您似乎同时检查两者。

为 20M 行创建索引需要一段时间,最好不要在最高负载期间,或者可能使用CREATE INDEX CONCURRENTLY .... GIN 索引可能比普通 B 树索引大得多,维护成本也更高。一定要运行最新版本的 Postgres,最近版本对 GIN 索引进行了重大改进。

那么你稍微调整和简化的查询应该是快速和正确的

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

您只需要此查询的一个索引。

有关的:

  • @CraigRinger:我研究了`concat_ws()` 不是`IMMUTABLE` 并添加了解释[上面引用的答案](http://stackoverflow.com/a/12320369/939860)。解释相当简单,我的包装器是安全的,因为它只接受 `text` 参数。 (2认同)