SaaS系统刚上线时,一条 WHERE name LIKE '%keyword%' 可能只需要几毫秒,开发团队甚至感觉不到它有什么问题。但随着租户数量、业务记录和并发请求持续增长,同样的SQL可能从几十毫秒变成几百毫秒,最后甚至拖慢整个数据库。此时真正需要解决的,并不是简单地给字段“加一个索引”,而是先弄清楚用户究竟需要什么样的搜索,以及MySQL当前为什么无法高效执行。
最容易优化的是前缀搜索。例如 WHERE name LIKE 'abc%',在合适的数据类型、字符集、排序规则以及查询条件下,MySQL可以利用B-Tree索引快速定位以 abc 开头的记录。此时通常应该优先建立普通索引,例如 (name),而不是一看到LIKE就上全文搜索。对于只需要返回少量字段的查询,还可以设计覆盖索引,减少回表成本。
但 LIKE '%abc%' 是完全不同的问题。通配符位于字符串开头时,传统B-Tree索引通常无法像前缀匹配那样直接定位起始范围,因此数据库可能需要扫描大量索引记录甚至整张表。解决方案也不能简单写成“改用全文索引”。全文索引解决的是词语或文本检索问题,而不是所有形式的任意子串匹配。用户搜索产品名称、文章标题、说明文字时,全文搜索可能非常合适;如果用户要求在任意位置匹配一段连续字符,则需要根据数据规模和搜索模式考虑其他方案,例如专门的搜索引擎、n-gram相关索引机制,或者调整业务搜索规则。
MySQL自己的全文索引同样不能被当成万能替代品。它有自己的分词、停用词、最小词长度以及自然语言处理规则,不同存储引擎和MySQL版本的行为也需要结合实际配置验证。中文搜索尤其需要测试实际业务词汇,因为中文不像英文那样天然由空格划分单词。MySQL 8.0提供了ngram parser等机制,可以针对中文、日文等没有明显空格分隔的语言进行文本检索,因此没有必要简单地认为“必须使用jieba”。如果业务需要复杂的中文分词、同义词、拼音、纠错、权重以及搜索推荐,专业搜索引擎通常更容易满足要求。
分页同样是LIKE搜索变慢后经常被忽视的问题。LIMIT 100000, 20意味着数据库需要找到并跳过大量前面的结果,数据量越大,成本通常越高。对于按照稳定排序字段连续翻页的场景,可以采用基于游标或键集分页,例如利用上一页最后一条记录的排序键继续向后查询。不过,分页方式必须和排序规则、唯一性以及索引设计一起考虑,不能简单把所有OFFSET查询都改成游标。
SaaS系统还有一个特别重要的问题,就是租户隔离。假设所有租户共享一张业务表,那么搜索通常应该包含 tenant_id 条件,例如 WHERE tenant_id = ? AND name LIKE 'abc%'。这里恰恰不能得出“索引不要包含tenant_id”的结论。对于多租户共享表,(tenant_id, name)这样的联合索引往往非常有价值,因为数据库首先缩小到当前租户的数据范围,再执行后续搜索。到底应该使用 (tenant_id, name)、(name, tenant_id)还是其他组合,需要根据WHERE条件、排序方式、选择性以及实际执行计划决定,而不是套用固定规则。
对于高频搜索,还应该使用 EXPLAIN、慢查询日志以及Performance Schema观察真实执行情况。不能只看SQL表面写法。需要关注实际扫描行数、访问类型、过滤效率、排序和临时表情况,并结合数据分布判断索引是否真正发挥作用。一个拥有几百万条记录的表,如果每个租户只有几千条数据,和所有数据集中在少数几个大型租户中的情况,最佳索引设计可能完全不同。
当MySQL已经无法经济地承担复杂搜索时,再考虑引入Elasticsearch或OpenSearch之类的搜索系统会更加合理。搜索引擎适合全文检索、多字段搜索、相关性排序、分词、模糊搜索、聚合以及复杂过滤等场景。但这意味着系统从“一套数据库”变成“数据库加搜索索引”两套数据系统,随之而来的就是数据同步、延迟、失败重试、索引重建以及一致性问题。
因此,数据库与搜索引擎之间通常需要建立可靠的数据同步机制。可以通过应用层事件、消息队列、CDC等方式把数据库变化传递给搜索索引。像Debezium这样的CDC工具可以捕获数据库变更,但它只是同步链路中的一个组件,并不能自动解决所有一致性问题。实际系统仍然需要处理重复事件、乱序事件、删除事件、消费者故障以及搜索索引重建等情况。
Redis也可以帮助缓解热点搜索,但缓存并不能把一个糟糕的查询变成好查询。如果用户不断搜索完全不同的关键词,缓存命中率可能很低;如果数据变化频繁,缓存失效又会带来额外复杂度。布隆过滤器可以帮助判断某些不存在的键,但它并不适合直接解决普通的关键词搜索问题。至于用Sorted Set实现“权重缓存淘汰”,也不是Redis缓存搜索结果的通用方案,实际应根据访问频率、数据更新模式和缓存容量选择策略。
另一个容易出现的误区,是把搜索操作与事务锁联系起来。普通关键词查询通常不需要 SELECT ... FOR UPDATE。这个语句用于事务中的锁定读取,主要服务于需要并发控制的数据更新场景。如果只是搜索商品、客户或文章,给查询加行锁反而可能增加锁竞争。搜索系统应该尽量采用无锁读取、合适的事务隔离级别和合理的连接池配置,而不是为了“并发安全”给搜索查询加锁。
至于数据量达到多少以后必须分库分表,也不存在统一答案。百万级数据完全可能通过正确的索引、SQL重写、分页优化和硬件资源继续运行;千万级甚至更大的数据表,也不意味着必须立即分库分表。真正应该观察的是查询延迟、QPS、写入量、索引大小、Buffer Pool命中情况、磁盘IO、锁等待以及业务增长速度。过早进行分库分表,往往会把一个数据库性能问题升级成分布式系统问题。
如果搜索字段本身具有明显的业务边界,还可以从数据模型入手减少搜索范围。例如按租户隔离、按照状态过滤、区分冷热数据,或者把经常搜索的字段从巨大的宽表中拆分出来。对于历史数据,可以根据业务保留周期进行归档,但不能因为“数据超过千万”就机械地执行冷热分离。
一个成熟的SaaS搜索架构通常是逐级演进的。第一阶段解决SQL本身的问题,确认WHERE条件、索引、排序和分页是否合理;第二阶段通过缓存和数据库资源优化解决热点流量;第三阶段,当业务真正需要全文搜索、复杂相关性排序或大规模搜索时,再把搜索能力拆到OpenSearch、Elasticsearch等专用系统;到了更大规模,再根据租户数量、数据增长和并发模型考虑分片、分库或其他分布式方案。
真正应该避免的是把所有搜索问题都归结为“LIKE太慢”。LIKE 'abc%'和LIKE '%abc%'完全不是同一种性能问题;单租户查询和跨租户查询也不是同一种数据访问模式;几百万条记录的数据库和数亿条记录的搜索平台,更不应该采用同一套架构。
SaaS系统最可靠的优化路径通常不是一开始就堆砌Redis、Kafka、Elasticsearch、分库分表,而是先让数据库知道自己究竟应该查什么。把搜索条件、租户范围、排序方式和返回字段设计清楚,再通过执行计划和真实负载验证索引效果。如果MySQL仍然无法满足业务需求,再引入专门的搜索系统。搜索架构真正成熟的标志,不是组件数量越来越多,而是每一个组件都有明确的工作边界。
MySQL LIKE 搜索越来越慢,SaaS 系统应该怎样处理关键词查询
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP