在数据库开发中,我们最不愿意面对的往往不是复杂的 Join,而是"文本处理"。
- 模糊搜索: 业务方要求"搜索包含'苹果'的所有商品",开发人员反手一个 LIKE '%苹果%',导致全表扫描,数据库 CPU 飙升。
- 数据清洗: 历史数据里混入了非法的手机号格式,需要清洗,只能写 Python 脚本把几百万条数据拉出来跑正则。
- 多行合并: 前端要求把"用户拥有的所有标签"合并成一个逗号分隔的字符串返回,后端只能在内存里拼接。

一、 拒绝全表扫描:LIKE 的替代者------全文索引 (Full-Text)
场景复现:
表 t_articles 有 100 万篇文章。需求是搜索内容中包含 "database" 的文章。
低效解法:
sql
SELECT * FROM t_articles WHERE content LIKE '%database%';
原理分析:
标准的 B+ 树索引只能支持前缀匹配(LIKE 'database%')。一旦通配符 % 出现在开头,索引立即失效,数据库必须逐行扫描(Full Table Scan),性能复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)。
技术解法:倒排索引 (Inverted Index)
MySQL InnoDB 引擎支持 全文索引 (Full-Text Index) 。它通过分词构建"词 -> 文档ID"的倒排映射,实现 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 或 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g N ) O(logN) </math>O(logN) 级别的搜索。
1. 创建索引:
sql
ALTER TABLE t_articles ADD FULLTEXT INDEX ft_content (content);
2. 使用 MATCH ... AGAINST 语法:
sql
SELECT * FROM t_articles
WHERE MATCH(content) AGAINST('database' IN NATURAL LANGUAGE MODE);
实战注意点:
- 最小词长: 默认情况下,少于 3 个字符的词(如 "it", "is")会被忽略。可以通过 innodb_ft_min_token_size 调整。
- 停用词 (Stopwords): 常见的无意义词汇("the", "and")会被自动过滤。
- 分词器: 对于中文搜索,必须使用 ngram 分词器(WITH PARSER ngram),否则无法正确切分中文词组。

二、 SQL 里的瑞士军刀:正则表达式 (RegExp)
场景复现:
数据表中混入了脏数据,需要找出所有"非法的邮箱地址"进行标记;或者需要将手机号的中间四位脱敏为 ****。
低效解法:
LIKE 只能做简单的通配,无法表达"数字出现3次"这种逻辑。通常做法是写脚本把数据 Select 出来,用代码里的正则库处理完再 Update 回去。
技术解法:MySQL 8.0 的正则函数族
MySQL 8.0 引入了基于 ICU 库的完整正则支持(之前版本仅支持简单的 REGEXP 匹配)。
1. 查找脏数据 (REGEXP):
sql
-- 查找不包含 @ 符号,或者不以 .com/.net 等结尾的邮箱
SELECT * FROM t_users
WHERE email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';
- 提取数据 (REGEXP_SUBSTR):
从复杂的 JSON 字符串或日志文本中提取特定 ID:
sql
-- 提取 "Order ID: 12345 Created" 中的数字
SELECT REGEXP_SUBSTR(log_content, '[0-9]+') FROM t_logs;
- 数据清洗与脱敏 (REGEXP_REPLACE) ------ 杀手级功能:
直接在 SQL 层完成脱敏,无需应用层介入。
scss
-- 将手机号 13812345678 替换为 138****5678
-- 逻辑:捕获前3位($1),忽略中间4位,捕获后4位($2),重新拼接
UPDATE t_users
SET phone = REGEXP_REPLACE(phone, '^([0-9]{3})[0-9]{4}([0-9]{4})$', '$1****$2');
三、 行转列利器:GROUP_CONCAT 的陷阱与优化
场景复现:
用户表 t_users 和 标签表 t_tags 是多对多关系。
前端 API 需要返回如下格式:
{ "user_id": 1, "tags": "Developer, Admin, VIP" }
低效解法:
先查用户,再循环查标签(N+1 查询);或者查出所有关联数据,在应用层做 Map 聚合。
技术解法:GROUP_CONCAT
MySQL 提供了一个聚合函数,专门用于将"多行数据"合并为"一行字符串"。
vbnet
SELECT
u.id,
u.username,
-- 将该用户的所有 tag_name 用逗号连接
GROUP_CONCAT(t.tag_name ORDER BY t.id DESC SEPARATOR ', ') as tags
FROM t_users u
JOIN t_user_tags ut ON u.id = ut.user_id
JOIN t_tags t ON ut.tag_id = t.id
GROUP BY u.id;
⚠️ 致命陷阱:默认长度限制
很多开发者在开发环境测试没问题,一上生产环境发现标签被截断了。
这是因为 MySQL 对 GROUP_CONCAT 的结果长度有限制,由系统变量 group_concat_max_len 控制。
- 默认值: 仅 1024 字节 。如果合并后的字符串超过 1KB,会被静默截断,且不会报错。
解决方案:
在执行 SQL 前,或者在 Session 级别调大该参数:
ini
SET SESSION group_concat_max_len = 102400; -- 设置为 100KB
SELECT ...

总结
数据库不仅仅是存储引擎,它同样具备强大的计算和处理能力。
- 搜索: 遇到模糊匹配,别无脑 LIKE。如果数据量大且需要自然语言处理,请启用 Full-Text Index。
- 清洗: 善用 MySQL 8.0 的 REGEXP_REPLACE,它能让你用一句 SQL 完成百万级数据的清洗和脱敏,效率远超 Python 脚本。
- 聚合: 使用 GROUP_CONCAT 简化"一对多"查询,但永远不要忘记检查 group_concat_max_len 参数。