NL连接一定是小表驱动大表效率高吗

本文涉及的产品
RDS AI 助手,专业版
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: NL连接一定是小表驱动大表效率高吗

前言

两表使用nest loop(以下简称NL)方式进行连接,小表驱动大表效率高,这似乎是大家的共识,但事实上这是有条件的,并不总是成立。这主要看大表扫描关联字段索引后返回多少数据量,是否需要回表,如果大表关联后返回大量数据,然后再回表,这个代价就会很高,大表处于被驱动表的位置可能就不是最佳选择了。

实验举例

使用benchmarksql压测的两个表bmsql_warehousebmsql_order_line来测试,初始化10仓数据。

mysql> show create table bmsql_warehouse\G
*************************** 1. row ***************************
       Table: bmsql_warehouse
Create Table: CREATE TABLE `bmsql_warehouse` (
  `w_id` int NOT NULL,
  `w_ytd` decimal(12,2) DEFAULT NULL,
  `w_tax` decimal(4,4) DEFAULT NULL,
  `w_name` varchar(10) DEFAULT NULL,
  `w_street_1` varchar(20) DEFAULT NULL,
  `w_street_2` varchar(20) DEFAULT NULL,
  `w_city` varchar(20) DEFAULT NULL,
  `w_state` char(2) DEFAULT NULL,
  `w_zip` char(9) DEFAULT NULL,
  PRIMARY KEY (`w_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
mysql> show create table bmsql_order_line\G
*************************** 1. row ***************************
       Table: bmsql_order_line
Create Table: CREATE TABLE `bmsql_order_line` (
  `ol_w_id` int NOT NULL,
  `ol_d_id` int NOT NULL,
  `ol_o_id` int NOT NULL,
  `ol_number` int NOT NULL,
  `ol_i_id` int NOT NULL,
  `ol_delivery_d` timestamp NULL DEFAULT NULL,
  `ol_amount` decimal(6,2) DEFAULT NULL,
  `ol_supply_w_id` int DEFAULT NULL,
  `ol_quantity` int DEFAULT NULL,
  `ol_dist_info` char(24) DEFAULT NULL,
  PRIMARY KEY (`ol_w_id`,`ol_d_id`,`ol_o_id`,`ol_number`),
  KEY `ol_stock_fkey` (`ol_supply_w_id`,`ol_i_id`),
  KEY `ol_d_id` (`ol_d_id`),
  CONSTRAINT `ol_order_fkey` FOREIGN KEY (`ol_w_id`, `ol_d_id`, `ol_o_id`) REFERENCES `bmsql_oorder` (`o_w_id`, `o_d_id`, `o_id`),
  CONSTRAINT `ol_stock_fkey` FOREIGN KEY (`ol_supply_w_id`, `ol_i_id`) REFERENCES `bmsql_stock` (`s_w_id`, `s_i_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

查看如下sql的执行计划与效率:

select  * from bmsql_order_line a join bmsql_warehouse b on a.ol_d_id=b.w_id
 where a.ol_dist_info like 'a%' and b.w_ytd =300000.00;
mysql> explain analyze select  * from bmsql_order_line a join bmsql_warehouse b on a.ol_d_id=b.w_id
    ->  where a.ol_dist_info like 'a%' and b.w_ytd =300000.00;
+--------------------------------------------------------------------+
| EXPLAIN                                                            |
+--------------------------------------------------------------------+
| -> Nested loop inner join  (cost=396352.21 rows=323755) (actual time=11.542..19705.922 rows=115207 loops=1)
    -> Filter: (b.w_ytd = 300000.00)  (cost=1.15 rows=9) (actual time=0.780..0.893 rows=10 loops=1)
        -> Table scan on b  (cost=1.15 rows=9) (actual time=0.743..0.810 rows=10 loops=1)
    -> Filter: (a.ol_dist_info like 'a%')  (cost=12059.95 rows=35973) (actual time=1.401..1969.304 rows=11521 loops=10)
        -> Index lookup on a using ol_d_id (ol_d_id=b.w_id)  (cost=12059.95 rows=323788) (actual time=1.388..1833.176 rows=300209 loops=10)
 |+--------------------------------------------------------------------+
1 row in set (20.31 sec)

从上面的执行计划看出,优化器选择小表b表驱动大表a,b表返回10条记录,属于小表,a表为被驱动表,每次关联使用二级索引ol_d_id,扫描索引320209行,回表过滤后剩余11521行记录,属于大表,最终结果集返回115207行数据。使用此计划耗时20秒左右。

使用hint改变表的连接顺序

mysqlb> explain analyze select /*+ join_order(a,b) */  * from bmsql_order_line a join bmsql_warehouse b on a.ol_d_id=b.w_id  where a.ol_dist_info like 'a%' and b.w_ytd =300000.00;
+---------------------------------------------------------------------------+
| EXPLAIN                                                                   |
+---------------------------------------------------------------------------+
| -> Nested loop inner join  (cost=408609.87 rows=323755) (actual time=1.374..4696.931 rows=115207 loops=1)
    -> Filter: (a.ol_dist_info like 'a%')  (cost=295295.55 rows=323755) (actual time=1.036..4614.585 rows=115207 loops=1)
        -> Table scan on a  (cost=295295.55 rows=2914088) (actual time=0.937..4275.678 rows=3002091 loops=1)
    -> Filter: (b.w_ytd = 300000.00)  (cost=0.25 rows=1) (actual time=0.000..0.000 rows=1 loops=115207)
        -> Single-row index lookup on b using PRIMARY (w_id=a.ol_d_id)  (cost=0.25 rows=1) (actual time=0.000..0.000 rows=1 loops=115207)
+----------------------------------------------------------------------------+
1 row in set (4.79 sec)

从上面的执行计划看出,改变连接顺序后,大表a驱动小表b,此计划执行耗时4秒左右,相比小表b驱动大表a,时间上节省了近80%。由此可见,并不总是小表驱动大表效率高。

其实这属于两表关联,返回大量数据的SQL,在MySQL8.0版本可以控制优化器使用hash join,走hash join的效率会比NL要高。忽略两表关联字段上的索引,让优化器选择走hash join。

mysql>  explain analyze select  * from bmsql_order_line a ignore index(ol_d_id) join bmsql_warehouse b ignore index(primary) on a.ol_d_id=b.w_id  where a.ol_dist_info like 'a%' and b.w_ytd =300000.00;
+----------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                             |
+-------------------------------------------------------------------------------------------+
| -> Inner hash join (a.ol_d_id = b.w_id)  (cost=295489.08 rows=3997) (actual time=0.428..3586.047 rows=115207 loops=1)
    -> Filter: (a.ol_dist_info like 'a%')  (cost=29634.41 rows=35973) (actual time=0.155..3549.633 rows=115207 loops=1)
        -> Table scan on a  (cost=29634.41 rows=2914088) (actual time=0.133..2747.262 rows=3002091 loops=1)
    -> Hash
        -> Filter: (b.w_ytd = 300000.00)  (cost=1.15 rows=9) (actual time=0.129..0.156 rows=10 loops=1)
            -> Table scan on b  (cost=1.15 rows=9) (actual time=0.123..0.147 rows=10 loops=1)
 |
+----------------------------------------------------------------------------------+
1 row in set (3.67 sec)

此处注意: 虽然官方文档上说可以使用BNLNO_BNL的hint来启用与禁用hash join,但是在关联字段上有索引的情况下,优化器不会评估hash join的代价,也就不会选择hash join,NO_BNL能够禁用hash join,但是BNL并不能严格让优化器选择hash join。

如果大表的关联字段使用索引覆盖,不需要回表的情况下执行效率如何呢?

看下面的SQL的执行计划,SQL中变换大表a的关联字段。

mysql> explain analyze select * from bmsql_order_line a  join bmsql_warehouse b on a.ol_w_id=b.w_id  where a.ol_dist_info like 'a%' and b.w_ytd =300000.00;
+--------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                    |
+--------------------------------------------------------------------------------------------+
| -> Nested loop inner join  (cost=494.86 rows=544) (actual time=0.868..4154.968 rows=115207 loops=1)
    -> Filter: (b.w_ytd = 300000.00)  (cost=1.15 rows=9) (actual time=0.387..0.476 rows=10 loops=1)
        -> Table scan on b  (cost=1.15 rows=9) (actual time=0.363..0.417 rows=10 loops=1)
    -> Filter: (a.ol_dist_info like 'a%')  (cost=1.15 rows=60) (actual time=0.119..414.532 rows=11521 loops=10)
        -> Index lookup on a using PRIMARY (ol_w_id=b.w_id)  (cost=1.15 rows=544) (actual time=0.109..385.753 rows=300209 loops=10)
 |
+-------------------------------------------------------------------------------------------+
1 row in set (4.23 sec)

从上面的执行计划看出,优化器依然选择小表b驱动大表a,大表作为被驱动表,使用主键进行扫描,不需要回表,在此例子中小表驱动大表与大表驱动小表的执行耗时是差不多的,哪种方式效率高主要看大表过滤后的数据量占全表的百分比,不同的数据量可能就需要选择不同的方式。

总结

MySQL8.0有两种连接方式,选择NL还是hash join,要看两表关联后返回少量数据还是大量数据,一般情况下,少量数据 NL 优于 hash join,大量数据,hash join优于 NL。

如果只能选择NL连接(低于MySQL8.0的版本),那么在NL 情况下,是小表驱动大表快还是大表驱动小表快,看大表关联使用的索引是否形成索引覆盖,及关联后返回的数据量。

大表关联使用二级索引,关联后返回大量数据,又需要回表,这种情况下,一般选择大表驱动小表效率高些;关联后返回少量数据,一般选择小表驱动大表效率高些。

大表关联使用索引覆盖,要看大表过滤后的数据量占全表的百分比,不同的数据量可能就需要选择不同的方式。

不要试图去记住这些结论,深入了解表的连接方式与扫描方式,理解SQL的执行过程,一切都会变得顺理成章,我们的人脑会对SQL选择哪种执行计划执行效率高有一个清晰的判断,如果优化器做出错误的决策,可以尝试使用各种优化方式干涉优化器的决策。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
数据挖掘 数据处理 索引
python常用pandas函数nlargest / nsmallest及其手动实现
python常用pandas函数nlargest / nsmallest及其手动实现
445 0
|
JSON 小程序 前端开发
微信小程序(二十四)微信小程序富文本编辑器editor
{ "component": true, "usingComponents": {} }
497 0
|
JSON C++ 数据格式
【VsCode】通过tasks.json中的problemMatcher属性的fileLocation子属性设定问题的输出内容
vscode 对于 json 文件的解析方式的开源代码部分. 摘录 文件目录设定部分的说明:
609 0
|
存储 监控 前端开发
现货合约量化交易系统开发技术规则
现货合约量化交易系统的开发是综合性工程,涵盖目标确定、市场调研、功能规划等前期准备;选择编程语言和技术栈;设计系统架构;开发策略管理、资金管理、交易执行、数据分析及用户界面模块;进行单元、集成、压力测试及优化;最后选择服务器部署并持续监控。
|
SQL 缓存 关系型数据库
故障案例:MySQL唯一索引有重复值,官方却说This is not a bug
故障案例:MySQL唯一索引有重复值,官方却说This is not a bug
494 0
|
SQL 监控 关系型数据库
MySQL 8.0不再担心被垃圾SQL搞爆内存
MySQL 8.0.28开始,新增一个特性,支持监控统计并限制各个连接(会话)的内存消耗,避免大量用户连接因为执行垃圾SQL消耗过多内存,造成可能被OOM kill的风险。 首先,需要先设置系统选项 global_connection_memory_tracking = 1,之后可以通过系统状态变量 Global_connection_memory 查看当前所有连接消耗的内存总量:
957 0
|
机器学习/深度学习 人工智能 自然语言处理
【A I 软件开发】一文讲清交互应用的实现原理
【A I 软件开发】一文讲清交互应用的实现原理
884 0
|
开发工具 git
git push origin master提交报错解决办法
git push origin master提交报错解决办法
693 0
|
Linux 网络安全 数据安全/隐私保护
CentOS7下使用DenyHosts阻止SSH暴力攻击
CentOS7下使用DenyHosts阻止SSH暴力攻击
580 1
CentOS7下使用DenyHosts阻止SSH暴力攻击
|
SQL 存储 JSON
Hive学习---7、企业级调优(二)
Hive学习---7、企业级调优(二)