0%

PostgreSQL优化器白话(8) - 算计不到就受穷

“俗话说啊,吃不穷,穿不穷,算计不到就受穷。”大明一边啃着大腰子,一边说:“所以该吃就得吃,吃是吃不穷的。”

小明说:“可是算来算去,物理路径的代价还是有选不准的时候啊。”

牛二哥说:“小明你已经走火入魔了,我们正在美美的吃饭,你看大明啃得竹签子都冒火星子,你非要扯到优化器,太扫兴了,好了,我吃饱了,我来和你聊聊。”说着牛二哥抽了张纸巾擦了擦嘴,纸巾被嘴角的油浸成透明状,飘悠悠的被牛二哥弹进了垃圾筐,然后牛二哥抚摸着自己沟满壕平的肚子,慢条斯理的说:“最优路径选的不准是谁的原因,那就是代价模型不行啊,代价模型不行赖谁,那就是程序员没建好啊,所以要怪就要怪到程序员自己头上。”

小明问道:“可是我看PostgreSQL数据库的代价计算已经很复杂了啊?”

“可是数据库的周边环境更复杂啊。你想想,在实际应用中,数据库用户的配置硬件环境千差万别,CPU的频率、主存的大小和磁盘介质的性质都会影响执行计划在实际执行时的效率。”牛二哥说完,喝了一口红酒。

大明接过来继续说道;“虽然在代价估算的过程中,我们无法获得‘绝对真实’的代价,但是‘绝对真实’的代价也是不必要的,因为我们只是想从多个路径(Path)中找到一个代价最小的路径,只要这些路径的代价是可以‘相互比较’的就可以了,因此可以设定一个‘相对’的代价的单位1,同一个查询中所有的物理路径都基于这个“相对”的单位1来计算的代价,这样计算出来的代价就是可以比较的,也就能用来对路径进行挑选了。”

然后大明给牛二哥递了根中华烟,说:“饭后一根烟,赛过活神仙,来,抽一根。”牛二哥接过烟,大明把火递了过来,牛二哥点上了烟,把打火机扔到茶几上,然后深吸了一口,喷云吐雾的说:“PostgreSQL数据库采用顺序读写一个页面的IO代价作为单位1,而把随机IO定位了顺序IO的4倍”

小明说:“我知道,我知道,这个我查过相关的书,首先,目前的存储介质很大部分仍然是机械硬盘,机械硬盘的磁头在获得数据库的时候需要付出寻道时间,如果要读写的是一串在磁盘上连续的数据,就可以节省寻道时间,提高IO性能,而如果随机读写磁盘上任意扇区的数据,那么会有大量的时间浪费在寻道上。其次,大部分磁盘本身带有缓存,这就形成了主存->磁盘缓存->磁盘的三级结构,在将磁盘的内容加载到内存的时候,考虑到磁盘的IO性能,磁盘会进行数据的预读,把预读到的数据保存在磁盘的缓存中,也就是说如果用户只打算从磁盘读取100个字节的数据,那么磁盘可能会连续的读取磁盘中的512字节(不同的磁盘预读的数量可能不同)并将其保存到磁盘缓存,如果下一次是顺序读取100个字节之后的内容,那么预读的512字节的数据就会发挥作用,性能会大大的增加,而如果读取的内容超出了512字节的范围,那么预读的数据就没有发挥作用,磁盘的IO性能就会下降。”说完小明得意的说:“怎么样,我说的对吧?”

牛二哥说:“你说的对,目前PostgreSQL的查询优化大量的考虑了随机IO和顺序IO所带来的性能差别,在这方面做了不少优化,但是现在的磁盘技术越来越发达了,以后随机IO和顺序IO是不是还差这么多,就值得商榷了。”

“那到底还有那些代价基准单位呢?”小明继续问道。

大明回答道:“基于磁盘IO的代价单位当然就是和Page有关的了,也就是说我们刚才说的顺序IO和随机IO都属于IO方面的基准代价。让后让牛二哥给你介绍一下CPU方面的代价基准单位,我先去吃个鸡。”

牛二哥说:“CPU方面的基准单位有哪些呢?比如说我们通过IO把磁盘页面读到了缓存,但我们要处理的是元组啊,所以还需要把元组从页面里解出来,还要处理元组,这部分主要消耗的是CPU,所以会有一个元组处理的代价基准单位,另外,我们在投影、约束条件里有大量的表达式,这些表达式求解也主要消耗CPU资源,所以还有一个表达式代价的基准单位。”

牛二哥弹了弹烟灰,继续说道:“现在PostgreSQL数据库增加了很多并行路径,因此它也产生了通信代价,这个也需要计算的。”

小明听了之后,说:“那我们就能得到一个这样的公式。”说着在纸上写了一个公式:

1
总代价 = CPU代价 + IO代价 + 通信代价

然后小明继续说:“可是我通过EXPLAIN还查看过PostgreSQL的执行计划,我从执行计划中还看到有启动代价和总代价,这是怎么回事呢?”

牛二哥听了之后,想了想,在纸上写了一个公式:

1
总代价 = 启动代价 + 执行代价

然后牛二哥说:“这是从另一个角度来计算代价,启动代价是指从语句开始执行到查询引擎返回第一条元组的代价(另一种说法是准备好去获得第一条元组的代价),总代价是SQL语句从开始执行到结束的所有代价。”

“可是。。。为什么要区分启动代价和执行代价呢?”

“这个嘛。。。。”牛二哥思考了一下,觉得一句两句不容易说清楚,于是写了个例子:

1
SELECT * FROM TEST_A WHERE a > 1 ORDER BY a LIMIT 1;

“我们假设这个在TEST_A(a)上有一个B树索引,晓得不,那这个语句可能会形成什么样的执行计划呢?”

小明想了想,觉得空想可能有点困难,于是在纸上画了一起,最终他画了两个执行路径:

1
2
3
4
5
6
执行路径1:LIMIT 1
-> SORT(a)
-> SeqScan WHERE A > 1;
执行路径2:LIMIT 1
-> IndexScan WHERE A > 1;
(小明注:B树索引有序,不用再排序了)

小明说:“我觉得这两个都可以,不过我觉得第二个更好,因为它节省了排序的时间。”

牛二哥问:“你知道的,PostgreSQL数据库采用动态规划的方法来实现路径的搜索,它是一种自底向上的方法,也就是说会先建立筛选扫描路径,然后用筛选后的扫描路径再去形成连接路径,那么在我们筛选扫描路径的时候,是不知道它的上层有没有LIMIT的,这时候如果单独看SeqScan + SORT和IndexScan你觉得哪个好呢?”

“嗯,我知道陷阱在哪里,大明和我说过,A > 1的选择率高的话会选择顺序扫描,而A > 1的选择率低的情况下,会选择索引扫描,这是因为索引扫描会产生随机IO,也就是说在选择率高的情况下,有可能SeqScan + SORT会优于IndexScan,虽然SeqScan + SORT会有排序,但是IndexScan的随机IO实在是太可观了。”

牛二哥点了点头,说:“对的,假设选择率比较高,这时候选择了SeqScan + SORT,是因为它不知道再上层是LIMIT 1,如果上面是LIMIT 1,就会导致索引扫描不用全部扫完,只要扫一丢丢就可以了,这时候随机IO就很小了,但是SeqScan + SORT就还必须全部执行完才能获取到LIMIT 1,也就是说SeqScan + SORT、或者说SORT要获取第一条元组的启动代价是比较高的,如果上面有LIMIT 1这样的子句,那么启动代价高的路径可能就没有优势了,这就是启动代价的作用。”

小明恍然大悟,说:“SORT要全部做完才能获取第一条元组,它的启动代价大,但是总代价小,而索引扫描呢,因为本身有序,它的启动代价是小的,但是由于有随机IO,所以它的总代价是大的,如果我们只按照总代价进行筛选,就没办法获得最优的代价了。”

“什么什么?启动代价。。。你们进展很快嘛。”这时大明跑过来,说:“让我们想一下晚上吃点什么吧?”

小明:“吃点好的,很有必要。我这脑细胞已经快用没了。”