点击蓝色“有关SQL”关注我哟

加个“星标”,天天与10000人一起快乐成长

图 | Lenis 

叮叮...

清脆的微信声,把我从梦中唤醒。早已习惯起床之后,回复下公众号信息,爬一下微信群里的楼。

只是今天,特别热闹。红色的小圆点,已经提示,微信群有 128+ 的未读消息了。

好奇打开了下,这次讨论的,是一道 SQL 面试题。


我对代码有洁癖,看到扭捏成一坨的 SQL, 忍不住拿到 Visual Studio Code 里,用 poorman's sql formatter 给 PS 下。

就像发朋友圈,任何不经滤镜,美颜的图,从不发。女孩子们都懂的!

以下是整理后的代码,颜值高很多:


select *
from user
where age = 10
limit 100000, 10 ;time 4.73s

select a.*
from user a
inner join (select id from user where age = 10 limit 100000,10) b on b.id = a.id ;time 0.53s

面试官提问,把第一段 SQL 改为第二段后,为什么性能会有如此之大的提高,优化逻辑是什么。

这一题,让我想到特别恶毒的一个段子:

你愿意躺在宝马车里哭,还是喜欢坐在自行车上笑

没有限定的条件,回答自然千奇百怪,甚至大相径庭。

如果有条件,为啥我不能躺在宝马里笑;又或者坐在自行车上,难道就不会哭了?

汇总了下,大家对这道题的优化逻辑:

  • user 表上有 age 的索引

  • user 表上有 age 的索引,还有 id 覆盖索引

  • 第二段的子查询不用回表

  • 第一段 SQL 执行了全表扫描

更有朋友质疑了第二段的性能提高:

  • 没有 order by,结果乱序,易产 bug

  • 第一段 SQL 重跑下,应该 0 秒就出结果

  • 除非是查询缓存,第二段效率未必高

  • MySQL优化器真笨,为什么不直接跳到第 100000 条,白白浪费读取那么多数据

回答都很精彩,质疑也都有理有据。可以看得出来,能回答出一二的朋友,数据库功底都很棒。

插一句:没有基础的小白,你肯定很烦讨论这样的问题。这就是为什么,我的群里,读者都需要有基础。当然,你若真感兴趣,态度友好,有颗求知红心,那非常欢迎你。

回到题目上来,要回答好这道 SQL,特别考验数据库的底层认知。仅仅从语法角度,这一题不难,无非是子查询 + inner join 的考察。

从数据库体系结构上回答,这一题就比较复杂。还要考虑到 MySQL 的产品特性,比如 MySQL 8 , 有些关系型数据库的理论,在这里就行不通,比如查询缓存。

据我有限的认识,这道题考察了这些方面:

  • 数据库的数据页结构

  • 数据库的索引页结构

  • 查询优化器的原理

  • 数据页,索引页访问算法

  • 数据库缓存设计

  • 数据库并发控制

  • 数据库引导优化器的方法

  • 数据库执行计划

在继续阅读之前,请各位看官,自备清茶或咖啡,以免读到干渴而放弃。准备好了,咱们就开始。

数据库的数据页结构

数据页是数据库的底层存储单元。我知道,很多初学者听到底层,就头大。认为和 c/c++ 一样无聊,甚至像是看到了汇编,天然想着要逃避。

我曾经也这样。甚至幻想,像虚竹一样,头顶头,获取无崖子一甲子功力。但靠YY,解决不了任何实际问题。纯靠与数据库大V,握个手,喝个咖啡,是不会获得任何技巧的。

接受了这个事实,我就开始死磕市面上能买到的数据库相关书籍了。

于是我发现,数据库的数据页结构,并非想象的那么难。用作业本来比喻,就很好理解。

小时候写作业,大家都用的本子,应该都还印象挺深吧。

田字格语文本与数学用作业本


这种本子的每一页,都记载着我们难忘的童年。

有被罚抄留下的课文段落;也有正儿八经写下的作文;还有写的小纸条,通常那一页写上一两句就撕了,现在想想够浪费吧。

在这样的本子里写字,你写得字大,还是小;或者写一行空一行,都会 影响这一页的信息密度。明明可以用一页纸写完,写的字儿大了,写的稀疏了,行与行之间还有空一两行,就会加大信息密度的间隙。

数据库的数据页也一样。它从上到下,写满了byte(字节),或者为了 insert 速度和减少行溢出,中间空几行。


这样的数据页,组合起来,就成了存储一张表的结构。数据量越大,数据页也越多。

如果没有很好的设计字段长度,存储的时候,也没有安排的紧密些,那么原本存储1万行的数据,就有可能需要10万行的空间。



上两图就很好的解释了,空间安排的重要性。

这和在作业本上写作业一个道理。

写作业讲完了,我们来讲讲读。

如果你打算从头到尾去读你的作业本,想必会花很多时间,才能找到你想要的曾经写过的特别佩服自己的那段话,或者公式推理。

此时,有两种方法,可以帮你:

一种,一开始写作业,就把字儿写得小一些,把空行都写上字儿,这样把 14 页的作业本,浓缩成 2 页,自然翻的页数少了;

二是,另外拿一个本子,把每页的关键字记下来,比如螃蟹在第1,3,5页;冰激淋在第2,4,6页;游戏机在7,8,9页。这样,找起来就少翻几页。

聪明如你,读到这里,一定想到些什么。没错,第一段 SQL 和第二段 SQL,在没有索引的情况下(假设你没有索引的概念),那么第二种写法,反而更慢一些。

大家都是在寻找 age=10 的数据,而第二段SQL,找完之后,还要再找这 10 条数据所在数据页上的其他数据。

相当于,你翻遍作业本,好不容易找到你想到的那段话,和数学公式,发现老师还要求你把那一页上的其他段落或者应用例子,都找出来。这样你需要重复去读,耗时会更多

事实上,经过实验,也的确如此。

在 MySQL 5.5 中,emp_info 有588万数据,没有任何索引和主键。

这儿,我用 employees 库代替。我并没有原问题一模一样的数据。


select SQL_NO_CACHE emp.*
from emp_info emp
where age = 20
limit 100000,10 ;10 row(s) returned 0.109 sec / 0.000 sec花费 109ms
select SQL_NO_CACHE emp.*from emp_info emp inner join (select emp_no from emp_info where age=20 limit 100000,10) tmp on tmp.emp_no = emp.emp_no 10 row(s) returned 0.172 sec / 0.000 sec花费 172ms

细心的朋友会发现,两段 SQL 中都加了 SQL_NO_CAHE. 这是为了防止 Query Cache 的发生,增加说服力。MySQL 5.6 及以下版本都支持 Query Cache, 也就是查询缓存。

解释下为什么要设计 Query Cache.

当二段 SQL 一模一样,连续执行两次时,第二次查询耗时为0. 这是因为,优化器充分利用第一次的缓存数据,秒出结果

这是怎么做到的?

这本书能告诉你,为什么:

简单来说,第一次执行的某条 SQL 会被优化器编译为一段 hash 文本,且它的执行结果,会被存储在内存中。

当一模一样的 SQL 再次发送到优化器时,会和存储的 hash 值做个对比,如果一样,就直接返回内存中的结果,而不需要再次执行。

这功能,想想都兴奋。但,也有弊端。能重复利用缓存,必须是底层数据没有变化,一旦变化了,那么结果就会不对,对于第二次发送 SQL 命令的用户来说,就产生了数据不一致。

在一个非常繁忙的 OLTP 应用中,数据更新出乎你想象的快,查询缓存往往顷刻间就会失效。与其维护这么段失效的内存,不如不维护,空出来干点别的事,多好。

于是 MySQL 8 就废弃了它。

在本例中,加上 SQL_NO_CACHE 这样的 hint 后,就是要排除利用查询缓存带来优化的可能。这样,每次执行都重新走一遍解析,优化到取数。保证实验的公平性。

我把这 588万数据,导入 MySQL 8 版本中,同样执行上面的 SQL,奇迹就来了:

select  emp.*
from employee_info emp
where age = 20
limit 100000,10 ;10 row(s) returned 0.094 sec / 0.000 sec花费 94ms
select emp.*
from employee_info emp
inner join (select emp_no from employee_info where age=20 limit 100000,10) tmp on tmp.emp_no = emp.emp_no 10 row(s) returned 4.485 sec / 0.000 sec花费 4.485s

没想到 MySQL 8 在默认配置下,比 MySQL 5.5 还 “健忘”。翻过的作业,居然一点都不记得。

看执行计划知晓,子查询和外层查询,虽然访问同一个表,但却当成两个表来处理。

至此,大家可以清楚的看到,第二种 SQL 不经优化,性能还不如第一种写法

数据库的索引页结构

刚刚,在讲述提高查询效率的时候,用到了 2 个方法。这两个方法,在数据库中,用索引来实现了。

假设,在作业本上,每一页都写了一篇小散文。我用另外一个本儿,按照关键字,记录这些关键字在作业本中对应出现的页码和行号:

螃蟹:

  • 第 1 页,第 4 行;

  • 第 3 页,第 6 行;

  • 第 5 页,第 8 行

冰激淋:

  • 第 2 页,第 1 行;

  • 第 4 页,第 9 行;

  • 第 6 页,第 5 行

于是,原本按照从作业本,一页页寻找螃蟹,需要翻完所有页,才能找全,现在有了索引本,一页3行,就搞定。

回到面试题来,看第二段 SQL,要找100010 行数据,在索引中找,和在全表中找,消耗的时间,就不在同一个数量级了。

具体来细说。

在作业本上,写的小作文,除了螃蟹,冰激淋等关键字,肯定还有很多很多其他词汇,比如"小妹生日那天,我送给她 2 盒冰激凌,6 只螃蟹,还有 10 多玫瑰"。

这样一来,一页上只出现一个螃蟹,翻完整个本儿,才知道有 5 页是包含螃蟹两字的。

那 user 这张表,也一样,可能有 10 个字段,每个数据页能存上 100 条数据,而每过 10 页,才有一个 age=20 的用户,那么 100000 条数据,可能就被稀释在 1000000 个数据页中。

但索引页就不一样了,100000 个 age=10, 就在 100000行上,每个索引页能存 1000 条,那么 1000 页索引页也就存完了。

通过对比,至少有 1000000/1000 即 1000 倍的时间节省了。

以上只是假设,真实情况,要复杂的多。

有索引的地方,并不简单。因为索引最大的风险,在于回表。

什么是回表?

根据关键字"螃蟹",去找哪一页出现过它,这是索引干的活。但依据"螃蟹"这个关键字,进一步找到作文中的主角,比如"小妹",那索引就做不到了。只能翻开作业本,去每一页包含"螃蟹"的作文中,去找。这种情况,就是回表。

可见,回表又增加了一次操作,会增加耗时。

而第一段 sql, 比起第二段,增加了回表的次数。因为并没指定按照什么去排序,这就是优化器矛盾的地方了。假如加上按照 id 排序,就和第二段一样了。

举个例子:

假设表 employees_info,数据还是588万条,但是加了两样索引:-- emp_no 为主键
-- 以 age 为索引select SQL_NO_CACHE emp.*
from employees_info emp
where age = 20
limit 100000,10 ;10 row(s) fetched - 63ms

看来 MySQL 5.5 优化器在这里做了判断,以 age 为排序,这样最大的消耗在索引访问上。

假设要以 employees_info 其中另外的 from_date 来排序,看下结果:

select SQL_NO_CACHE emp.*
from employees_info emp
where age = 20
order by emp.from_date
limit 100000,10 ;10 row(s) fetched - 12.63s

这样一来,不仅仅要把索引 age=20 的数据全部找遍,还需回表抓下 from_date 的值。这就是回表的代价。

在 MySQL 8 上,这段 SQL 已经无法跑了,52s 才出结果。

回到写法的对比上来:


select SQL_NO_CACHE emp.*
from employees_info emp
inner join (select emp_no from employees_info where age=20 limit 100000,10) tmp on tmp.emp_no = emp.emp_no 10 row(s) fetched - 33ms

比起 63ms, 快1倍。

于是,第二种写法,在有索引的情况下,优势就来了。

无论在 MySQL 5.5 还是 MySQL 8, 第二种写法,都具有性能优势。

但是,这道题,是具有歧义的。没有 Order By, Limit 的意义在这两种写法中,就不同

改成这样,就有对比性了:

select  emp.*
from emp_info emp
where age = 20
order by emp.emp_no
limit 100000,10 ;10 row(s) fetched - 102msselect emp.*
from emp_info emp
inner join (select emp_no from emp_info where age=20 order by emp_no limit 100000,10) tmp on tmp.emp_no = emp.emp_no10 row(s) fetched - 26ms

这样,第二段 SQL 的优势才能说得清楚。相信看完上面的解释,原理就很清晰了。

但这里还涉及到优化器的成本模型计算,为什么第一段 SQL 没有被优化,看上去放弃了索引,而采用了全表扫描?

下回再讲!

--完--

往期精彩:

本号精华合集(三)

如何写好 5000 行的 SQL 代码

如何提高阅读 SQL 源代码的快感

我在面试数据库工程师候选人时,常问的一些题

零基础 SQL 数据库小白,从入门到精通的学习路线与书单

外企一道 SQL 面试题,刷掉 494 名候选人相关推荐

  1. 一道SQL面试题(行列互换)

    转载:http://blog.csdn.net/jackfrued/article/details/45021897 有一个SQL题在面试中出现的概率极高,最近有学生出去面试仍然会遇到这样的题目,在这 ...

  2. 简单的一道 SQL 题,谈如何提高编程水平

    点击蓝色"有关SQL"关注我哟 加个"星标",天天与12238人一起快乐成长 前两天发了一篇<SQL的3列4种对比方法>,近500 阅读.一个月没更 ...

  3. 一道SQL语句面试题,关于group by

    一道SQL语句面试题,关于group by 表内容:2005-05-09 胜 2005-05-09 胜 2005-05-09 负 2005-05-09 负 2005-05-10 胜 2005-05-1 ...

  4. 一道SQL语句面试题

    最近面试了几个人,给他们出了一道SQL语句面试题,但是反应效果不太理想.题目如下: 表scores有四个字段,学生stu.班级class.学院institute.分数score, 要求返回:班级考试人 ...

  5. 牛客网SQL 进阶篇刷题

    牛客网SQL 进阶篇刷题(1-19) 用户1001在2021年9月1日晚上10点11分12秒开始作答试卷9001,并在50分钟后提交,得了90分: 用户1002在2021年9月4日上午7点1分2秒开始 ...

  6. 算法面试必备-----数据库与SQL面试题

    算法面试必备-----数据库与SQL面试题 算法面试必备-----数据库与SQL面试题 1.数据库理论问题 问题:什么是数据库,数据库管理系统,数据库系统,数据库管理员? 问题:什么是元组,码,候选码 ...

  7. 数据分析|SQL面试题集锦

    https://zhuanlan.zhihu.com/p/136619982 https://www.cnblogs.com/diffrent/p/8854995.html 1.用一条SQL 语句 查 ...

  8. 一道HashSet面试题引发的蝴蝶效应

    没错,我又借着"面试题"的名头来搞事情了,今天要说的是 HashSet ,而这确实是一个实际面试中遇到的问题.当时的场景大概是这样的,面试官在了解了你的知识广度以后,决心来考察一番 ...

  9. 数据库基础SQL知识面试题一

    数据库基础SQL知识面试题一 作者:尹正杰  版权声明:原创作品,谢绝转载!否则将追究法律责任. 如标题所示,本篇博客主要介绍基础知识的面试题.大家可以用来测试面试者的技术水平,由于个人水平所限,难免 ...

最新文章

  1. addTwoNumbers
  2. 手机蓝牙扫码怎么使用_手机自带的蓝牙功能那么好用,为什么使用的人却并不那么多?...
  3. 【Java 虚拟机原理】堆区 | Java VisualVM 工具
  4. oracle中ci是什么意思,enq: CI - contention(附AWR)
  5. python开发跟淘宝有关联微_使用Python分析淘宝用户行为
  6. matlab连续信号离散化_MATLAB开发自动驾驶第八课-控制信号帧的回放以进行标签化处理...
  7. Windows Communication Foundation(WCF) 一步一步从入门到精通
  8. 1526B. I Hate 1111
  9. mysql 复杂的sql_mysql 一个复杂的sql
  10. android studio - 解决Android Studio不停的Indexing的问题
  11. 微软云计算介绍与实践(实践之一)
  12. Winsock传输数据
  13. 使用计算机时应先按,计算机应用基础试卷及答案
  14. 一铲子下去,服务器全瘫痪......
  15. vue使用 moment.js 格式化时间(获取当前日期的周一和周日)
  16. human pose estimation期刊简单汇总
  17. 新办林业规划资质(丙级)应具备的条件?
  18. RoCE网络技术和实现方式
  19. 完美修复损坏的系统文件
  20. 对flash cs5的展望

热门文章

  1. C语言基础 --使用指针数组存储若干英文书名,调用排序函数将其排序后输出
  2. Android从零单排--常见设计模式
  3. Ubuntu安装Flash Player插件
  4. poj3090(欧拉函数)
  5. 知乎热议:为什么有很多中国人跑去日本当程序员?
  6. Tmall商品详情API接口代码对接教程
  7. 不容忽视的10个新兴硬件技术
  8. 快速上手highcharts
  9. 2021年施工升降机安装拆卸工(建筑特殊工种)考试内容及施工升降机安装拆卸工(建筑特殊工种)复审模拟考试
  10. HTML,CSS的学习心得