CategoryResourceRepost/极客时间专栏/SQL必知必会/第二章:SQL性能优化篇/28丨从磁盘I|O的角度理解SQL查询的成本.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

142 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="28丨从磁盘I/O的角度理解SQL查询的成本" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/16/52/16678aa507d1c3adc521f2fbc451eb52.mp3"></audio>
在开始今天的内容前,我们先来回忆一下之前的内容。
数据库存储的基本单位是页对于一棵B+树的索引来说是先从根节点找到叶子节点也就是先查找数据行所在的页再将页读入到内存中在内存中对页的记录进行查找从而得到想要数据。你看虽然我们想要查找的只是一行记录但是对于磁盘I/O来说却需要加载一页的信息因为页是最小的存储单位。
那么对于数据库来说,如果我们想要查找多行记录,查询时间是否会成倍地提升呢?其实数据库会采用缓冲池的方式提升页的查找效率。
为了更好地理解SQL查询效率是怎么一回事今天我们就来看看磁盘I/O是如何加载数据的。
这部分的内容主要包括以下几个部分:
1. 数据库的缓冲池在数据库中起到了怎样的作用?如果我们对缓冲池内的数据进行更新,数据会直接更新到磁盘上吗?
1. 对数据页进行加载都有哪些方式呢?
1. 如何查看一条SQL语句需要在缓冲池中进行加载的页的数量呢
## 数据库缓冲池
磁盘I/O需要消耗的时间很多而在内存中进行操作效率则会高很多为了能让数据表或者索引中的数据随时被我们所用DBMS会申请占用内存来作为数据缓冲池这样做的好处是可以让磁盘活动最小化从而减少与磁盘直接进行I/O的时间。要知道这种策略对提升SQL语句的查询性能来说至关重要。如果索引的数据在缓冲池里那么访问的成本就会降低很多。
那么缓冲池如何读取数据呢?
缓冲池管理器会尽量将经常使用的数据保存起来,在数据库进行页面读操作的时候,首先会判断该页面是否在缓冲池中,如果存在就直接读取,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进行读取。
缓存在数据库中的结构和作用如下图所示:
<img src="https://static001.geekbang.org/resource/image/05/9b/05e1692282d25768c87bbc31a1479b9b.png" alt=""><br>
如果我们执行SQL语句的时候更新了缓存池中的数据那么这些数据会马上同步到磁盘上吗
实际上当我们对数据库中的记录进行修改的时候首先会修改缓冲池中页里面的记录信息然后数据库会以一定的频率刷新到磁盘上。注意并不是每次发生更新操作都会立刻进行磁盘回写。缓冲池会采用一种叫做checkpoint的机制将数据回写到磁盘上这样做的好处就是提升了数据库的整体性能。
比如当缓冲池不够用时需要释放掉一些不常用的页就可以采用强行采用checkpoint的方式将不常用的脏页回写到磁盘上然后再从缓冲池中将这些页释放掉。这里脏页dirty page指的是缓冲池中被修改过的页与磁盘上的数据页不一致。
### 查看缓冲池的大小
了解完缓冲池的工作原理后,你可能想问,我们如何判断缓冲池的大小?
如果你使用的是MySQL MyISAM存储引擎它只缓存索引不缓存数据对应的键缓存参数为key_buffer_size你可以用它进行查看。
如果你使用的是InnoDB存储引擎可以通过查看innodb_buffer_pool_size变量来查看缓冲池的大小命令如下
```
mysql &gt; show variables like 'innodb_buffer_pool_size'
```
<img src="https://static001.geekbang.org/resource/image/93/16/93b1b8acaf39f46245c6b751200e8316.png" alt=""><br>
你能看到此时InnoDB的缓冲池大小只有8388608/1024/1024=8MB我们可以修改缓冲池大小为128MB方法如下
<img src="https://static001.geekbang.org/resource/image/78/70/78fd83a2ff4f0a10ed20ec147ffbac70.png" alt=""><br>
然后再来看下修改后的缓冲池大小此时已成功修改成了128MB
<img src="https://static001.geekbang.org/resource/image/a3/cc/a364961bda6d5c4742460620053ec1cc.png" alt=""><br>
在InnoDB存储引擎中我们可以同时开启多个缓冲池这里我们看下如何查看缓冲池的个数使用命令
```
mysql &gt; show variables like 'innodb_buffer_pool_instances'
```
<img src="https://static001.geekbang.org/resource/image/d1/27/d1f7896087c58bfef202f9a21e96ed27.png" alt="">
你能看到当前只有一个缓冲池。实际上`innodb_buffer_pool_instances`默认情况下为8为什么只显示只有一个呢这里需要说明的是如果想要开启多个缓冲池你首先需要将`innodb_buffer_pool_size`参数设置为大于等于1GB这时`innodb_buffer_pool_instances`才会大于1。你可以在MySQL的配置文件中对`innodb_buffer_pool_size`进行设置大于等于1GB然后再针对`innodb_buffer_pool_instances`参数进行修改。
### 数据页加载的三种方式
我们刚才已经对缓冲池有了基本的了解。
如果缓冲池中没有该页数据,那么缓冲池有以下三种读取数据的方式,每种方式的读取效率都是不同的:
**1. 内存读取**
如果该数据存在于内存中基本上执行时间在1ms左右效率还是很高的。
<img src="https://static001.geekbang.org/resource/image/a5/4f/a5d16af8d34ebdcfe327ef7b4841ad4f.png" alt=""><br>
**2. 随机读取**
如果数据没有在内存中就需要在磁盘上对该页进行查找整体时间预估在10ms左右这10ms中有6ms是磁盘的实际繁忙时间包括了寻道和半圈旋转时间有3ms是对可能发生的排队时间的估计值另外还有1ms的传输时间将页从磁盘服务器缓冲区传输到数据库缓冲区中。这10ms看起来很快但实际上对于数据库来说消耗的时间已经非常长了因为这还只是一个页的读取时间。
<img src="https://static001.geekbang.org/resource/image/50/49/50fb2657341103548a76fce6f7769149.png" alt=""><br>
**3. 顺序读取**
顺序读取其实是一种批量读取的方式因为我们请求的数据在磁盘上往往都是相邻存储的顺序读取可以帮我们批量读取页面这样的话一次性加载到缓冲池中就不需要再对其他页面单独进行磁盘I/O操作了。如果一个磁盘的吞吐量是40MB/S那么对于一个16KB大小的页来说一次可以顺序读取256040MB/16KB个页相当于一个页的读取时间为0.4ms。采用批量读取的方式,即使是从磁盘上进行读取,效率也比从内存中只单独读取一个页的效率要高。
## 通过last_query_cost统计SQL语句的查询成本
我们先前已经讲过一条SQL查询语句在执行前需要确定查询计划如果存在多种查询计划的话MySQL会计算每个查询计划所需要的成本从中选择成本最小的一个作为最终执行的查询计划。
如果我们想要查看某条SQL语句的查询成本可以在执行完这条SQL语句之后通过查看当前会话中的last_query_cost变量值来得到当前查询的成本。这个查询成本对应的是SQL语句所需要读取的页的数量。
我以product_comment表为例如果我们想要查询comment_id=900001的记录然后看下查询成本我们可以直接在聚集索引上进行查找
```
mysql&gt; SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE comment_id = 900001;
```
运行结果1条记录运行时间为0.042s
<img src="https://static001.geekbang.org/resource/image/da/fd/da50c6f039b431fbab963be16f16b5fd.png" alt=""><br>
然后再看下查询优化器的成本,实际上我们只需要检索一个页即可:
```
mysql&gt; SHOW STATUS LIKE 'last_query_cost';
```
<img src="https://static001.geekbang.org/resource/image/cd/43/cde712f5d9fc42f31310511677811543.png" alt=""><br>
如果我们想要查询comment_id在900001到9000100之间的评论记录呢
```
mysql&gt; SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE comment_id BETWEEN 900001 AND 900100;
```
运行结果100条记录运行时间为0.046s
<img src="https://static001.geekbang.org/resource/image/be/a0/be4d6cd476fd0c9898df5f785aa0c0a0.png" alt=""><br>
然后再看下查询优化器的成本这时我们大概需要进行20个页的查询。
```
mysql&gt; SHOW STATUS LIKE 'last_query_cost';
```
<img src="https://static001.geekbang.org/resource/image/62/fe/628138aa22d3244f8d08dc701a1f61fe.png" alt=""><br>
你能看到页的数量是刚才的20倍但是查询的效率并没有明显的变化实际上这两个SQL查询的时间基本上一样就是因为采用了顺序读取的方式将页面一次性加载到缓冲池中然后再进行查找。虽然页数量last_query_cost增加了不少但是通过缓冲池的机制并没有增加多少查询时间。
## 总结
上一节我们了解到了页是数据库存储的最小单位这一节我们了解了在数据库中是如何加载使用页的。SQL查询是一个动态的过程从页加载的角度来看我们可以得到以下两点结论
1. 位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。
1. 批量决定效率。如果我们从磁盘中对单一页进行随机读那么效率是很低的差不多10ms而采用顺序读取的方式批量对页进行读取平均一页的读取效率就会提升很多甚至要快于单个页面在内存中的随机读取。
所以说遇到I/O并不用担心方法找对了效率还是很高的。我们首先要考虑数据存放的位置如果是经常使用的数据就要尽量放到缓冲池中其次我们可以充分利用磁盘的吞吐能力一次性批量读取数据这样单个页的读取效率也就得到了提升。
<img src="https://static001.geekbang.org/resource/image/f2/77/f254372aac175d6ac571ebe9ec024777.jpg" alt=""><br>
最后给你留两道思考题吧。你能解释下相比于单个页面的随机读,为什么顺序读取时平均一个页面的加载效率会提高吗?另外,对于今天学习的缓冲池机制和数据页加载的方式,你有什么心得体会吗?
欢迎在评论区写下你的答案,如果你觉得这篇文章有帮助,不妨把它分享给你的朋友或者同事吧。