MySQL中的LIKE查询看起来非常简单,但当文章数量从几万增长到几十万甚至更多以后,搜索速度可能出现明显下降。尤其是新闻网站、博客和内容管理系统,经常需要让用户按照标题、正文或关键词进行搜索,这时候一个简单的LIKE '%关键词%'可能让数据库扫描大量数据。
但LIKE慢并不意味着LIKE本身有问题。真正需要分析的是查询模式、索引是否能够参与、需要检查多少行、返回多少数据,以及搜索字段究竟是结构化字段还是长文本。文章搜索如果从一开始就把所有需求都交给LIKE,数据增长以后往往才发现数据库承担了本来应该由搜索系统完成的工作。
一、为什么LIKE查询可能很慢
最典型的情况是:
SELECT id, title
FROM articles
WHERE title LIKE '%人工智能%';
如果普通B-Tree索引建立在title上,%位于搜索字符串最前面时,索引通常无法利用字符串开头的有序性来快速确定搜索范围。数据库往往需要检查大量索引记录甚至大量表记录,因此数据量越大,延迟越容易上升。
而下面这种查询则完全不同:
SELECT id, title
FROM articles
WHERE title LIKE '人工智能%';
因为搜索条件从固定字符串开始,B-Tree索引在适当情况下可以利用索引的有序结构定位到相应范围。
因此,“LIKE不能使用索引”是错误的。更准确的说法是:LIKE是否能够有效利用普通索引,与通配符的位置以及具体执行计划有关。
还需要注意,LIKE '%abc%'也不等于数据库一定进行“全表扫描”。MySQL最终采用什么执行计划,需要通过EXPLAIN实际确认。如果存在其他过滤条件、覆盖索引或者优化器认为某个索引仍然具有成本优势,执行计划可能与简单的全表扫描不同。
二、索引不是解决全文搜索的万能工具
很多开发人员看到查询慢,第一反应就是给字段增加INDEX。
对于标题这样的结构化字段,普通索引非常重要。例如网站经常按照分类、发布时间和状态筛选文章:
WHERE category_id = ?
AND publish_status = 1
ORDER BY created_at DESC
LIMIT 20
这类查询可以通过合理的组合索引大幅减少需要检查的数据。
但如果需求变成:
WHERE title LIKE '%人工智能%'
普通B-Tree索引就不是理想的全文搜索方案。
TEXT字段也不是“不能建立索引”。MySQL可以对TEXT建立前缀索引,也可以在支持的存储引擎和字符集条件下建立FULLTEXT全文索引。普通索引与全文索引解决的是不同问题,不能把两者混为一谈。
更重要的是,索引碎片也不能简单解释成LIKE突然变慢的主要原因。InnoDB会通过B-Tree页分裂、页合并以及后台维护机制管理索引结构。真正遇到性能下降时,应该先通过EXPLAIN、实际执行时间、扫描行数和服务器I/O判断原因,而不是看到查询变慢就执行OPTIMIZE TABLE。
三、先用EXPLAIN确认数据库到底做了什么
遇到搜索速度下降,第一步通常不是改SQL,而是查看执行计划。
例如:
EXPLAIN
SELECT id, title, created_at
FROM articles
WHERE title LIKE '%人工智能%'
ORDER BY created_at DESC
LIMIT 20;
重点观察type、possible_keys、key、rows、filtered以及实际查询需要检查的数据量。
如果条件非常简单而表中已经有数十万甚至数百万条记录,LIKE '%关键词%'需要检查大量记录并不奇怪。
MySQL 8还可以使用EXPLAIN ANALYZE观察实际执行过程和实际耗时,这对于判断优化器估算与真实执行之间的差异尤其有价值。
因此,性能优化应该建立在实际执行计划上,而不是根据某一个SQL写法直接判断数据库一定采用了某种执行方式。
四、文章搜索应该区分“前缀搜索”和“全文搜索”
文章网站通常存在两种完全不同的搜索需求。
第一种是前缀搜索,例如用户输入“人工”,希望找到标题以“人工”开头的文章。这类需求可以考虑:
WHERE title LIKE '人工%'
并配合适当的普通索引。
第二种是全文搜索,例如用户输入“人工智能 芯片 出口”,希望搜索标题和正文中与这些词相关的文章。这已经不是普通字符串前缀匹配,而是全文检索问题。
这时候应该考虑MySQL FULLTEXT,或者独立搜索引擎。
MySQL 8的InnoDB支持FULLTEXT索引。例如:
ALTER TABLE articles
ADD FULLTEXT KEY ft_title_content (title, content);
然后可以使用:
SELECT id, title, created_at
FROM articles
WHERE MATCH(title, content)
AGAINST ('人工智能' IN NATURAL LANGUAGE MODE);
中文搜索需要特别注意分词问题。MySQL可以使用ngram parser处理中文等没有明显空格分隔的语言,但具体效果取决于ngram配置、文本特点和搜索需求。
因此,如果网站规模不大,MySQL FULLTEXT可能已经能够满足需求,没有必要为了“专业”两个字就马上部署Elasticsearch。
五、什么时候应该使用Elasticsearch
当搜索需求逐渐变复杂,例如需要中文分词、同义词、关键词高亮、搜索联想、复杂相关度排序、拼音搜索、错别字处理、字段权重、聚合统计等,独立搜索引擎的价值就会越来越高。
典型架构可以是:
MySQL
↓
保存文章和业务数据
↓
搜索索引同步
↓
Elasticsearch
↓
用户搜索
MySQL负责可靠保存业务数据,搜索引擎负责高效检索。
但这里存在一个重要原则:搜索引擎通常不是业务数据库的替代品。文章正文、作者、发布时间、发布状态等核心数据仍然应该有明确的权威来源。搜索索引可以重建,因此不能把搜索索引当成唯一数据源。
对于新闻网站,还需要处理MySQL与搜索索引之间的数据同步。如果文章更新以后索引没有同步,用户看到的搜索结果就可能与文章页面实际内容不一致。
六、不要把缓存当成搜索性能的第一解决方案
Redis可以缓存热门搜索结果,但缓存并不能解决一个设计不合理的搜索SQL。
例如:
SELECT *
FROM articles
WHERE title LIKE '%abc%'
OR content LIKE '%abc%';
如果每次都需要扫描大量文章,那么增加Redis之前,首先应该判断是否应该使用FULLTEXT或搜索引擎。
缓存更适合解决另外一种问题:某些搜索词非常热门,而且搜索结果短时间内变化不大。此时可以缓存关键词对应的文章ID、排序结果或者分页结果,减少重复查询。
缓存还必须考虑失效问题。文章发布、修改、删除以后,旧搜索结果不能长期保留。
七、分页不能只用LIMIT 100000
传统分页经常写成:
LIMIT 100000, 20
当偏移量越来越大时,数据库可能需要先找到并跳过大量记录,再返回最后的20条,因此深度分页可能越来越慢。
如果搜索结果按照稳定字段排序,可以考虑基于游标或seek的分页方式。例如按照created_at和id组成稳定排序键:
WHERE created_at < ?
OR (created_at = ? AND id < ?)
ORDER BY created_at DESC, id DESC
LIMIT 20;
这种方式尤其适合新闻列表和时间线。
但是,全文搜索中的相关度排序更加复杂,不能简单认为所有搜索结果都应该使用同一种分页方式。具体方案需要结合搜索引擎、排序字段和用户体验设计。
八、标题搜索与正文搜索不要完全等价
新闻网站的标题通常比正文更适合直接搜索和排序。
例如用户搜索“芯片出口”,标题中出现完整关键词的文章,通常应该比正文只出现一次相关词的文章更靠前。
因此可以让标题拥有更高的搜索权重,同时使用发布时间作为重要的辅助排序因素。
例如搜索结果可以综合:
相关度
+
标题匹配程度
+
发布时间
+
文章状态
而不是单纯按照点击量排序。
点击量可以作为辅助信号,但如果把点击量权重设置过高,搜索结果可能长期被旧的热门文章占据,新发布但高度相关的内容反而难以出现。
九、不要为了搜索而给所有字段建立大量索引
索引本身也有成本。
每增加一个索引,INSERT、UPDATE和DELETE都可能需要维护更多索引结构,同时占用磁盘空间和内存缓存。
因此,正确的做法不是“搜索什么字段就全部建立INDEX”,而是根据实际SQL、过滤条件、排序方式和数据分布设计索引。
例如新闻网站常见查询:
WHERE publish_status = 1
ORDER BY created_at DESC
LIMIT 20
与:
WHERE category_id = ?
AND publish_status = 1
ORDER BY created_at DESC
LIMIT 20
需要考虑的索引策略就不完全相同。
索引设计应该围绕真实查询模式,而不是围绕数据库字段数量。
十、MySQL配置也不能替代正确的搜索设计
innodb_buffer_pool_size当然重要。对于以InnoDB为主的数据库服务器,合理的Buffer Pool配置能够减少磁盘I/O。
SSD、足够的内存以及合理的数据库参数同样可以改善整体性能。
但这些优化不能把一个需要扫描大量文章的全文搜索SQL变成真正意义上的高效全文检索。
另外,MySQL 8.0已经删除Query Cache,因此不应该继续按照旧版本教程去配置query_cache_type、query_cache_size等参数解决搜索性能问题。
同样,分区表也不是“数据超过100万行就必须使用”的性能按钮。是否采用分区,应根据数据生命周期、分区键、查询模式以及维护需求判断。
十一、一个新闻网站应该怎样设计搜索
如果网站只有几万篇文章,搜索需求主要是标题和正文关键词,可以优先考虑MySQL FULLTEXT,并通过EXPLAIN ANALYZE观察实际性能。
如果文章数量继续增长,而且搜索需要中文分词、相关度排序、关键词高亮、搜索建议、同义词和复杂过滤,那么可以把搜索功能逐步迁移到Elasticsearch等专业搜索系统。
一个比较合理的结构是:
用户输入关键词
↓
搜索接口
↓
判断搜索类型
↓
前缀搜索 → MySQL索引
↓
全文搜索 → MySQL FULLTEXT / Elasticsearch
↓
得到文章ID和相关度
↓
根据发布状态、权限等业务条件过滤
↓
返回标题、摘要、时间和链接
对于新闻网站,还应该把publish_status、发布时间、分类等业务条件与搜索条件区分开。搜索系统负责“找到相关内容”,MySQL负责业务数据的一致性,两者各自承担擅长的工作。
十二、真正有效的优化顺序
文章搜索出现性能问题时,可以按照以下顺序排查:
首先确认SQL和真实执行计划;其次确认LIKE模式是否导致普通索引无法有效利用;然后检查过滤条件、排序和分页是否合理;再决定MySQL FULLTEXT是否已经足够;如果搜索需求继续复杂,再考虑Elasticsearch等专业搜索系统;最后才是针对缓存、服务器内存、SSD和数据库参数进行进一步优化。
最重要的是不要把所有搜索问题都归结为“给字段加索引”。
普通B-Tree索引擅长按照有序键快速定位数据,FULLTEXT擅长全文检索,而Elasticsearch等搜索系统则可以进一步处理分词、相关度、联想和复杂搜索逻辑。三者解决的是不同层次的问题。
对于一个不断增长的新闻网站,最稳妥的路线通常不是一开始就堆叠复杂技术,而是先让MySQL承担它擅长的结构化查询,在搜索需求真正超过数据库能力以后,再引入专业搜索系统。这样既能控制系统复杂度,也能避免为了一个简单的文章搜索功能提前维护一整套不必要的基础设施。
MySQL LIKE 查询为什么可能很慢?文章搜索功能应该怎样设计
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP