PostgreSQL在处理模糊查询时,传统的B-Tree索引无法有效支撑LIKE '%关键词%'这类前后均带通配符的匹配,因为B-Tree只能利用左前缀特性。pg_trgm扩展通过将字符串拆分为连续三个字符组成的三元组,并基于这些三元组构建GIN或GiST索引,显著提升模糊搜索性能。它既能让普通LIKE查询走索引,也能实现基于相似度的模糊匹配,非常适合用户昵称搜索、地址补全、文本去重等场景。

一、三元组索引的核心原理
pg_trgm中的trgm是trigram的缩写,意思是连续三个字符组成的片段。扩展在拆分字符串时,会先在字符串两端各加上两个空格,然后按顺序每三个字符切分一次。以hello为例,生成的三元组包括两个前导空格加h、一个前导空格加he、hel、ell、llo、lo加一个后导空格等。这种设计可以让索引保留字符串的边界信息,比如开头和结尾的片段也参与匹配。
加速LIKE查询的关键在于三元组的包含关系。如果某个行文本包含查询模式,那么该行文本的三元组集合必然包含查询模式的所有三元组。反过来,如果一行文本的三元组集合不包含查询模式的任意一个三元组,这行文本就一定不匹配。因此查询时只需提取模式中的三元组,通过索引找到包含这些三元组的候选行,再做一次精确过滤即可。这个候选集通常远小于全表,从而大幅降低扫描成本。
可以使用show_trgm函数直观查看字符串被拆分后的结果,例如执行SELECT show_trgm('hello');会返回若干三元组。对于中文文本,pg_trgm按字符而不是字节拆分,因此同样能够正常工作,只是每个三元组对应三个汉字字符。
-- 查看字符串拆分出的三元组
SELECT show_trgm('hello');
二、如何创建pg_trgm索引并执行相似度查询
要使用pg_trgm,首先需要在数据库中启用扩展。扩展安装完成后,便可以为文本列创建GIN或GiST索引。GIN索引适合LIKE/ILIKE这类包含匹配,查询速度快,但构建时间较长、占用空间较大。GiST索引在空间占用上更有优势,并且支持相似度排序的KNN检索,适合需要按相似度排序返回结果的场景。
创建GIN索引的语法是在列上指定gin_trgm_ops操作符类,GiST索引同理使用gist_trgm_ops。以users表的name字段为例,如果要加速SELECT * FROM users WHERE name LIKE '%abc%',只需创建对应的GIN索引,优化器就会自动选择索引扫描。对于数据量大且更新频繁的表,建议在维护窗口创建GIN索引,并关注索引膨胀情况。
相似度查询依赖similarity函数和百分号操作符。similarity函数返回两个字符串三元组重合程度的0到1之间的数值,数值越高代表越相似。通过设置pg_trgm.similarity_threshold参数可以控制百分号操作符的匹配阈值,默认是0.3。配合GiST索引,还可以使用距离操作符进行按相似度排序,快速返回最接近的记录。
-- 启用扩展 CREATE EXTENSION IF NOT EXISTS pg_trgm; -- 创建GIN索引加速LIKE/ILIKE CREATE INDEX idx_users_name_trgm ON users USING gin (name gin_trgm_ops); -- 创建GiST索引支持相似度排序 CREATE INDEX idx_users_name_gist ON users USING gist (name gist_trgm_ops); -- 设置相似度阈值并查询相似名字 SET pg_trgm.similarity_threshold = 0.3; SELECT name, similarity(name, 'Alice') AS sim FROM users WHERE name % 'Alice' ORDER BY name <-> 'Alice' LIMIT 10;
三、GIN与GiST的适用场景及调优建议
GIN和GiST并不是完全互相替代的关系。GIN索引查询性能通常更高,尤其是对于高并发的LIKE查询,因为GIN使用倒排列表存储每个三元组出现的行位置,查找时直接定位。代价是写入更新时需要维护大量倒排项,导致插入、更新变慢,索引体积也比GiST大。GiST索引采用树形结构,写入开销相对均衡,体积更小,最重要的是支持按距离排序的KNN查询,适合相似内容推荐和模糊搜索排序。
对于短字符串,pg_trgm的效果会明显下降。比如查询模式只有两个字符时,三元组数量很少,候选集合可能接近全表,索引过滤能力变弱。实际项目中可以结合长度条件来减少无用匹配,例如WHERE name LIKE '%ab%' AND char_length(name) BETWEEN 2 AND 20。另外如果字符长度小于三个字符,pg_trgm可能直接退回全表扫描,这时需要评估是否值得使用该索引。
相似度阈值的选择也很关键。阈值设置过高会漏掉部分拼写错误的查询,设置过低则返回过多不相关结果。一般建议从0.3开始,根据业务反馈逐步调整。对于中文文本,字符数量少、组合多,三元组区分度尚可,但也可以考虑先分词再存储分词结果,用pg_trgm做二次模糊匹配。此外,如果只关心大小写不敏感的匹配,ILIKE虽然能走pg_trgm索引,但有一定额外开销,使用lower函数索引或citext类型会更稳定。
四、常见误区与性能陷阱
一个常见误区是认为pg_trgm可以完全替代Elasticsearch等专用全文搜索工具。pg_trgm擅长的是模糊匹配和相似度计算,对于复杂的自然语言分词、相关性排序、海量文档全文检索,仍然需要借助专门的搜索引擎。pg_trgm的优势在于与PostgreSQL事务、SQL查询能力紧密集成,数据量在千万级别以内时通常能获得不错的响应速度。
另一个误区是只要创建了pg_trgm索引,所有LIKE查询就一定会走索引。实际上如果查询模式只包含常见三元组,候选行比例很高,优化器可能判断全表扫描成本更低而放弃索引。此外,以通配符开头的LIKE查询虽然能被加速,但正则表达式匹配默认不受pg_trgm索引支持,只有将正则转换为触发器或使用其他扩展才能优化。
为了保持pg_trgm索引的高效,建议定期执行VACUUM和ANALYZE更新统计信息,让优化器做出更准确的计划。对于更新频繁的表,可以评估采用GIN索引的fastupdate参数或使用GiST索引降低写放大。实际落地时,先在测试环境用EXPLAIN ANALYZE确认索引是否被使用、扫描行数是否明显减少,再决定生产部署方案。

