小表驱动大表更快吗,不是

背景

head头表(5000),line行表(15万),导出数据包含头和行,一对多。

以行表为维度导出15万数据。

sql

如下两个sql查询,有如下差异

驱动方式:第一个大表驱动小表,第二个反之

第一个自己比较:根据ID排序,和不排序,explain分析

sql 复制代码
explain
        select
        head.code,
        head.company_code,
        head.company_name,
        head.expense_date,
        head.data_source_num,
        head.data_source,
        head.prepared_by,
        head.status,

        line.asset_number,
        line.tag_number,
        line.asset_name,
        line.asset_category_1,
        line.asset_category_2,
        line.retired_units,
        line.retired_date,
        line.retirement_type,
        line.retirement_cost,
        line.disposal_amount,
        line.comment
        from
         asset_fa_disposal_line line
        left join asset_fa_disposal head on line.parent_code = head.code
        order by line.id desc
        limit 0,1000
      
        
    无排序    
    SIMPLE	line		ALL					141466	100.0	
1	SIMPLE	head		eq_ref	uniq_code	uniq_code	194	fssc_backend_asset.line.parent_code	1	100.0	

    有排序
1	SIMPLE	line		index		PRIMARY	8		141466	100.0	Backward index scan
1	SIMPLE	head		eq_ref	uniq_code	uniq_code	194	fssc_backend_asset.line.parent_code	1	100.0	

--         asset_fa_disposal head
--         left join asset_fa_disposal_line line on head.code = line.parent_code
     --   order by line.id desc
        

explain
        select
        head.code,
        head.company_code,
        head.company_name,
        head.expense_date,
        head.data_source_num,
        head.data_source,
        head.prepared_by,
        head.status,

        line.asset_number,
        line.tag_number,
        line.asset_name,
        line.asset_category_1,
        line.asset_category_2,
        line.retired_units,
        line.retired_date,
        line.retirement_type,
        line.retirement_cost,
        line.disposal_amount,
        line.comment
        from
        asset_fa_disposal head
        left join asset_fa_disposal_line line on head.code = line.parent_code
        order by line.id desc
         limit 0,1000
        
        
    无排序     
    SIMPLE	head		ALL					4832	100.0	
1	SIMPLE	line		ref	disposal_line_parent_code	disposal_line_parent_code	194	fssc_backend_asset.head.code	31	100.0	

    有排序
1	SIMPLE	head		ALL					4832	100.0	Using temporary; Using filesort
1	SIMPLE	line		ref	disposal_line_parent_code	disposal_line_parent_code	194	fssc_backend_asset.head.code	31	100.0

问题

1,不是小表驱动大表吗,为什么这里大表驱动小表效率更高

2,针对第一个sql,line表驱动时,排序好像比不排序,效率更高,line表不排序type=all,排序反而是index为什么

3,数据库中总共有15万数据,需要导出,每次查询1000,分页查询,大表驱动小表和小表驱动大表,影响最终的分页结果吗,比如导出数量有影响吗

4,采用第一个sql,又想不遗漏head数据(head无line关联数据),需要保证正确导出分页,怎么使用union优化

回答

1

确实,在大多数JOIN优化场景中,我们通常推荐"小表驱动大表"的原则,但您这个案例却出现了相反的情况。让我详细解释为什么这里"大表驱动小表"反而更快:

为什么这个特例中大表驱动小表更快

  1. LEFT JOIN的特殊性

    • 您使用的是LEFT JOIN,不是INNER JOIN

    • LEFT JOIN的逻辑是:必须返回左表的所有行,无论右表是否有匹配

    • 第一个查询(大表驱动):

      sql

      复制

      下载

      复制代码
      FROM asset_fa_disposal_line(line) LEFT JOIN asset_fa_disposal(head)

      优化器知道必须返回line表所有141,466行,所以选择全表扫描line,然后对每行快速查找head

  2. 索引效率差异

    • head.code有uniq_code索引(唯一索引),查找速度极快(O(1)复杂度)

    • line.parent_code有普通索引,查找速度稍慢

    • 大表驱动时:141,466次唯一索引查找

    • 小表驱动时:4,832次普通索引查找,每次返回约31行(总共约150,000行)

  3. 数据分布特点

    • 每个head.code平均对应约31个line记录(141466/4832≈29.3)

    • 这种"一对多"关系使得从小表出发会导致更多中间结果

  4. 执行成本对比

    • 大表驱动:141,466次高效唯一索引查找

    • 小表驱动:4,832次范围查找,每次返回约31行,需要处理更多数据页

何时"大表驱动小表"更优

  1. LEFT JOIN且右表有极高效索引

  2. 右表的查找条件比左表的选择性更好

  3. 数据关系是一对多且"多"的一方数据量极大

  4. 查询需要返回左表大多数行时(如无WHERE条件)

验证方法

您可以尝试以下实验验证:

sql

复制

下载

复制代码
-- 强制小表驱动大表
SELECT /*+ JOIN_ORDER(head, line) */ ... FROM head LEFT JOIN line ...

-- 强制大表驱动小表 
SELECT /*+ JOIN_ORDER(line, head) */ ... FROM line LEFT JOIN head ...

总结

"小表驱动大表"是通用原则,但在LEFT JOIN、特殊索引结构和特定数据分布下,可能会出现大表驱动更优的情况。优化器根据成本估算选择了更适合当前表结构和数据分布的执行计划。

2

您观察到了一个非常有意思的现象,这涉及到MySQL优化器的工作机制。让我详细解释为什么在第一种查询中(line表驱动),加上ORDER BY line.id DESC后执行效率反而更高:

核心原因:MySQL的"排序驱动优化"

当查询包含ORDER BY且使用LIMIT时,MySQL优化器会尝试"优先排序后连接"的策略:

  1. 无排序时(性能较低)

    • type=ALL表示全表扫描line表(141,466行)

    • 对每一行再通过索引查找head表

    • 最后才应用LIMIT 1000

  2. 有排序时(性能更高)

    • type=index且显示"Backward index scan"表示优化器选择:

      • 直接按id索引的逆序扫描(从最大id开始)

      • 只需扫描1000行满足LIMIT的数据

      • 然后对这1000行做关联查询

    • 避免了全表扫描

关键技术点

  1. Backward index scan(反向索引扫描)

    • InnoDB索引默认是升序排列的

    • 当使用ORDER BY id DESC时,MySQL 8.0+可以智能地从索引尾部开始反向扫描

    • 只需读取满足LIMIT的行数(1000行)即可停止

  2. LIMIT的优化作用

    sql

    复制

    下载

    复制代码
    ORDER BY line.id DESC LIMIT 1000
    • 告诉优化器"我只需要1000条排好序的记录"

    • 优化器会优先使用id索引直接定位到符合条件的记录

  3. 索引覆盖优势

    • 虽然您的查询选择了很多列,但排序只依赖line.id

    • id是主键,索引结构本身就包含全部排序信息

执行流程对比

无排序流程:

复制

下载

复制代码
1. 全表扫描line表(141,466行)
2. 对每行查找head表
3. 组合所有结果
4. 应用LIMIT 1000

有排序流程:

复制

下载

复制代码
1. 从line表的id索引尾部开始反向扫描
2. 扫描1000行后停止(不读全表)
3. 对这1000行查找head表
4. 返回结果

为什么第二种方式(head驱动)没有这种优化

当head表驱动时:

  • 排序字段(line.id)不在驱动表

  • MySQL必须先完成全部连接操作

  • 生成包含所有字段的临时结果集(约150,000行)

  • 然后排序这个巨大临时表

  • 最后应用LIMIT

最佳实践建议

  1. 排序字段应尽量来自驱动表

  2. 确保排序字段有索引如您案例中的line.id

  3. 结合LIMIT使用ORDER BY可以触发这种优化

  4. 对于分页查询,这种模式性能最好

这种优化特别适合:

  • 大型事实表关联小型维度表

  • 需要最新/最旧记录的查询

  • 分页展示场景

您的案例完美展示了当表结构设计合理(line.id有主键索引)时,MySQL优化器能够发挥的强大优化能力。

3

分页查询驱动方式对导出结果的影响分析

在您的场景中(15万数据,每次查询1000条分页导出),驱动方式的选择确实会影响最终导出结果,但影响的性质取决于您的具体需求。

两种驱动方式对结果的影响

1. 大表驱动小表(line LEFT JOIN head)
  • 结果特点

    • 保证line表的所有记录都会出现在结果中

    • 如果某些line记录没有对应的head记录,head相关字段为NULL

    • 结果数量 = line表的记录数(15万)

2. 小表驱动大表(head LEFT JOIN line)
  • 结果特点

    • 保证head表的所有记录都会出现在结果中

    • 如果某些head记录没有对应的line记录,line相关字段为NULL

    • 结果数量 ≥ head表的记录数(因为有1对多关系)

关键差异点

  1. 结果集基数不同

    • 大表驱动:结果行数=line表行数(15万)

    • 小表驱动:结果行数≈head表行数×平均关联数(4832×31≈15万)

  2. NULL记录出现位置不同

    • 大表驱动:可能出现head字段为NULL的记录

    • 小表驱动:可能出现line字段为NULL的记录

  3. 分页边界问题

    • 两种方式的分页切分点可能不同,因为记录排序不同

    • 特别是当使用ORDER BY时,排序字段的位置会影响分页边界

导出数量是否受影响?

  • 如果只是简单导出所有关联数据:两种方式导出的总记录数应该相同(15万),因为您描述的是1对多关系且数据完整

  • 如果有NULL记录过滤:导出数量可能会有差异

  • 如果使用ORDER BY:分页边界不同可能导致某些记录在不同页中出现

实际建议

  1. 业务需求决定驱动方式

    • 如果需要确保不遗漏任何line记录 → 使用line驱动

    • 如果需要确保不遗漏任何head记录 → 使用head驱动

  2. 导出一致性保障

    sql

    复制

    下载

    复制代码
    -- 推荐使用游标方式而非分页导出,确保数据一致性
    DECLARE cur CURSOR FOR 
    SELECT ... FROM line LEFT JOIN head ... ORDER BY line.id;
    
    -- 或者使用基于ID的范围查询
    SELECT ... FROM line LEFT JOIN head 
    WHERE line.id > last_id ORDER BY line.id LIMIT 1000;
  3. 性能考虑

    • 如您之前发现的,line驱动+ORDER BY line.id性能更好

    • 对于大批量导出,建议使用这种高效方式

  4. 导出验证

    • 可以在导出完成后检查两种方式的记录数是否匹配预期

    • 特别检查边界条件记录是否都被包含

总结:驱动方式主要影响的是结果集的组成和顺序,在1对多关系且数据完整的情况下,总导出量应该相同,但具体哪些记录出现在哪些分页中可能会有差异。

4

sql 复制代码
SELECT * FROM (
    -- 有关联的记录
    SELECT 
        /* 字段列表 */,
        1 as record_type,
        line.id as sort_key
    FROM asset_fa_disposal head
    JOIN asset_fa_disposal_line line ON head.code = line.parent_code
    
    UNION ALL
    
    -- 无关联的记录
    SELECT 
        /* 字段列表 */,
        2 as record_type,
        head.code as sort_key
    FROM asset_fa_disposal head
    WHERE NOT EXISTS (
        SELECT 1 FROM asset_fa_disposal_line 
        WHERE parent_code = head.code
    )
) combined
ORDER BY record_type, sort_key DESC
LIMIT 1000 OFFSET 0;
相关推荐
不穿铠甲的穿山甲1 分钟前
mysql-mysql源码本地调试
数据库·mysql
篱笆院的狗1 小时前
如何使用 Redis 快速实现排行榜?
数据库·redis·缓存
开利网络1 小时前
数据资产化浪潮下,企业如何构建去中心化商业新生态?
大数据·数据库·人工智能·信息可视化·重构
小陈又菜3 小时前
Real SQL Programming
数据库·sql
%d%d23 小时前
Redis 插入中文乱码键
数据库·redis·缓存
goldfishsky3 小时前
elasticsearch
开发语言·数据库·python
今天也想快点毕业4 小时前
【SQL Server Management Studio 连接时遇到的一个错误】
java·服务器·数据库
Gauss松鼠会5 小时前
ElasticSearch迁移至openGauss
大数据·数据库·elasticsearch·jenkins·opengauss·gaussdb
LFloyue6 小时前
mongodb集群之分片集群
数据库·mongodb
济宁雪人6 小时前
Maven高级篇
java·数据库·maven