【6】mysql查询性能优化-关联子查询

【README】

  1. 先说结论:一般用inner join来改写in和exist,用left join来改写not in,not exist;(本文会比较内连接,包含in子句的子查询,exist的性能 )

  2. 本文总结自高性能mysql 6.5.1章节【关联子查询】

  3. 数据库表及数据特征:

  • wn_film_tbl: 电影表, 50w数据量;
  • wn_actor_tbl: 演员表, 50w数据量;
  • wn_file_actor_rel_tbl: 电影演员关联表, 10w数据量;
  1. ddl如下:
sql 复制代码
CREATE TABLE `wn_film_tbl` (
  `film_id` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '电影id',
  `film_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '电影名称',
  `release_year` int(11) NOT NULL COMMENT '上映年份',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`film_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='电影表'

CREATE TABLE `wn_actor_tbl` (
  `actor_id` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '演员id',
  `actor_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '演员名称',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='演员表'

CREATE TABLE `wn_film_actor_rel_tbl` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `film_id` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '电影id',
  `actor_id` varchar(20) COLLATE utf8mb4_general_ci NOT NULL COMMENT '演员id',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `last_modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_film_id` (`film_id`) COMMENT '电影id索引',
  KEY `idx_actor_id` (`actor_id`) COMMENT '演员id索引'
) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='电影与演员关联表'

【1】查询某演员参演的电影清单

  1. 具体的,查询 演员0420AAAID000099参演的电影清单,通过造数,该演员参演的电影有 15000部;

  2. 几种不同的实现方式:

  • 方案1: 使用 包含in()子句的子查询**(性能非常低,作为反例,供各位看官参考)**;
  • 方案2:使用内连接;
  • 方案3:使用exists;

【1.1】使用包含in()子句的子查询

  1. sql如下(为了方便打印执行计划,这里仅查询一条#limit 1):
sql 复制代码
select sql_no_cache * 
from wn_film_tbl film
where film_id in (select film_id from wn_film_actor_rel_tbl  where actor_id='0420AAAID000099')
limit 1
;
  1. 查看其执行计划及查询成本:
  • explain 查看执行计划;
  • show status like 'last_query_cost' 查询上一个sql的执行成本;(成本的最小单位是随机读取1个大小为4K的数据页 )
  1. 执行计划剖析(执行计划列的含义,参见
    mysql_explain执行计划字段解析-CSDN博客):

  2. 关联表执行步骤:

  • 第1步: 扫描film_actor_rel_tbl 表的普通索引 idx_actor_id ,查询 actor_id 等于0420AAAID000099 的 电影演员关联记录,得到列表 result_list
  • 第2步:全表扫描查询结果result_list(猜测是在内存中,因为extra没有 using temporary);
  • 第3步:扫描film表的主键索引树,查询film表中film_id(主键) 等于 result_list#film_id 字段值的记录并返回;
  1. 对于上述步骤的总结:mysql对任何关联都执行嵌套循环关联操作(即先查询 rel表,然后全表扫描rel表,循环遍历rel表,再在单次遍历中循环遍历 film表 );
  • 有个问题: 为什么不反着来? 先查询 film表,再在循环遍历film过程中,遍历rel表 (动脑筋的时候到了);
  • 小表驱动大表原则; 为什么小表驱动大表,大表驱动小表不行吗?(如果理解了上面的问题,就可以推导出小表驱动大表的依据)
  1. 嵌套循环关联查询步骤如下:
  • 查询外层表的数据行, 并构建外层表的迭代器;
  • 遍历外层表迭代器,获取单个外层表数据行(第1层循环)
    • 根据单个外层表数据行**,查询**内层表的数据行,并构建内层表迭代器;
    • 遍历内层表迭代器,获取单个内层表数据行(第2层循环)
      • 合并外层与内层表数据行,构建输出结果;
      • 内层表迭代器滑动;
  • 外层表迭代器滑动;

【1.2】使用内连接

sql如下:

sql 复制代码
select sql_no_cache film.* 
from wn_film_tbl film
inner join wn_film_actor_rel_tbl rel on film.film_id = rel.film_id 
where rel.actor_id='0420AAAID000099'
limit 1

【敲黑板】

  • 显然,根据执行计划,我们看到,mysql先查询了rel 表,然后再查询了 film表,为什么?(id相同,则按照从上到下的顺序执行)
  • 为什么不是先查询film表,再查询rel表;

【1.3】使用exists

sql如下:

sql 复制代码
select sql_no_cache film.* 
from wn_film_tbl film
where exists (
  select * from wn_film_actor_rel_tbl rel where rel.actor_id='0420AAAID000099'
  and rel.film_id = film.film_id 
) 
limit 1;

【2】总结:

  1. 根据 last_query_cost 可以看到: 内连接需要扫描 36572个数据页;in子句的子查询需要扫描42467个数据页,所以内连接性能 【优于】 in子句的子查询;

  2. 从扫描行数来看: 内连接性能优于 in子句的子查询, in子句的子查询 优于 exists

问题:为什么 exits子查询的 last_query_cost结果显示为0呢?没懂

相关推荐
小码的头发丝、16 分钟前
Django中ListView 和 DetailView类的区别
数据库·python·django
小兜全糖(xdqt)23 分钟前
mysql数据同步到sql server
mysql·adb
Karoku06626 分钟前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
周全全1 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
白云如幻1 小时前
MySQL的分组函数
数据库·mysql
荒川之神1 小时前
ORACLE 闪回技术简介
数据库·oracle
时差9533 小时前
【面试题】Hive 查询:如何查找用户连续三天登录的记录
大数据·数据库·hive·sql·面试·database
让学习成为一种生活方式3 小时前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
秋意钟3 小时前
MySQL日期类型选择建议
数据库·mysql
Dxy12393102164 小时前
python下载pdf
数据库·python·pdf