SaaS系统里的搜索功能经常同时面对租户、状态、姓名、邮箱、订单号、时间范围和排序等条件。很多开发者遇到查询变慢,第一反应就是“再加一个索引”。但索引并不是越多越好,也不是把所有搜索字段塞进一个超长复合索引就能解决问题。真正有效的索引设计,应该从实际SQL、数据分布、查询频率、排序方式以及写入成本出发。
首先要区分单字段查询和多字段组合查询。如果用户经常通过邮箱、手机号或者订单号精确查找记录,而且这些字段具有较高选择性,可以分别建立单列索引。例如:
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_phone ON users(phone);
如果邮箱在业务上必须唯一,则更应该考虑UNIQUE约束,而不是仅仅建立普通索引。
对于普通的短字符串字段,通常没有必要使用前缀索引。前缀索引更适合非常长的字符串,而且它牺牲了一部分索引中的区分能力。比如一个邮箱字段只有几百字节以内,是否需要前缀索引应该根据实际字段长度、字符集以及查询特征测试,而不能简单规定“前100字符”。
真正需要认真设计的是复合索引。例如SaaS用户列表可能经常执行:
SELECT id, name, email, created_at
FROM users
WHERE tenant_id = ?
AND status = ?
AND created_at >= ?
ORDER BY created_at DESC
LIMIT 50;
这种查询就不能分别建立tenant_id、status和created_at三个索引,然后期待MySQL自动把它们组合成最优方案。一个针对查询模式设计的复合索引可能更加合适,例如:
CREATE INDEX idx_users_tenant_status_created
ON users(tenant_id, status, created_at);
这里最重要的不是一句“高选择性字段必须放最左边”,而是理解复合索引的左侧前缀和查询条件之间的关系。tenant_id如果是多租户系统中的常用过滤条件,通常具有非常强的业务意义,因为几乎所有查询首先都应该限定租户。之后再根据status、时间范围、排序和其他条件决定顺序。
复合索引的字段顺序应该由真实查询模式决定。例如一个查询经常是:
WHERE tenant_id = ?
AND status = ?
AND created_at >= ?
另一个查询却经常是:
WHERE tenant_id = ?
AND email = ?
那么一个(tenant_id,status,created_at)索引并不能自动替代(tenant_id,email)。不要试图设计一个“万能索引”,而应该统计最重要的SQL访问模式。
范围条件也会影响后续索引列的利用方式。比如:
WHERE tenant_id = ?
AND status = ?
AND created_at >= ?
在created_at进入范围扫描后,后续字段是否还能充分用于定位、排序,需要结合MySQL版本和实际执行计划判断。因此复合索引不能只靠背诵“等值列在前、范围列在后”解决所有问题,最终还是应该用EXPLAIN或EXPLAIN ANALYZE验证。
对于排序,同样不能简单地说“排序字段一定要放进索引”。如果查询是:
WHERE tenant_id = ?
ORDER BY created_at DESC
LIMIT 50;
那么:
CREATE INDEX idx_users_tenant_created
ON users(tenant_id, created_at);
就可能同时帮助过滤和获取有序结果,避免为了排序读取大量候选行。但如果查询条件、排序方向和索引结构不匹配,MySQL仍然可能需要额外排序。
覆盖索引也是常见优化手段。假设查询:
SELECT name, email
FROM users
WHERE status = 1
AND create_time > '2023-01-01';
可以考虑:
CREATE INDEX idx_users_status_time_name_email
ON users(status, create_time, name, email);
这样索引中包含查询需要的数据,某些情况下MySQL可以减少回表。但覆盖索引不是“字段越多越好”。索引越宽,占用空间越大,写入和更新成本也越高。InnoDB的二级索引还包含主键值,因此设计覆盖索引时应该把这一点考虑进去。
模糊搜索是另一个容易产生误区的地方。普通B+Tree索引通常无法有效处理:
WHERE name LIKE '%john%'
因为通配符位于开头,数据库很难利用索引的有序结构快速定位起点。但:
WHERE name LIKE 'john%'
在合适的字符集、排序规则和查询条件下则可能利用普通索引。
如果业务真正需要“任意位置包含关键词”、自然语言搜索或者大量文本搜索,就不应该强行依靠普通B+Tree索引。可以根据业务规模考虑MySQL FULLTEXT,或者专门的搜索系统。FULLTEXT和LIKE '%keyword%'解决的是不同类型的问题,也不能简单认为全文索引就是普通模糊搜索的升级版。
日期查询同样需要注意写法。下面这种查询通常比对索引列直接套函数更容易利用索引:
WHERE create_time >= '2023-01-01'
AND create_time < '2024-01-01'
而:
WHERE YEAR(create_time) = 2023
会让普通索引的利用变得困难。实际项目中应该优先把范围计算放到应用层或者查询参数中,让数据库直接对原始时间列进行范围查找。
类型也应该保持一致。比如整数主键最好使用整数参数,而不是让数据库在比较过程中进行隐式类型转换。虽然“id='123'一定导致索引失效”是错误的绝对说法,但保持应用参数类型与数据库字段类型一致,通常更容易获得可预测的执行计划。
多租户SaaS还必须把性能问题和数据隔离问题分开考虑。比如:
CREATE INDEX idx_user_tenant_email
ON users(tenant_id, email);
可以帮助典型的:
WHERE tenant_id = ?
AND email = ?
查询,但真正的租户隔离不能依靠索引完成。应用层、Repository、ORM、后台任务、缓存和消息队列都必须正确携带租户上下文。索引只能帮助数据库执行查询,不能替代权限控制。
对于大型SaaS系统,还要特别注意“超级租户”问题。如果某个租户拥有几千万条记录,而其他租户只有几千条,那么即使所有表都有tenant_id索引,这个大租户的查询仍然可能成为热点。此时应该进一步分析数据分布、分页方式、查询频率、冷热数据以及是否需要分区或分片,而不是单纯继续增加索引。
索引数量也没有一个“超过10个就一定不好”的硬性规则。一个复杂业务表可能有多个不同查询路径,因此拥有超过10个索引完全可能是合理的;反过来,一个写入非常频繁的表即使只有几个索引,也可能已经承担很高的维护成本。每增加一个索引,INSERT、DELETE以及涉及索引列的UPDATE通常都需要维护更多B+Tree结构,同时增加磁盘空间和Buffer Pool压力。
因此判断索引是否合理,应该观察真实工作负载。可以使用:
SHOW INDEX FROM users;
了解当前索引结构,再用:
EXPLAIN
SELECT ...
检查优化器选择了什么访问路径。对于需要更深入验证的查询,可以使用:
EXPLAIN ANALYZE
SELECT ...;
EXPLAIN主要告诉你优化器计划怎样执行,而EXPLAIN ANALYZE会实际执行查询并提供运行过程中的统计信息。两者结合起来,比单纯看到一个key字段更加有价值。
还应该结合慢查询日志、数据库CPU、磁盘I/O、锁等待、Buffer Pool命中情况以及实际响应时间判断问题。一个查询即使使用了索引,也不代表它一定快;反过来,一个查询显示全表扫描,也不一定就是故障。如果表只有几百行,扫描整张表可能比走索引更加合理。
分页也是SaaS搜索经常被忽略的性能问题。例如:
LIMIT 50 OFFSET 500000;
即使有合适的索引,大OFFSET仍然可能让数据库扫描并跳过大量记录。对于大型列表,可以考虑基于索引的Keyset Pagination,例如使用上一页最后一条记录的created_at和主键继续向后查询。
最终,SaaS搜索索引设计应该从SQL开始,而不是从“我要给哪些字段加索引”开始。先收集最重要的查询,再分析WHERE、JOIN、ORDER BY和LIMIT的组合关系,然后设计单列或复合索引,最后用真实数据和执行计划验证。对于多租户系统,tenant_id往往是重要的索引组成部分,但它只是整个设计的一部分。
一个成熟的MySQL索引策略,不是建立一堆看起来漂亮的索引,而是让最常执行、最影响用户体验的查询能够以合理的代价完成,同时控制写入、存储和维护成本。索引真正解决的是访问路径问题,而不是所有数据库性能问题。SaaS系统到了数据量和并发量较大的阶段,查询设计、事务、锁、缓存、归档、分区和架构本身,往往需要与索引一起考虑。
SaaS 搜索多个字段时,MySQL 索引应该怎样设计
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP