这样的SQL执行为什么不会报错?optimizer_trace深度历险

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 【10月更文挑战第12天】本文探讨了一条看似错误但实际上能成功执行的SQL语句,通过开启MySQL的优化器追踪功能,详细分析了SQL的执行过程,揭示了子查询被优化器解析为连接操作的原因,最终解释了为何该SQL不会报错。文章不仅增进了对SQL优化机制的理解,也展示了如何利用优化器追踪解决实际问题。

这样的SQL执行为什么不会报错?optimizer_trace深度历险

起因

在一个风和日丽的周五,小菜终于忙完本周的工作,看了看屏幕右下角的时间 17:30

小菜伸了伸懒腰,惬意的说到:还有半个小时下班,晚上回去吃点什么好呢?

小菜手摸了摸下巴,进入思考:今天似乎还忘记了什么..

“原来今天忙到没空摸鱼”,于是小菜赶紧打开掘金,逛逛沸点

...

逛着逛着,发现这样一条沸点:

(由于图中查询的结果不符合社会主义核心价值观,我给打了码)

经过

有两张表:

  1. t_user字段:id、name
  2. t_user_role字段:user_id、role
CREATE TABLE `t_user`  (
  `id` int NOT NULL,
  `name` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `t_user` VALUES (1, 'caicai');
INSERT INTO `t_user` VALUES (2, '菜菜');

CREATE TABLE `t_user_role`  (
  `user_id` int NULL DEFAULT NULL,
  `role` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `t_user_role` VALUES (1, '牛马');
INSERT INTO `t_user_role` VALUES (2, '老板');

t_user_role通过字段user_id与t_user的字段id进行关联

来查看这一个子查询的SQL:

 select * from t_user_role where user_id in (select user_id from t_user where id = 1);

在子查询中 select user_id from t_user where id = 1user_id 字段并不是 t_user 中的,而是 t_user_role 表中的

按道理,应该报错在t_user中找不到user_id字段才对,但这个SQL执行后却能够查询出数据:

user_id role
1 牛马
2 老板

小菜陷入了沉思,难道我的MySQL学错了?

小菜单独执行select user_id from t_user where id = 1时,发现正常报错:1054 - Unknown column 'user_id' in 'field list'

小菜挠了挠头,遇到这种情况好像无从下手,进入持续的思考后:小菜从单独执行报错,子查询执行就不报错的情况下,开始怀疑起子查询

小菜心想:子查询在某种情况下,优化器会使用半连接进行优化,会不会是这种情况造成的呢?

于是,小菜立马使用explain查看SQL的执行计划,但是很可惜,并没有在附加信息中查看半连接相关的优化

小菜不信邪,打算”打破砂锅问到底“,转眼一看右下角,时间已经来到了 17:45

小菜:怎么办,还有15分钟就下班了,再研究的话可能会超时,违背我以往到点就走的风格

“不管了”,小菜一怒之下怒了一下,准备开启优化器追踪,看看这个优化器到底再搞什么鬼

分析

#1.开启优化器追踪
SET optimizer_trace="enabled=on";

#2.执行SQL+查看优化器追踪(一起执行)
select * from t_user_role where user_id in (select user_id from t_user where id = 1);
select * from informationschema.OPTIMIZERTRACE;

#3.关闭
SET optimizer_trace="enabled=off";

(文末附带TRACE的图片,不想实操的同学可以直接看图片)

然后查看第二个结果中的TRACK:

(由于数据太多,我们一个一个查看)

优化器追踪的过程分为:preparation准备、optimization优化、execution执行

小菜看到内容带有join,整个人都兴奋了,小菜:我就知道,肯定是将子查询优化为连接了,机智如我

子查询在MySQL中会被转换为内部/外部查询

in中的查询被解析为内部查询 select#2: 它将查询的user_id指明为t_user_role的字段(到这里已与我们写SQL本意不同了)

select `t_user_role`.`user_id` from `t_user` where (`t_user`.`id` = 1)
#简化后
select `t_user_role`.`user_id` from `t_user` where `t_user`.`id` = 1
#优化前
select `user_id` from `t_user` where `t_user`.`id` = 1

到这里,原来的SQL被解析为:

#原SQL
select * from t_user_role where user_id in (select user_id from t_user where id = 1);

#解析后
select `t_user_role`.`user_id` AS `user_id`,`t_user_role`.`role` AS `role` from `t_user_role` where `t_user_role`.`user_id` in (select `t_user_role`.`user_id` from `t_user` where (`t_user`.`id` = 1))#优化下 方便查看
select * from t_user_role where user_id in (select t_user_role.user_id from t_user where id = 1)

至此,我们已经能够理解为啥SQL不会报错,原来user_id被解析为t_user_role的字段,因此不会报错

接下来,会将in子查询转换为半连接semijoin (小菜心想:哈哈果然是半连接,我真聪明)

并将(t_user.id = 1) and (t_user_role.user_id = t_user_role.user_id)作为半连接的关联条件

transformations_to_nested_joins会将子查询转化为半连接

#转换的半连接
select `t_user_role`.`user_id` AS `user_id`,`t_user_role`.`role` AS `role` 
from `t_user_role` 
semi join (`t_user`) 
where ((`t_user`.`id` = 1) and (`t_user_role`.`user_id` = `t_user_role`.`user_id`))

#优化 方便阅读 取消 `t_user_role`.`user_id` = `t_user_role`.`user_id`
select * 
from `t_user_role`
semi join `t_user` 
where `t_user`.`id` = 1

至此,准备阶段完毕,接下来进入优化:

优化阶段名词太多,不一一介绍了(”俺也不会呀,俺又不是DBA“,小菜小声BB)

condition_processing优化where条件

substitute_generated_columns将列代替的表达式进行替换,这里没有优化

table_dependencies表中的依赖,如外键、视图、触发器...

ref_optimizer_key_uses 列出ref可用的列(执行计划中的执行方式ref)

rows_estimation计算扫描行数、成本

considered_execution_plans考虑的执行计划

attaching_conditions_to_tables 表附加条件

优化完后进行执行

那么半连接的SQL语句如何优化成真正的SQL呢?

#半连接
select * 
from `t_user_role`
semi join `t_user` 
where `t_user`.`id` = 1

#转换成内连接 + 去重
select * 
from `t_user_role`
inner join `t_user` 
where `t_user`.`id` = 1

如果不太了解原理可以理解成 半连接 = 内连接 + 去重

MySQL会有多种去重手段来实现半连接,屏蔽去重的优化可以把运行的SQL看成内连接

至此,小菜终于分析完毕,一看时间17:58 “还剩两分钟,收拾一下准备下班”

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 MySQL进阶之路,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 gitee-StudyJavagithub-StudyJava 感兴趣的同学可以stat下持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

相关文章
|
3月前
|
SQL 数据库
数据库数据恢复—SQL Server数据库报错“错误823”的数据恢复案例
SQL Server附加数据库出现错误823,附加数据库失败。数据库没有备份,无法通过备份恢复数据库。 SQL Server数据库出现823错误的可能原因有:数据库物理页面损坏、数据库物理页面校验值损坏导致无法识别该页面、断电或者文件系统问题导致页面丢失。
109 12
数据库数据恢复—SQL Server数据库报错“错误823”的数据恢复案例
|
3月前
|
SQL 数据库
SQL解析相关报错
SQL解析相关报错
49 5
|
4月前
|
XML SQL 数据格式
XML动态sql查询当前时间之前的信息报错
XML动态sql查询当前时间之前的信息报错
57 2
|
4月前
|
SQL 分布式计算 DataWorks
DataWorks操作报错合集之新建项目的元数据的sql报错,如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
2月前
|
SQL 关系型数据库 MySQL
|
3月前
|
关系型数据库 MySQL Nacos
nacos启动报错 load derby-schema.sql error
这篇文章描述了作者在使用Nacos时遇到的启动错误,错误提示为加载derby-schema.sql失败,作者通过将数据库从Derby更换为MySQL解决了问题。
nacos启动报错 load derby-schema.sql error
|
3月前
|
关系型数据库 MySQL Java
flywa报错java.sql.SQLSyntaxErrorException: Unknown database ‘flyway‘
flywa报错java.sql.SQLSyntaxErrorException: Unknown database ‘flyway‘
42 1
|
4月前
|
SQL 分布式计算 DataWorks
DataWorks操作报错合集之使用sql查询报错无权限,是什么原因
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
|
5月前
|
SQL 关系型数据库 分布式数据库
PolarDB产品使用问题之sql运行报错是神么原因
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。
|
4月前
|
SQL Java 数据库连接
【Azure 应用服务】Java ODBC代码中,启用 Managed Identity 登录 SQL Server 报错 Managed Identity authentication is not available
【Azure 应用服务】Java ODBC代码中,启用 Managed Identity 登录 SQL Server 报错 Managed Identity authentication is not available
下一篇
DataWorks