这种确实是一个很麻烦的技术难题,因为这里还牵扯到了分页。
就是说,join了十多个表时,还带上非常多的查询条件,且最终按照这些条件,做最终的分页。
这个会给优化带来很大的麻烦的,不过也不是没有办法的。由于你没有提供sql,因此我只能基于你说的场景,构建一下sql,然后给出解决方案,供你参考一下。
下面这些解决方案,在我工作这十几年来,在不同的公司都实施过,且有效的。
为了说清楚方案,我按你的场景先建10张表
就按订单业务来说明,注意我只列核心字段哈,具体的表结构如下:
t_order 订单主表:id(PK), order_no(UK), store_id, user_id, order_status, order_type, create_time, total_amount
t_order_item 订单明细表:id(PK), order_id, sku_id, qty, item_amount
t_store 门店表(维表):id(PK), store_name, city_id, store_type
t_city 城市表(维表):id(PK), city_name
t_user 会员表(维表):id(PK), mobile, nickname, register_time
t_user_level 会员等级表(维表/关联表):user_id(PK), level_code, level_name
t_payment 支付表:id(PK), order_id, pay_status, pay_channel, pay_time, paid_amount
t_promotion_order 订单优惠汇总:id(PK), order_id, promo_type, discount_amount
t_invoice 发票表:id(PK), order_id, invoice_status, invoice_time
t_refund 退款/售后表:id(PK), order_id, refund_status, refund_amount, refund_time
然后假设在WEB订单管理后台中,界面上的查询条件是:
- 时间范围(必填)
- 门店(可选)
- 订单状态(可选)
- 城市(可选)
- 手机号(可选)
- 支付方式(可选)
- 发票状态(可选)
- 退款状态(可选)
解决方案一:延迟关联
就是说,先在主表里通过索引快速圈定这 ID,以便先走【覆盖索引】,而不是先去 Join。等拿到了 ID,再回过头来 Join 这 10 张表取详情。
当然这个是有适用场景的:
就是筛选条件主要集中在主表(如:只按时间、订单状态筛选)。
我们假设每页查询20条,那么具体的优化sql如下:
sql
SELECT o.order_no, u.nickname, s.store_name,xxxxxx,.....
FROM t_order o
JOIN (
-- 【关键】只在主表走覆盖索引,快速定位 20 个 ID
SELECT id FROM t_order
WHERE create_time BETWEEN '2026-01-01' AND '2026-01-31'
AND order_status = 1
ORDER BY create_time DESC
LIMIT 100000, 20
) AS batch ON o.id = batch.id
-- 接下来才去join
LEFT JOIN t_user u ON o.user_id = u.id
LEFT JOIN t_store s ON o.store_id = s.id ...
这种优化还有一个专业的说法,叫【主键点查】,可以将数万次的大表 Join 变成了 20 次的查询。
那如果管理后台的查询条件没有在主表里呢? 那怎么办?
这个真的很麻烦,因为上面提到的延迟关联做法失效了,我们必须得join后才能过滤了。一般我是用两种办法来处理的。
第一种是用冗余字段的方式,比如说按照城市查询的和支付方式查询的。那么就可以直接在订单主表里,冗余这两个字段,这样就不需要join了。
当然,这个是要去刷历史数据的,需要用一个定时任务,夜深人静的,慢慢的刷。
如果你不想加冗余字段,那只能用宽表了,但是这个是有不少的开发工作量的,且分还两种情况。
- 第一种是,宽表依然用的是mysql;
- 第二种是用es来构建宽表;
用mysql构建宽表
如果你不想贸贸然引入ES,那使用mysql来构建宽表也是一个可行的解决方案。具体的步骤如下:
- 在从库或专门的查询库上,建立一张 t_order_wide_table;
- 将 10 张表的所有高频筛选字段(城市、手机号、支付状态、发票状态)全部平铺到这一张单表里。
- 通过监听Binlog,或者跑定时任务,将10张表的变化合并更新到这张宽表。
这样就就可以用如下的sql来做查询了:
sql
-- 优化后的宽表查询:无 Join,全索引覆盖
SELECT * FROM t_order_wide_table
WHERE city_id = 12
AND mobile = '13800138000'
AND pay_channel = 'alipay'
ORDER BY create_time DESC
LIMIT 100000, 20;
这套方案的优缺点如下:
优点:查询性能提升一个量级。不需要引入 ES 那套复杂的运维体系,团队开发上手极快。
缺点:宽表需要额外的存储空间,处理模糊查询,性能依然不行。
用es构建宽表
这个方案是大多数互联网大厂采用的方案,当时我们的订单管理后台,商品管理后台的数据查询,清一色的都是从ES里去查询的。
是一个广泛的常用的解决方案。我建议,如果有条件,就直接用es来构建宽表,因为产品需求不断地迭代,管理后台的查询条件也在一直加,且还需要支持多个字段都模糊查询的场景,这个mysql的B+树,真的是搞不定的。
假设你的台账需要全字段模糊搜索(如商品名、备注、收货地址)或者动态组合筛选(多个条件随意选),MySQL 的 B+ 树索引,真的,无能为力。
如果用es的话,关键的步骤如下:
- 监听 MySQL 的 Binlog,实时将这 10 张表的数据聚合;
- 在 ES 中建立一个 order_index,每一条记录就是一个包含 10 张表信息的 JSON;
- 所有的台账查询直接走 ES,MySQL 只负责核心业务的事务写入。
我总结一下如何回答面试官的这个问题
1.原则:OLTP(交易)与 OLAP(分析)是要分离的。复杂的统计台账不该由核心交易库去承担;
2.短期(SQL级):分离筛选与抓取。使用延迟关联,先瘦身 Join 拿 ID,再回表取详情;
3.中期(字段冗余):将高频的跨表筛选字段(如城市、状态)冗余到主表,消除 Join。
4.长期(架构级):引入搜索引擎(ES)。将多表关系转化为文档宽表,彻底解决任意维度检索和深分页问题。
你自己心里一定要有个谱,mysql这种关系型数据库,就是存在局限性的,必要的时候,的确是需要做【架构级别】的优化的。
好了,如果你觉得这篇回答能帮助你,请顺手点赞和收藏,如果你有疑问的,欢迎在评论区留言,我们一起讨论一下。
最近在知乎出了
- 「应付6000万会员的秒杀系统专栏」
- 「几亿用户,百万并发的C端商品系统实战」
- 「技术团队DDD领域驱动设计三年落地实战」
- 「应付亿级用户规模的支付系统代码实战」
- 「应付亿级用户的会员体系代码实战」
- 「我的项目管理实战手记:10个真实主导项目,还原实战现场」
专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:
- 老码头的技术浮生录
它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。「另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。」
当前星球里免费看的专栏是:
- 「应付6000万会员的秒杀系统专栏」
- 「几亿用户,百万并发的C端商品系统实战」
- 「技术团队DDD领域驱动设计三年落地实战」
- 「应付亿级用户规模的支付系统代码实战」
- 「应付亿级用户的会员体系代码实战」
- 「我的项目管理实战手记:10个真实主导项目,还原实战现场」
知识星球内后续将推出20+个付费专栏,覆盖电商全链路:
| 选购线 | 用户会员营销线 | 中后台 |
|---|---|---|
| 购物车服务 | 营销系统 | 订单系统 |
| 商品服务 | 用户系统 | 支付系统 |
| 菜单服务 | 结算服务 |
从前台选购到中后台结算,星球成员全部免费,后续新增也不额外收费。
我的知乎账号:
- SamDeepThinking