MySQL join了十几张表,有哪些优化思路?

这种确实是一个很麻烦的技术难题,因为这里还牵扯到了分页。

就是说,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