You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PostgreSQL 12.11大表词相似度查询性能下降,求优化方案

PostgreSQL Trigram查询优化:2000万行Word表多文档查询变慢问题

环境与表结构

使用PostgreSQL v12.11(无法升级),表结构如下:

document (
  id uuid primary key
)

page (
  id uuid primary key,
  document_id uuid not null references document
)

word (
  id uuid primary key,
  page_id uuid not null references page,
  text text not null
)

现有索引与查询语句

已创建索引

CREATE INDEX word_page_id_idx ON word (page_id);
CREATE INDEX word_text_idx ON word USING GIN (text gin_trgm_ops);
CREATE INDEX page_document_id ON page (document_id);

查询语句

SELECT w.*, word_similarity(:searchString, w.text) similarity
    FROM document d
             INNER JOIN page p ON p.document_id = d.id
             INNER JOIN word w ON w.page_id = p.id
    WHERE d.id IN (:documentIds)
    AND :searchString <% w.text
    ORDER BY similarity DESC, w.text;

问题现象

初期查询性能良好,但当word表达到2000万行后,查询速度明显变慢,尤其是多文档查询场景。例如查询18个文档时,执行计划显示扫描了185700条word记录,但这些文档实际仅关联约9000个词汇。尝试AI建议的(page_id, text)联合GIN索引,未生效。

执行计划分析(EXPLAIN ANALYZE 中文翻译)

+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 查询计划                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Sort  (成本=11351.77..11351.79 行数=8 宽度=63) (实际时间=3825.241..3825.247 行数=74 循环=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
|  排序键: (word_similarity('05.06.2025'::text, w.text)) DESC, w.text                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
|  排序方法: quicksort  内存: 35kB                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
|  ->  Nested Loop  (成本=248.89..11351.65 行数=8 宽度=63) (实际时间=130.456..3825.106 行数=74 循环=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
|        ->  Nested Loop  (成本=0.83..138.23 行数=45 宽度=16) (实际时间=0.517..4.050 行数=28 循环=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
|              ->  Index Only Scan using document_pkey on document d  (成本=0.41..31.98 行数=18 宽度=16) (实际时间=0.010..0.217 行数=18 循环=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
|                    索引条件: (id = ANY ('{470d35da-a65e-4eb5-aae1-1ce9334ff617,44f05ab8-94f7-4e90-9008-a224b0f2c458,c9b8161d-2975-4902-884f-11c658960ca0,9315c0ec-b460-4044-b4a3-79b77f40faea,b6e0a7ef-37c3-4540-aeb9-424567e3c51f,0b0adbbe-7d70-484d-a589-f5952dd9c4ae,5f51a47e-11cc-4bf4-a3f6-d1452262d82f,099dfc29-1803-4a87-8820-891697b26047,3e6a8d9c-a40b-4980-a49e-8285eee4dedc,4078b105-aff4-478d-97ae-2b785b1bdd08,30b7a7f6-b67a-4f0c-8cfb-e686a01b8e89,7265f939-7f47-4264-8a72-43f71312ba74,3e03b9b5-2c24-4d7a-b063-5bec34ad6e1e,23f69244-81a9-4dbc-984d-21bfd2bd0147,39837f1c-491e-4b43-9c86-713c7b16ec7a,5ba1dce8-97e0-4e22-8685-e018c4dbbf31,15e34009-1ed7-470a-9b16-34fc416595eb,9fb7604b-eafd-49be-b8cf-d9f2c5d663bf}'::uuid[]))|
|                    堆获取: 0                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
|              ->  Index Scan using page_document_id on page p  (成本=0.42..5.86 行数=4 宽度=32) (实际时间=0.203..0.209 行数=2 循环=18)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
|                    索引条件: (document_id = d.id)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
|        ->  Bitmap Heap Scan on word w  (成本=248.06..249.18 行数=1 宽度=59) (实际时间=136.266..136.449 行数=3 循环=28)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
|              重检查条件: (page_id = p.id)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
|              过滤: ('05.06.2025'::text <% text)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
|              被过滤移除的行数: 3                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
|              堆块: exact=50                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
|              ->  BitmapAnd  (成本=248.06..248.06 行数=1 宽度=0) (实际时间=135.254..135.254 行数=0 循环=28)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
|                    ->  Bitmap Index Scan on word_page_id_idx  (成本=0.00..9.16 行数=761 宽度=0) (实际时间=0.021..0.021 行数=251 循环=28)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
|                          索引条件: (page_id = p.id)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
|                    ->  Bitmap Index Scan on word_text_idx  (成本=0.00..233.26 行数=21568 宽度=0) (实际时间=135.229..135.229 行数=185700 循环=28)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
|                          索引条件: (text %> '05.06.2025'::text)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
|规划时间: 26.187 ms                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
|执行时间: 3825.320 ms                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

核心问题分析

  1. 全表Trigram扫描重复执行:执行计划中,每个匹配的page(共28个)都会触发一次BitmapAnd操作——先获取该page下的所有word,再扫描全表的trigram索引获取匹配记录,最后取交集。28次循环导致总扫描量达到28×185700=520万+,这是性能瓶颈的核心。
  2. 联合GIN索引无效原因:PostgreSQL 12的GIN索引不支持普通列(如page_id)与trigram列的组合,仅能存储复杂类型或特定操作符类的列,因此AI建议的联合索引无法生效。

优化方案

方案1:创建GiST多列联合索引

GiST索引支持普通列与trigram列的混合索引,可直接针对page_id和text做联合过滤:

CREATE INDEX word_page_text_gist_idx ON word USING GIST (page_id, text gist_trgm_ops);

该索引能让查询直接定位到指定page下的trigram匹配记录,避免重复扫描全表trigram索引。

方案2:冗余存储document_id到word表(最优解)

若允许表结构变更,在word表中冗余存储所属文档ID,彻底简化查询逻辑:

-- 添加字段并更新数据
ALTER TABLE word ADD COLUMN document_id uuid;
UPDATE word w SET document_id = p.document_id FROM page p WHERE w.page_id = p.id;
ALTER TABLE word ADD CONSTRAINT word_document_id_fkey FOREIGN KEY (document_id) REFERENCES document(id);

-- 创建联合GiST索引
CREATE INDEX word_document_text_gist_idx ON word USING GIST (document_id, text gist_trgm_ops);

优化后的查询语句可直接从word表过滤文档ID和trigram匹配,无需关联document和page表:

SELECT w.*, word_similarity(:searchString, w.text) similarity
FROM word w
WHERE w.document_id IN (:documentIds)
AND :searchString <% w.text
ORDER BY similarity DESC, w.text;

方案3:调整查询逻辑与计划

将查询顺序反转,先通过trigram索引获取匹配的word,再关联文档过滤:

SELECT w.*, word_similarity(:searchString, w.text) similarity
FROM word w
JOIN page p ON w.page_id = p.id
JOIN document d ON p.document_id = d.id
WHERE d.id IN (:documentIds)
AND :searchString <% w.text
ORDER BY similarity DESC, w.text;

可临时关闭嵌套循环,强制优化器选择哈希连接或合并连接:

SET enable_nestloop = off;

方案4:调整Trigram匹配阈值

若业务允许,通过调整pg_trgm.similarity_threshold减少匹配记录数:

SET pg_trgm.similarity_threshold = 0.3; -- 根据业务需求调整,默认值为0.3

验证建议

  1. 优先尝试方案3,无需修改表结构和索引,快速验证效果;
  2. 若方案3性能提升不足,再创建方案1的GiST联合索引;
  3. 若业务允许表结构变更,方案2是长期最优解,能彻底消除重复扫描问题。

内容的提问来源于stack exchange,提问作者T_01

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 07:55:53