把PostgreSQL的全文检索和模糊查询放在一起比性能,其实有点不公平。这就像拿一辆专业赛车和一辆家用轿车比谁跑得快,结论从一开始就注定了。但问题在于,很多开发者在实际项目中仍然在“用家用轿车跑赛道”——明明需要复杂的文本搜索能力,却还在用LIKE加前后百分号硬扛。真正该问的不是谁更快,而是你的查询场景到底属于哪一种。全文检索解决的是“语言”问题,模糊查询解决的是“字符串”问题,这两者在数据库内部的执行路径完全不同。

模糊查询的本质与性能天花板

模糊查询最典型的写法就是 LIKE '%关键词%',或者使用正则表达式 ~ 操作符。这种查询的核心逻辑是逐行扫描,把每一行的目标字段拿出来,和你的模式做匹配。在PostgreSQL中,如果你没有在字段上建索引,这就是一次全表扫描,数据量一大,查询时间线性增长。即使建了标准的B-tree索引,一旦你的模式以百分号开头,索引立刻失效,因为B-tree依赖前缀匹配,而'%关键词%'这种模式根本没有固定前缀可言。

为了解决这个问题,PostgreSQL提供了 pg_trgm 扩展,它可以把字符串拆成连续的三个字符一组(trigram),然后对这些trigram建GIN或GiST索引。这样 LIKE '%关键词%' 和 ILIKE 不区分大小写的模糊查询都能走索引。具体效果取决于关键词长度和数据分布:如果关键词很短,比如两个字符,匹配到的trigram会非常多,索引扫描的范围巨大,性能提升有限;如果关键词长度适中且区分度高,索引能大幅缩小扫描范围。但即使走了trigram索引,模糊查询的本质仍然是“字符匹配”,它完全不理解单词的语义、词形变化、同义词,也无法处理自然语言中的停用词和权重排序。你搜“跑步”,它不会匹配到“跑过步”或“跑着步”,除非你的模式本身做了处理。

全文检索的底层机制

PostgreSQL的全文检索走的是一条完全不同的路。它不是直接比对字符串,而是先把文本解析成词位(token),再通过词典规范化成词素(lexeme)。比如“The cats are running”经过英文词典处理后,会变成“cat”和“run”这两个词素,停用词“the”和“are”直接丢弃。这些词素会被存储在一个特殊的数据类型 tsvector 中,然后对这个tsvector列建GIN索引。当你执行全文搜索时,查询字符串同样被解析和规范化,生成 tsquery,再去索引中匹配。这个过程中,大小写、时态、单复数、语态等变化都被统一处理了。

从索引结构看,GIN索引对tsvector的每个词素都建立了倒排索引,直接指向包含该词素的行。这意味着全文检索的索引查找是精确到词素级别的,不需要像trigram那样扫描一个范围的索引条目。对于大文本字段的包含查询,比如在几百万篇文章中搜索包含“数据库性能优化”的文档,全文检索的索引效率远高于trigram。而且全文检索天然支持布尔逻辑组合、短语搜索、权重设置和排序,这些都是模糊查询无法做到的。

真实场景下的性能对比

我们直接看一组测试数据。假设有一张包含200万行记录的 articles 表,每行的 content 字段平均长度约2000字。测试环境为PostgreSQL 15,配置了基本的shared_buffers和work_mem优化。先看模糊查询,使用 pg_trgm 的GIN索引:

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_content_trgm ON articles USING gin (content gin_trgm_ops);

-- 模糊查询
EXPLAIN ANALYZE 
SELECT * FROM articles WHERE content LIKE '%数据库性能优化%';

这个查询在200万行数据中,如果关键词“数据库性能优化”长度足够且不是高频词,索引扫描大约需要200到500毫秒。但如果换成短关键词“数据”,匹配的trigram数量暴增,查询时间可能飙升到2秒以上。再来看全文检索:

-- 添加tsvector列并建索引
ALTER TABLE articles ADD COLUMN content_tsv tsvector;
UPDATE articles SET content_tsv = to_tsvector('chinese', content);
CREATE INDEX idx_content_tsv ON articles USING gin (content_tsv);

-- 全文检索
EXPLAIN ANALYZE 
SELECT * FROM articles WHERE content_tsv @@ to_tsquery('chinese', '数据库 & 性能 & 优化');

同样的查询意图,全文检索的索引扫描通常在10到50毫秒级别完成,而且这个性能不随关键词长度和频率剧烈波动。因为倒排索引直接定位到包含这三个词素的行,然后做交集运算,整个过程在索引层面就完成了大部分工作。这里的关键差异在于:trigram索引需要扫描所有包含相关三字符组的行,然后回表检查完整匹配;而全文检索的倒排索引直接给出了精确的行ID列表。

中文场景的特殊挑战

很多对比文章会忽略一个致命问题:中文分词。pg_trgm对中文的处理是按字符切分trigram,完全不理解词语边界。比如“全文检索性能”会被切成“全文文”“文检索”“检索性”“索性能”这样的trigram。这种切分在短关键词场景下会导致大量误匹配,索引选择性很差。而全文检索依赖分词器,PostgreSQL默认的中文分词器是按单字切分的,效果同样糟糕。真正让中文全文检索可用的是安装 zhparser 或 jieba 这类第三方分词扩展:

-- 使用zhparser分词
CREATE TEXT SEARCH CONFIGURATION chinese_zh (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION chinese_zh ADD MAPPING FOR n,v,a,i,e,l WITH simple;

-- 使用jieba分词
CREATE TEXT SEARCH CONFIGURATION chinese_jieba (PARSER = jieba);

配置好分词器后,中文文本才能被正确切分成有意义的词语,全文检索的精度和性能才能达到预期。而模糊查询在中文场景下,即使有trigram索引,也始终受困于字符匹配的局限性,无法理解“数据库”和“资料库”之间的语义关联。如果你的应用需要处理中文自然语言搜索,全文检索加分词器是唯一正确的选择,模糊查询只能作为辅助手段。

模糊查询不可替代的场景

全文检索虽然强大,但并非万能。有些场景下模糊查询反而是更合适的选择,甚至无法被替代。第一种是前缀匹配和后缀匹配,比如搜索以“PostgreSQL”开头的所有字符串,LIKE 'PostgreSQL%' 配合B-tree索引性能极高,全文检索做这个反而别扭。第二种是精确子串匹配,比如搜索包含特定产品编码、序列号、URL片段的记录,这些内容没有语言意义,分词器可能把它们切得支离破碎,全文检索反而找不到。第三种是短字段的即时搜索,比如用户在输入框中边输入边提示,trigram索引可以很好地支持这种增量匹配,而全文检索需要完整的词语输入才能生效。

还有一种情况是数据量不大,比如只有几万行,而且查询模式多变,建全文检索的tsvector列和维护触发器反而增加了复杂度。这时候一个简单的 LIKE 查询配合 trgm 索引就足够用了,开发成本最低。关键是要根据实际的数据量、查询频率、用户输入模式来决定技术方案,而不是一味追求“高性能”。

混合架构:各取所长

在实际生产环境中,最高效的方案往往是混合使用。比如电商平台的商品搜索,用户输入的关键词既有品类词也有型号编码。你可以对商品标题和描述建全文检索索引,处理自然语言查询;同时对商品编码、品牌名等结构化字段建trigram索引,处理精确匹配和前缀搜索。查询时根据用户输入的特征动态选择索引:检测到纯数字或字母编码时走模糊查询,检测到自然语言短语时走全文检索。

PostgreSQL的强大之处在于它允许你在同一张表上同时建立多种索引,查询优化器会根据WHERE条件自动选择合适的索引组合。你甚至可以在一个查询中混合使用全文检索和模糊查询:

SELECT * FROM products 
WHERE content_tsv @@ to_tsquery('chinese', '无线耳机')
  AND product_code LIKE 'WH-%'
ORDER BY ts_rank(content_tsv, to_tsquery('chinese', '无线耳机')) DESC;

这种写法同时利用了两个索引,全文检索负责缩小文本匹配的范围,模糊查询负责精确过滤编码前缀,最后按全文检索的相关度排序。这种组合拳在实际业务中非常实用,也是PostgreSQL相比专用搜索引擎的一个独特优势——你不需要引入Elasticsearch这样的外部依赖,就能在数据库层面完成相当复杂的搜索逻辑。

索引维护与写入性能的权衡

讨论查询性能不能只看读,还要看写。全文检索的tsvector列需要额外的存储空间,而且每次插入或更新文本字段时,都需要重新生成tsvector。你可以用触发器自动维护,也可以使用生成列(PostgreSQL 12+支持)来简化:

ALTER TABLE articles ADD COLUMN content_tsv tsvector 
GENERATED ALWAYS AS (to_tsvector('chinese', content)) STORED;

生成列的好处是不需要手动维护,PostgreSQL自动在写入时计算并存储。但这意味着每次写入都要做分词和规范化,写入性能会受到影响。相比之下,trigram索引虽然也会增加写入开销,但GIN索引的更新机制已经相当成熟,而且不需要额外的存储列。如果你的系统是写入密集型,需要对这两种索引的写入成本做实际测试。一般来说,全文检索的写入开销略高于trigram索引,但在读取性能上的回报通常远大于这点额外成本。

还有一个容易被忽略的点是索引大小。对同样的文本内容,trigram的GIN索引通常比全文检索的GIN索引大得多,因为trigram把每个三字符组合都作为索引条目,而全文检索只存储规范化的词素。在200万行测试数据中,trigram索引可能占用2到3GB,而全文检索索引通常在500MB到1GB之间。索引大小直接影响缓存命中率,进而影响实际查询性能。

最终选型建议

如果你的搜索需求是自然语言级别的,用户输入的是口语化的查询词,需要处理同义词、词形变化、相关度排序,那全文检索是唯一正确的选择。不要试图用LIKE加百分号去模拟搜索引擎的行为,这在数据量超过十万行后就会迅速崩溃。如果你的搜索需求是精确的子串匹配、编码查找、前缀补全,那trigram索引配合LIKE或ILIKE完全够用,而且实现简单。如果你两者都需要,就在同一张表上同时建两种索引,让PostgreSQL的查询优化器帮你选择。

最后提醒一点:无论选择哪种方案,都要在生产环境的真实数据量下做基准测试。EXPLAIN ANALYZE输出的毫秒数在开发环境的小数据集上没有参考意义。用 pgbench 或者自定义脚本模拟并发查询,观察实际响应时间和资源消耗,才能做出正确的技术决策。PostgreSQL已经为你准备好了所有工具,剩下的就是根据场景做出务实的选择。