SQL Server – 执行计划和各种 join 方式 (Execution plan & Join Pattern)

前言

我几乎从来没有遇到过性能问题, 毕竟项目真的太小了. 一般上只要用常识去建 index 就可以了.

但是这并不阻碍我对知识的追求. 这篇是关于一些性能优化和原理的内容. 纯属学习, 希望未来有机会用到.

 

参考

SQL Server – 树结构 (二叉树, 红黑树, B-树, B+树) (必看)

Fish Li – 看懂SqlServer查询计划 (必看)

Sql Server中的表访问方式Table Scan, Index Scan, Index Seek (必看)

预读, 物理读, 逻辑读 (必看)

YouTube – Join Pattern (必看)

YouTube – How do SQL Indexes Work

 

What, When, Why?

什么是 Execution Plan?

execution plan 里头包含了 query 执行时的各种 information, 比如 IO 速度, 查找了多少 rows 等等

为什么要看 Execution Plan?

当 query 慢的时候, 可以通过分析 execution plan, 知道它为什么慢, 然后做优化.

怎样优化?

优化的方法有很多, 但绝大部分的情况加 index (索引) 就可以了.

总结

当我们发现 query 慢的时候就查看 execution plan, 然后添加 index, 通常它就会变快了.

 

Step by Step

开启 execution plan

跑 query

select * from Test where FirstName = '781F9AB8';

看结果

execution plan 的阅读方式是 右到左 上到下

Table Scan

table scan 是最慢的, 它就是把所有 data 都 read 出来一个一个匹配, 完全没有利用到 B+ tree 的优势.

遇到这种情况就加 index.

query "where FirstName" 想查找的是 FirstName, 那就加 index 到 FirstName.

Non Clustered Index Scan

假设我们在 FirstName 加了 non clustered index.

然后 query

select FiestName from Test;

注意, 我没有放 where 条件.

结果

这里用了一个 non clustered index scan. 虽然有 index, 但其实是很慢的. 因为 scan 就表示全部扫一遍.

只是从扫 table data 变成了扫 index. index 比 table data 小, 扫起来确实会快一些, 但依然没有利用到 B+ tree 的优势.

Clustered Index Scan

有 scan 就表示扫到完. 自然它也快不到哪去. 而且 clustered index 意味着它就是 data. 所以 clustered index scan 和 table scan 可以说是等价的.

只有在一个表没有 clustered index 的情况下 (heap table) 它才会是 table scan, 一旦有 clustered index 那就是 clustered index scan.

p.s 图中的 Parallelism 是并行执行的意思, 利用了多核 CPU

Index Seek 和 Key Lookup

在 FirstName 加上 index 以后, query 带 where

select * from Test where FirstName = 'Derrick';

结果

index seek 表示用到了 index 查找, 这个才真的有善用 B+ Tree 优势. 通常优化到这个 level 就能接受了.

另外, 之所以会有 key lookup 是因为 我 select all

B+ Tree 的 data 是放在最低层树叶的. 通过上层的 index 虽然定位到了 data. 但是依然需要去最底层 read data. 

这个 key lookup 指的就是这个操作过程. 

如果我把 query 换成

select FirstName from Test where FirstName = '781F9AB8';

那这个 key lookup 就没了. 因为 index 已经有 FirstName data, 就不用再到叶子节点 read data 了.

我们也可以把更多 data 写入 index 来优化掉 key lookup (但通常是没有必要的, 因为 key loopup 也没有很慢)

p.s 还有一个词叫 RID lookup (row id 的意思), 它和 key loopup 差不多意思, 只是它出现在 heap table.

Clustered Index Seek

select * from Test where FirstName = '781F9AB8';

结果

clustered index seek 是最快的. 即使是 select all 也不需要 key lookup 就可以直接定位获取 data 了.

clustered index 和 non clustered index 的区别是, clustered 是 data 本身在磁盘物理的排序. 一个表只能有一个 clustered index 一种排法.

non clustered index 则是目录只有 key 没有 data, 一个表可以做很多目录, 查找时先找目录, 然后再去 read data.

总结

通常

table scan > clustered index scan > index scan > index seek > clustered index seek

scan 表示扫到完, 不管是扫 data 还是 index 都慢

seek 表示利用了 B+ Tree 二分查找, 所以快

loopup 是因为 non clustered index 只保存了 target column data, 其余 row column data 在叶节点, 所以 seek 了之后还需要 read other data from leaf.

为什么说是 "通常" 呢? 

因为并不是所有情况下都是这样的. 有时候表小, 直接 scan table 反而更快.

又比如 bit column, 就是男/女. 这种情况你即使 seek 用上了 B+ Tree 二分查找也快不到哪去. 

所以我们做优化的步骤是先看哪一个 query 慢.

快的 query, 哪怕它是 table scan 也不用管它.

发现慢的 query 后, 就看它是 scan 还是 seek. 然后尝试加 index 让它变成其它的方式. then 在跑跑看是否有提速.

 

预读, 物理读, 逻辑读 (read-ahead reads, logical reads, physical reads)

除了看 execution plan 的 scan, seek 这些. 还有一个重要的指标是看缓存.

我们知道 IO 是很慢的 (从磁盘读取数据), 因为这个是物理操作, 需要移动磁头,

而从内存或缓存中读取数据是很快的, 因为不算物理操作, 它只是通通电.

所以我们要尽量确保 query 的时候不要从磁盘读取数据.

Clear cache

为了测试方便, 我们每次 query 的时候先 clear 干净 cache

DBCC DROPCLEANBUFFERS --清空执行计划缓存
DBCC FREEPROCCACHE --清空数据缓存

开启 statistics (侦测工具)

SET STATISTICS IO ON; -- OFF 就关闭

query

select * from Test where FirstName = '0000007B';

结果

SQL Server 读取最小单位是 page, 一个 page = 8kb. 

physical reads 3 表示从磁盘读了 3 次, 也就是 3 x 8kb = 24kb 的数据.

logical reads 是从缓存读取

read-ahead reads 是预读. 当 query 发出后, SQL Server 会先制作执行计划, 与此同时为了不浪费时间, 它会先去预读一些数据.

当执行计划做好后, 就执行. 这时如果 logical reads 不能满足所有结果, 那么就会去 physical reads.

当我们第二次运行相同 query 就会发现 physical reads 0. 因为之前的数据已经被缓存了 (除非我们跑上面的 clear cache command)

总结

physical reads 太多肯定就慢, 至于如何优化...我不知道, 看着办呗.

 

Join and Loop Pattern

除了 read row data, 连表也很讲究性能. SQL Server 有 3 种连表方式.

这 3 种方式在不同的前提下, 性能表现会不一样, SQL Server 会依据不同情况选择不同的方式.

比如表大小, 表是否已经有排序等等

Nested Loop Join

nested loop join 的过程是 for loop A 表, 拿每一条 row 再去 for loop B 表做 match.

假设 A, B 表都有 100 rows. 那么 100 x 100 就需要 10000 次 compare.

这种方式适合用在 A 表比较小, B 表有 index 的情况. 这 2 点可以大大降低找的次数.

Merge Join

merge join 的过程是先拿 AB 表做一个排序 (注意排序是很慢的)

然后 loop A 表, 拿 row 去 match B 表.

由于有排序了, 所以不需要检索所有的 B rows. 只要匹配到比较大的就可以回到 A 表下一个了.

这种方式适合用在 AB 表已经有排序好的情况 (比如 AB 表都有相同的 index)

Hash Join

hash join 的过程是先拿 AB 表每一条 row 做哈希算法.然后放入哈希表.

哈希的特色是可以算出位置. 相同位置的就表示同值.

这种方式适合用在, 你需要完全找出 AB 表所有匹配的情况. 因为有些时候我们可能只需要找出几个 A 表匹配. 

那么 nested loop 会比较合适. hash 一定要全部 row 都做完才能出结果, 不像 nested loop 找一个是一个.

总结

三种方式都有它适合的场景. 同样的, 我们做优化首先是看它是否慢, 然后才想办法 (加 index) 让它换一个方式试试看是否提速.

 

小 tips

在做分析时, 经常需要一些 command

DBCC DROPCLEANBUFFERS --清空执行计划缓存
DBCC FREEPROCCACHE --清空数据缓存
SET STATISTICS IO ON; 
SET STATISTICS TIME ON;

left join TestChild WITH (INDEX([IX_TestChild_TestId])) --强制使用 index

-- 强制 join pattern
left hash join
left merge join
left loop join

 

posted @ 2023-03-31 12:37  兴杰  阅读(151)  评论(0编辑  收藏  举报