最近在排查一个MySQL查询异常时,遇到了一个相当诡异的现象:对同一个字段执行看似相同的模糊查询,使用LIKE和REGEXP操作符得到的结果数量差异巨大。具体表现为:
sql复制-- 使用REGEXP查询分号
SELECT * FROM kg_question_answer WHERE answer REGEXP ';';
-- 返回3条记录
-- 使用LIKE查询分号
SELECT * FROM kg_question_answer WHERE answer LIKE '%;%';
-- 返回731条记录
作为有多年数据库开发经验的工程师,我第一反应是检查SQL语法是否正确。确认无误后,开始怀疑是否因为转义字符的问题导致REGEXP匹配失效。于是尝试了以下变体:
sql复制-- 尝试转义分号
SELECT * FROM kg_question_answer WHERE answer REGEXP '\\;';
-- 依然返回3条记录
-- 尝试不同的正则表达式写法
SELECT * FROM kg_question_answer WHERE answer REGEXP '[;]';
-- 结果不变
这些尝试都没有改变查询结果,说明问题不在转义字符上。此时我注意到Navicat显示的查询结果中,LIKE匹配到的记录包含全角分号(;)和半角分号(;),而REGEXP似乎只匹配了半角分号。
通过SHOW CREATE TABLE kg_question_answer查看表结构,发现该表使用的字符集和排序规则为:
code复制CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci
这里的关键在于_ai_ci这个排序规则后缀:
_ci表示大小写不敏感(Case Insensitive)_ai表示口音不敏感(Accent Insensitive)在MySQL 8.0及以上版本中,utf8mb4_0900_ai_ci是默认排序规则。这种排序规则会将许多相似的字符视为等价,包括:
重要提示:这种"模糊匹配"行为是设计如此,并非bug。MySQL文档明确说明_ai_ci排序规则会忽略某些字符差异。
LIKE是SQL标准操作符,其行为受排序规则直接影响:
sql复制-- 以下查询在utf8mb4_0900_ai_ci下等价
SELECT * FROM t WHERE col LIKE '%;%';
SELECT * FROM t WHERE col LIKE '%;%';
REGEXP(或RLIKE)是MySQL的正则表达式实现:
sql复制-- 严格匹配半角分号
SELECT * FROM t WHERE col REGEXP ';';
-- 严格匹配全角分号
SELECT * FROM t WHERE col REGEXP ';';
如果需要精确区分全角/半角字符:
方案一:修改排序规则
sql复制-- 建表时指定二进制排序规则
CREATE TABLE t (
answer TEXT
) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
-- 或修改现有表
ALTER TABLE kg_question_answer MODIFY answer TEXT COLLATE utf8mb4_bin;
方案二:使用REGEXP明确匹配两种分号
sql复制SELECT * FROM kg_question_answer
WHERE answer REGEXP '[;;]';
方案三:使用十六进制表示
sql复制-- 匹配半角分号(U+003B)
SELECT * FROM t WHERE answer LIKE CONCAT('%', 0x3B, '%');
-- 匹配全角分号(U+FF1B)
SELECT * FROM t WHERE answer LIKE CONCAT('%', 0xFF1B, '%');
如果需要保持当前排序规则的模糊匹配特性:
方案一:使用LIKE并接受模糊匹配
sql复制-- 会匹配所有分号变体
SELECT * FROM kg_question_answer WHERE answer LIKE '%;%';
方案二:组合使用LIKE和REGEXP
sql复制-- 先模糊匹配再精确筛选
SELECT * FROM kg_question_answer
WHERE answer LIKE '%;%'
AND answer REGEXP '[;]';
在实际项目中处理此类问题时,我总结了以下经验:
字符集选择建议:
utf8mb4_binutf8mb4_0900_ai_ci性能考量:
sql复制ALTER TABLE kg_question_answer
ADD COLUMN has_semicolon TINYINT GENERATED ALWAYS AS (IF(answer LIKE '%;%', 1, 0)) STORED,
ADD INDEX (has_semicolon);
开发规范建议:
调试技巧:
HEX()函数查看实际存储的二进制数据:sql复制SELECT answer, HEX(answer) FROM kg_question_answer
WHERE answer LIKE '%;%' LIMIT 10;
COLLATION()函数检查表达式使用的排序规则:sql复制SELECT COLLATION(answer) FROM kg_question_answer LIMIT 1;
除了分号外,utf8mb4_0900_ai_ci排序规则下还有许多字符会被视为等价。常见例子包括:
| 字符1 | 字符2 | Unicode码点 |
|---|---|---|
| a | а | U+0061 vs U+0430 |
| A | Α | U+0041 vs U+0391 |
| k | k | U+006B vs U+FF4B |
| - | ﹣ | U+002D vs U+FE63 |
可以通过以下查询测试特定字符的等价性:
sql复制SELECT 'a' = 'а' COLLATE utf8mb4_0900_ai_ci; -- 返回1(等价)
SELECT 'a' = 'а' COLLATE utf8mb4_bin; -- 返回0(不等价)
对于需要处理多语言数据的应用,理解这些细微差别至关重要。我曾经在一个国际化项目中遇到用户搜索"cafe"时匹配到"café"的记录,正是排序规则在发挥作用。