关系型数据库进阶之查询管理

在前面几篇文章中,我们已经介绍了总体架构以及客户端管理,今天我们来继续介绍查询管理。

query manager in databases

毫无疑问,查询管理是数据库的核心中的核心,也是最重要最难的地方。这个部分,哪怕是写的不好的query也会被转换成尽量快的方式来进行执行,在执行之后会把结果在返回给客户端管理。主要有一下几个步骤:

  • 解析query,看是否有效
  • 重写相应的query,去除一些没有必要的操作,主要是做预优化
  • 优化相应的查询,主要是提高性能,转化成高效的执行计划和数据访问计划
  • 编译计划
  • 最后,执行计划

我们主要来讨论一下前三个部分,因为他们基本就是query manager最核心的地方了。

query解析

所有的SQL语句都会发送到解析器来进行语法的检查。query语句中的任何语法错误都会导致query的结束,比如你把SELECT写成了SLECT,那么语句就会在这里的检查结束并报错。当然也不是仅仅这些简单的检查,还包括一些复杂的,比如你把WHERE写到了SELECT之前,那么这里也会报错。

紧接着会检查相关metadata,包括:

  • 相应的表是否存在
  • 相应的域是否在表中存在
  • 对应的操作在对应的列数据类型上是否有效(比如你不可以把一个整数和字符串相比较,或者你不能在整数上执行substring()操作等)

做完这些检查后,它会检查你是否有相应的表格的读写权限。

假如这些检查都ok,那么相应的SQL query会被转换成内部的表示形式(通常是一个tree),然后会被送到query rewriter中。

Query重写

在Query解析之后,会把相应的内部表示形式传到Query重写这里,那么这里需要的操作就是:

  • 预优化query
  • 避免不必要的操作
  • 帮助优化器找到最佳的方案

Query的重写其实就是把一系列预定义的规则在这个query上运行,假如这个query符合相应的规则,那么就根据这个规则进行重写。下面是一些可选规则:

  • View的合并:假如你在query中使用view,那么就会把view的代码合并到相应的SQL语句中
  • 子查询的展平:有子查询之后,是非常难进行优化的,所以这里会尽量把子查询展平了用来取代相应的子查询。举例如下:
	SELECT PERSON.*
	FROM PERSON
	WHERE PERSON.person_key IN
	(SELECT MAILS.person_key
	FROM MAILS
WHERE MAILS.mail LIKE 'christophe%');

这个语句会被转换成下面这个语句:

	SELECT PERSON.*
	FROM PERSON, MAILS
	WHERE PERSON.person_key = MAILS.person_key
and MAILS.mail LIKE 'christophe%';
  • 去除不必要的操作:比如你使用了DISTINCE,但是你又有一个UNIQUE的限制来保证数据不会重复,那么DISTINCT的关键词就会被去除。
  • 消除冗余连接:比如你有两个一样的join条件,只是有一个条件被隐藏在一个view中或者别的不容易注意的地方,那么这个同样的join条件其实就是冗余的,这里会去除掉。
  • 常数的算术评估:假如你写了一些需要计算的公式,那么它就会在这一步中计算。比如WHERE AGE > 10 +2,在这里会变改写成WHERE AGE > 12等等。
  • (高阶)Partition的选择:假如你使用的是Partition的表,那么这里会找到应该使用哪个partition
  • (高阶)物化视图(Materialized view)重写:假如你有一个物化视图是和query的预测相匹配的,那么重写器会检查你的view是不是最新的,假如是的话,会使用相应的物化视图来替代对应的原始表。
  • (高阶)自定义规则:假如有自定义的规则(比如Oracle policies),会执行相应的规则
  • (高阶)olap的转换:分析/窗口功能,start join, rollup等等,不过这个部分究竟是在重写这里执行还是在后面优化部分执行,取决于具体的数据库,不同的数据库会有不同的实现方法。

Query重写会把相应的结果发送到Query的优化器中,而那就是我们需要重点关注的地方。

统计

在分析query的优化之前,我们需要了解一下统计,没有统计就谈不上优化,因为你如果不知道具体的数据情况,如何才能做优化呢。那么什么样的数据是数据库需要的呢?

这里首先有一个概念就是操作系统如何存储数据库的数据的,通常来说有一个最小的数据单元,称之为页或者块(4k或者8k的默认大小)。这也就是假如你只有1k的数据,你也会占用一个页,也就意味着你会浪费7K的数据(假如一页是8K)。

好了,我们再回到统计的角度,当它进行统计的时候,会收集下面这些数据:

  • 表中的行数和页数
  • 表中每一列:
    • 不同的数据值
    • 数据值的长度(最大,最小,平均)
    • 数据的范围(最大,最小,平均)
  • 表的index的信息

这些统计可以帮助优化器来预估一个query需要消耗的磁盘I/O,CPU和内存情况。

每一个列的统计数据是非常重要的,举个简单的例子来说,假如我们有一个PERSON的表,然后有两个列:LAST_NAME和FIRST_NAME。有了统计数据之后,我们知道了FIRST_NAME其实只有1000个不同的值,而LAST_NAME有1000000个不同的值,那么在我们执行一个用LAST_NAME和FIRST_NAME进行join的语句时,我们会使用LAST_NAME, FIRST_NAME而不会使用FIRST_NAME, LAST_NAME,这是因为我们知道LAST_NAME有很多不同的值,所以通常只要比较前面几个字符就可以得到结果了,这样一来比较的次数就会变少很多。

当然上面这些只是基本的统计,数据库还可以创建一些高阶的统计,比如直方图。直方图是用来表示列中数据分别的统计方法,比如:

  • 最常见的值
  • 各个分位数

这些统计都会有助于数据库去发现更好的查询计划。尤其对于量的比较(比如WHERE AGE = 18)或者范围的查询(WHERE AGE > 10 and WHERE AGE < 40)。

这些统计数据一般都保存在数据库中的metadata中,比如你可以在表的下面部分中看到对应的统计数据:

  • Oracle中的USER/ALL/DBA_TABLES and USER/ALL/DBA_TAB_COLUMNS
  • DB2中的SYSCAT.TABLES以及SYSCAT.COLUMNS

另外需要注意的是,统计的数据要及时更新,假如有一个表格有1000000行,而统计中只有500行,这大概就是最糟糕的情况了。但这里有一个问题,就是统计其实是很耗时间的。这也是为什么大多数数据库中都不会自动计算。尤其是你有百万的数据,这些计算就更加复杂了。这种情况下,也许只计算一些基础的统计会是一个更好的选择。

当然,各个数据库还有很多其独有的高级统计,你可以阅读相应的数据库文档来了解其中的内容。

Query的优化

Query的优化是一个很复杂的内容,我们将会单独写一篇文章来详细聊一聊,这里只占个位。

Query的执行

到了这一步,我们已经有了优化过的执行计划,这个计划就被编译以后用来执行了。只有足够的资源(内存,CPU),Query的执行器就会执行相应的计划。执行计划中的各种操作可以串行执行也可以并行执行,主要还是由执行器来决定。执行中的获取和写入数据,就是通过数据管理来进行的,这也是我们下一章要讲的内容。

至此,Query的管理就讲好了,除了Query的优化单开一章外,其它内容就全部解释清楚了。

参考文章:http://coding-geek.com/how-databases-work/

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *