一、fuzzystrmatch 模块解决的核心问题
等值查询在数据质量稍差的环境里非常脆弱。例如客户表里同时存在 Christine 和 Kristine,两条记录可能指向同一个人,但使用 WHERE name = 'Christine' 永远匹配不到后者。模糊字符串匹配的目标不是判断两个字符串是否完全相等,而是给出一个相似度或距离值,让查询条件可以从精确匹配扩展为相似匹配。

PostgreSQL 的 fuzzystrmatch 扩展把这类算法打包成了一个轻量模块,安装后不需要外部程序支持。它主要解决三类问题:英文姓名的发音变体合并、拼写错误纠正、短文本近似去重。它不适合大段文本语义相似,也不适合中文同音字判断,但在客户主数据清洗、产品编号修正等场景下非常实用。
该模块的函数都接收 text 类型参数,返回整型或 text。使用成本很低,但因为多数函数没有索引支持,在大表上直接全量计算会非常慢。理解每个函数的计算逻辑,才能把模糊匹配局限在合理的候选集里。
二、核心函数:Soundex、Difference 与 Levenshtein
Soundex 是最早的发音编码算法之一,它把英文单词转换成一个字母加三位数字的编码。规则是保留首字母,后续字母按发音分组映射到 0 到 6,并且去掉连续重复数字和补零。例如 Robert 和 Rupert 都得到 R163,因此可以认为两者发音相近。Soundex 只关心英文字母,数字和空格会被忽略,小写会自动转成大写处理。
CREATE EXTENSION fuzzystrmatch;
SELECT soundex('Robert') AS robert_code,
soundex('Rupert') AS rupert_code;
-- 两者都返回 R163
SELECT difference('Robert', 'Rupert') AS diff_score;
-- 返回 4,表示两个 Soundex 编码完全相同
Difference 函数直接比较两个字符串的 Soundex 编码,返回 0 到 4 的整数:4 表示编码完全一致,0 表示完全不同。这个函数很适合在 WHERE 条件里做快速过滤,比如 difference(name, 'Kristine') >= 3 可以瞬间筛出发音接近的姓名。不过 Soundex 的粒度很粗,像 Smith 和 Smyth 会被认为是相同编码,但 Christine 和 Kristine 因为开头字母不同不会命中的情况也可能出现。
Levenshtein 距离衡量的是编辑操作次数:把一个字符串变成另一个字符串需要插入、删除或替换多少个字符。kitten 到 sitting 的距离是 3,分别是替换 k 为 s、替换 e 为 i、插入 g。它的结果很直观,适合处理拼写错误,不依赖发音规则,因此对英文之外的文本也有一定通用性。
SELECT levenshtein('kitten', 'sitting') AS edit_distance;
-- 返回 3
SELECT levenshtein(lower('PostgreSQL'), lower('PostgreSQl')) AS distance;
-- 返回 1,忽略一个字母的大小写差异
使用 Levenshtein 时建议先用 lower 归一化大小写,避免因为一个字母大小写差异产生不必要的距离。但 lower 只会处理英文字母,如果是土耳其语、德语等扩展字母,需要根据业务单独处理。
Metaphone 和 Double Metaphone 可以看作 Soundex 的增强版。Metaphone 对英文拼写规则建模更细,例如 Smith 和 Smyth 的 Metaphone 编码都是 SM0,而 ph 和 f 也能得到相同的表示。Double Metaphone 为每个输入返回两个编码,一个主编码和一个备选编码,用以覆盖更多发音变体和外来语。实际项目中如果英文姓名清洗较多,dmetaphone 通常比 Soundex 更准确。
SELECT metaphone('Smith', 10) AS smith_code,
metaphone('Smyth', 10) AS smyth_code;
-- 都返回 SM0
SELECT dmetaphone('Christine') AS primary_code,
dmetaphone_alt('Christine') AS alt_code;
-- 返回主备两份编码
三、实际场景:近似去重与模糊搜索
客户表里最常见的问题是同一个人被录入了两次,姓名略有差异。比如一条记录是 Katherine,另一条是 Katharine。可以用自连接配合 Levenshtein 距离找出编辑距离在 2 以内的记录对。为了避免每一对被计算两次,可以在连接条件里限制 id 大小关系,并排除完全相同的行。
SELECT a.customer_id AS id_a,
b.customer_id AS id_b,
a.full_name AS name_a,
b.full_name AS name_b,
levenshtein(lower(a.full_name), lower(b.full_name)) AS distance
FROM customers a
JOIN customers b
ON a.customer_id < b.customer_id
WHERE levenshtein(lower(a.full_name), lower(b.full_name)) BETWEEN 1 AND 2
ORDER BY distance, a.customer_id;
这种查询适合在几千到几万行的客户表上做离线去重,因为 Levenshtein 需要把每一对都算一遍。也可以在 WHERE 里先用 difference 过滤一轮,但需要注意 difference 基于发音不基于编辑距离,两个过滤条件的语义不同。
在做模糊搜索时,不要对整个大表直接按 levenshtein 排序,而应该先准备一个候选集。例如用户输入一个客户姓名,可以先通过姓名前缀、pg_trgm 的 GIN 索引或其他业务字段把候选范围缩到几百条,再对候选集计算编辑距离并排序。这样既能利用编辑距离的精确性,又能避免全表扫描。
对于产品型号、证件号码等短文本,Levenshtein 距离通常比发音函数更合适。短文本中一个字符差异就可能代表不同规格,发音函数会把过多无关项拉到一起。反过来,英文姓名和地址中的常见变体则更适合 Soundex 或 Metaphone。
四、性能边界与中文场景限制
Levenshtein 函数的时间复杂度是 O(n*m),其中 n 和 m 分别是两个字符串的长度。它的计算不像普通 SQL 条件那样可以利用 B-tree 索引,因此如果直接在大表上执行 JOIN 加 WHERE levenshtein(...) 条件,PostgreSQL 会对所有行对做全量运算,数据量增长到百万级时运行时间会迅速变得不可接受。
更合理的做法是把 fuzzystrmatch 当成精排工具,而不是初筛工具。先用 pg_trgm 扩展创建 GIN 索引进行相似候选召回,再对少量候选计算编辑距离。pg_trgm 提供 similarity 函数和 GIN 索引支持,擅长处理毫秒级相似搜索;fuzzystrmatch 则提供精确的发音编码和编辑距离。两者结合可以兼顾速度和准确度。
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_customers_name_trgm ON customers USING gin (full_name gin_trgm_ops);
-- 先召回相似候选
SELECT customer_id, full_name
FROM customers
WHERE full_name % 'Katharine';
-- 再对结果集做精确排序
SELECT customer_id, full_name,
levenshtein(lower(full_name), lower('Katharine')) AS distance
FROM customers
WHERE full_name % 'Katharine'
ORDER BY distance;
中文字符串是 fuzzystrmatch 的一个明显短板。Soundex 和 Metaphone 基于英文发音规则,对汉字基本无效。Levenshtein 虽然可以在字符层面计算中文编辑距离,但它无法区分同音字,也理解不了拼音相似性。例如输入张小明和张晓明,编辑距离是 1,确实能判断拼写接近;但输入王海和王海鹏,距离也很低,却很难体现语义差异。真正的中文模糊匹配通常需要拼音转换、同义词或向量检索配合。
另外,fuzzystrmatch 的函数对 NULL 输入会返回 NULL,因此在连接条件或 WHERE 中需要先确认字段非空,否则候选行会被静默忽略。对空字符串和超长字符串的行为也要提前测试,避免在边界数据上产生预期外的距离值。
PostgreSQLfuzzystrmatch字符串相似度修改时间:2026-10-03 08:20:18