PostgreSQL pg_trgm
pg_trgm 是 PostgreSQL 的一个 扩展模块,用于:
基于“字符相似度”进行高效的模糊匹配
核心能力:
- LIKE ‘%xxx%’
- ILIKE ‘%xxx%’
- 相似度搜索(模糊推荐)
- 排序(按相似度)
📌 它是 PostgreSQL 里处理模糊字符串查询的王牌
Trigram = 连续的 3 个字符
例如:
"融资计划"
会被拆成:
" 融", " 融资", "融资计", "资计划", "计划 "
pg_trgm 通过比较 trigram 集合的“交集比例”来判断相似度
SELECT * FROM pg_extension WHERE extname = 'pg_trgm';
CREATE EXTENSION pg_trgm;
📌 只需执行一次(数据库级)
CREATE INDEX idx_title_trgm
ON telegraph
USING gin (title gin_trgm_ops);
📌 90% 场景推荐 GIN
SELECT id
FROM telegraph
WHERE title LIKE '%融资%';
或:
SELECT id
FROM telegraph
WHERE title ILIKE '%融资%';
Bitmap Heap Scan
-> Bitmap Index Scan on idx_title_trgm
similarity(text, text) -- 返回 0 ~ 1
示例:
SELECT
title,
similarity(title, '融资计划') AS score
FROM telegraph
ORDER BY score DESC
LIMIT 10;
title % '融资计划'
等价于:
similarity(title, '融资计划') > pg_trgm.similarity_threshold
默认阈值:
SHOW pg_trgm.similarity_threshold;
-- 0.3
SELECT title
FROM telegraph
WHERE title % '融资计划'
ORDER BY similarity(title, '融资计划') DESC;
SET pg_trgm.similarity_threshold = 0.2;
| 类型 | 特点 | 是否推荐 |
|---|---|---|
| GIN | 查询快、索引大 | ✅ 推荐 |
| GiST | 索引小、支持排序 | ⚠️ 少用 |
GiST 示例(支持 <-> 排序)
CREATE INDEX idx_title_trgm_gist
ON telegraph
USING gist (title gist_trgm_ops);
✅ 会走索引:
LIKE '%xxx%'
ILIKE '%xxx%'
title % 'xxx'
title <-> 'xxx'
❌ 不会走索引:
LOWER(title) LIKE '%xxx%' -- ❌
title::text LIKE '%xxx%' -- ❌
SUBSTRING(title, ...) -- ❌
📌 字段上不能包函数
ALTER TABLE telegraph
ALTER COLUMN title SET STATISTICS 1000;
ANALYZE telegraph;
否则 planner 会严重低估匹配行数。
3个 中文字符 以上,才能利用索引。
- 表 < 5 万行
- 返回行数比例不低
👉 PostgreSQL 故意用 Seq Scan
ALTER TABLE telegraph
ALTER COLUMN title TYPE text COLLATE "C";
然后重建索引。
| 对比 | pg_trgm | 全文索引 |
|---|---|---|
| LIKE ‘%xxx%’ | ✅ 强 | ❌ 弱 |
| 中文 | ✅ 不分词 | ❌ 需分词 |
| 语义搜索 | ❌ | ✅ |
| 模糊推荐 | ✅ | ❌ |
📌 标题 / URL / 编号 / 短文本 -> pg_trgm
原因 99% 是:
- 统计信息不足
- planner 认为 Seq Scan 更便宜
- collation 成本过高
索引直接失效。
结合之前的 telegraph 表,推荐最终形态:
ALTER TABLE telegraph
ALTER COLUMN title SET STATISTICS 1000;
CREATE INDEX idx_telegraph_title_trgm
ON telegraph USING gin (title gin_trgm_ops);
查询:
SELECT id
FROM telegraph
WHERE title LIKE '%融资额%';
pg_trgm = PostgreSQL 里处理模糊字符串查询的终极武器
- LIKE ‘%xxx%’ 必备
- 中文场景必备
- 标题 / URL / 编号 / 模糊推荐首选
pg_trgm 在中文场景下:
- 1 个汉字:❌ 基本不会走索引
- 2 个汉字:⚠️ 可能走,也可能不走
- ≥ 3 个汉字:✅ 才是稳定、可靠走索引
👉 所以大家经验总结为一句话:
“中文 LIKE,至少 3 个字,pg_trgm 才靠谱”
pg_trgm 的最小匹配单位是 3 个字符(trigram)
假设你查询:
WHERE title LIKE '%融资%'
🔹 “融资” = 2 个汉字
生成的 trigram 极少:
" 融资", "融资 "
- 👉 trigram 数量太少
- 👉 选择性太低
- 👉 planner 判断:用索引不划算
WHERE title LIKE '%融资计划%'
trigram 会变成:
" 融资", "融资计", "资计划", "计划 "
- 👉 trigram 数量明显增加
- 👉 匹配精度提高
- 👉 索引过滤能力足够强
英文不同:
LIKE '%AI%'
- 字符短
- trigram 分布更离散
- 字符集小
📌 中文是多字节字符
-> trigram 的“信息密度”反而更低
重点理解这一句:
不是 pg_trgm 不支持,而是 PostgreSQL 觉得 Seq Scan 更便宜。
你可以强制验证:
SET enable_seqscan = off;
EXPLAIN ANALYZE
SELECT id FROM telegraph WHERE title LIKE '%融资%';
👉 很可能会看到 GIN Index Scan
但 ❌ 不代表线上该这么做
PostgreSQL 官方文档里并没有写:
“中文至少 3 个字”
但写了这句(含义非常重要):
The longer the search string, the more selective the trigram index is.
📌 对中文来说,“更长”≈ 至少 3 个字
| 搜索长度 | 推荐方案 |
|---|---|
| 1 字 | Seq Scan(正常) |
| 2 字 | 允许慢,或提示用户 |
| ≥3 字 | pg_trgm |
if len(keyword) < 3:
raise ValueError("关键词至少 3 个字")
📌 很多新闻 / 搜索系统都这么做
ALTER TABLE telegraph
ALTER COLUMN title SET STATISTICS 1000;
ANALYZE telegraph;
⚠️ 只能“缓解”,不能从根本解决
ALTER TABLE telegraph
ALTER COLUMN title TYPE text COLLATE "C";
但 不能改变 trigram 的物理限制
方案对比(说实话)
| 方案 | 现实评价 |
|---|---|
| pg_trgm | ❌ 不适合 |
| BTree | ❌ 不支持 LIKE ‘%x%’ |
| 全文索引 | ❌ 中文需分词 |
| ES / OpenSearch | ✅ 正解 |
| 前缀匹配(LIKE ‘融资%’) | ⚠️ 可用 |
📌 结论:数据库不是万能搜索引擎
pg_trgm 的 LIKE 在中文场景下:
- ✅ ≥ 3 个字:生产级
- ⚠️ 2 个字:不稳定
- ❌ 1 个字:不要指望索引
EXPLAIN ANALYSE SELECT id FROM telegraph WHERE title LIKE '%融资%';
-- Seq Scan on telegraph (cost=0.00..4187.25 rows=222 width=8) (actual time=0.045..16.284 rows=255 loops=1)
-- Filter: (title ~~ '%融资%'::text)
-- Rows Removed by Filter: 45928
-- Planning Time: 0.425 ms
-- Execution Time: 16.327 ms
EXPLAIN ANALYSE SELECT id FROM telegraph WHERE title LIKE '%融资额%';
-- Bitmap Heap Scan on telegraph (cost=6.87..11.31 rows=4 width=8) (actual time=0.029..0.033 rows=2 loops=1)
-- Recheck Cond: (title ~~ '%融资额%'::text)
-- Heap Blocks: exact=2
-- -> Bitmap Index Scan on idx_telegraph_title_trgm (cost=0.00..6.87 rows=4 width=0) (actual time=0.016..0.017 rows=2 loops=1)
-- Index Cond: (title ~~ '%融资额%'::text)
-- Planning Time: 0.433 ms
-- Execution Time: 0.057 ms
这两条 EXPLAIN ANALYZE 正好是 pg_trgm 中文 LIKE 的“标准教科书级对比案例”。
你现在看到的现象是 完全正确、完全符合原理的,不是配置问题、不是索引问题。
pg_trgm 在中文 LIKE 场景下:
- ‘%融资%’(2 个字)-> Seq Scan 是“理性选择”
- ‘%融资额%’(3 个字)-> 索引开始“有价值”
你这两条执行计划 正是 PostgreSQL 在“理性工作”。
EXPLAIN ANALYSE
SELECT id FROM telegraph WHERE title LIKE '%融资%';
执行计划
Seq Scan on telegraph
cost=0.00..4187.25
actual time=0.045..16.284
rows=255
Rows Removed by Filter: 45928
Execution Time: 16.327 ms
PostgreSQL 的“内心判断”
- 1️⃣ 表总行数 ≈ 46183 行
- 2️⃣ 匹配行数 = 255 行(0.55%)
- 3️⃣ 查询字符串 = 2 个汉字 -> trigram 极少
- 4️⃣ GIN 索引需要:
- 扫索引
- 构建 bitmap
- 回表
📌 结论:
👉 “顺序扫一遍 4.6 万行,比折腾 GIN 更快”
所以 Seq Scan 是最优解
EXPLAIN ANALYSE
SELECT id FROM telegraph WHERE title LIKE '%融资额%';
执行计划
Bitmap Heap Scan
-> Bitmap Index Scan on idx_telegraph_title_trgm
Execution Time: 0.057 ms
PostgreSQL 的“内心判断”
- 1️⃣ trigram 数量明显增加
- 2️⃣ 匹配行数 只有 2 行
- 3️⃣ 索引选择性极高
- 4️⃣ bitmap 只访问 2 个 heap block
📌 结论:
👉 “GIN 一下就能过滤到极少行,非常划算”
于是:
- ✅ 走 trigram 索引
- ✅ 查询时间 16ms -> 0.05ms(≈300 倍提升)
📌 中文 pg_trgm LIKE 的经验定律
| LIKE 内容 | 是否稳定走索引 | 原因 |
|---|---|---|
| 1 个字 | ❌ 不走 | trigram 不成立 |
| 2 个字 | ❌/⚠️ | trigram 太少 |
| ≥ 3 个字 | ✅ 稳定 | 选择性足够 |
这两条 SQL
➡️ 是这个定律的完美实证
因为 PostgreSQL 的目标是:
平均最优,不是单次最优
对于 %融资%:
- 如果未来数据更多
- 匹配比例更高
- GIN 成本会上升
👉 planner 保守选择 Seq Scan
这是 PostgreSQL 的设计哲学,不是缺陷。
你可以这样“作弊”:
SET enable_seqscan = off;
然后 %融资% 也会走索引。
⚠️ 但这是 反优化:
- 查询可能更慢
- 影响全局
- 生产环境 ❌
✅ 正确做法(生产级)
关键词至少 3 个汉字
这是 新闻 / 搜索系统的行业常规做法
-- >= 3 字
WHERE title LIKE '%关键词%'
-- < 3 字
允许慢 or 提示用户
数据库不是搜索引擎
pg_trgm 已经是数据库层的极限了
- ✅ 索引建对
- ✅ planner 判断正确
- ✅ 性能行为符合预期
- ✅ 没有“隐藏 bug”
你现在遇到的不是问题,而是:
已经理解到了 pg_trgm 的边界
pg_trgm 在中文 LIKE 中:
- %融资% -> 顺序扫描是对的
- %融资额% -> 索引是王者
这不是配置问题,是数学问题。