在 DB2 优化器中使用分布统计信息
简介
为了执行查询或 DML 语句(INSERT
、UPDATE
、DELETE
),DB2 必须创建一个访问计划(access plan)。访问计划定义按什么顺序访问表,使用哪些索引,以及用何种连接(join)方法来关联数据。好的访问计划对于 SQL 语句的快速执行至关重要。DB2 优化器可以创建访问计划。这是一种基于成本的优化器,这意味着它是根据表和索引的相关统计信息来作出决策的。DB2 在生成统计信息时,不但能提供基本统计信息,还允许创建所谓的分布统计信息。不但数据库管理员要理解分布统计信息,而且应用程序开发人员也要理解分布统计信息。应用程序开发人员必须小心谨慎,因为在某些情况下分布统计信息对于 DB2 优化器来说非常重要。主变量或参数标记(在 Java 中为 java.sql.PreparedStatement
)的使用可能会造成阻碍,使优化器无法最大限度地利用分布统计信息。本文解释什么是分布统计信息、分布统计信息在哪些情况下尤为重要,以及应用程序开发人员应该考虑些什么,才能使 DB2 优化器创建有效的访问计划。
基本统计信息和分布统计信息
在研究分布统计信息之前,我们先来看看基本统计信息,只要执行 RUNSTATS
即可收集这些表的相关统计信息。
表的相关统计信息:
- 当前使用的页面数
- 包含记录行的页面数
- 溢出的行数
- 表中的行数(基数)
- 对于 MDC 表,还有包含数据的块(block)数
表中各列的相关统计信息:
- 列的基数
- 列的平均长度
- 列中第二大的值
- 列中第二小的值
- 列中 NULL 值的个数
通常,执行 RUNSTATS
时,不但可以收集到关于表的统计信息,而且还可以收集到相应的索引的相关统计信息。要了解为索引而收集的统计信息,请参阅 DB2
Administration Guide: Performance - Statistical information that is collected。
观察一个表的基本统计信息,您可以看到,DB2 优化器知道一个表由多少行组成(表的基数),以及一个列包含多少个不同的值(列的基数)。但是,还有一些信息是基本统计信息无法提供的。例如,基本统计信息不能告诉优化器一个列中某些值出现的频率。假设表 TABLE_X 有大约 1,000,000 行,在该表上执行这样一条查询:
SELECT * FROM TABLE_X WHERE COLUMN_Y = 'VALUE_Z'
难道 DB2 优化器知道 TABLE_X 中有多少行满足条件 COLUMN_Y = 'VALUE_Z'
实际上,通过基本统计信息,DB2 优化器只能估计 'VALUE_Z'
在 COLUMN_Y
中出现的频率。在这种情况下,优化器认为所有值在 COLUMN_Y
中是平均分布的,这意味着它认为所有的值都有相同的出现频率。如果事实碰巧如此,这样估计并无大碍。但是,如果有些值比其他值出现得更频繁一些(例如,如果 'VALUE_Z'
分布统计信息----频率分布统计信息和分位数分布统计信息
有两种不同类型的分布统计信息 —— 频率统计信息和分位数统计信息。让我们通过一个示例表来研究一下这两种不同类型的分布统计信息。
示例表 “CARS” 表示一家汽车制造商,对于生产的每一辆汽车,在表中都有相应的一行。每辆汽车可以由它的 ID 来标识,因此 “ID” 是表 “CARS” 的主键(PK)。此外,表中有一个 “STATE” 列,表明汽车当前处在制造流程中的哪一步。一辆汽车的制造流程从第 1 步开始,然后是第 2 步、第 3 步,...、第 49 步、第 50 步、第 51 步、...、第 98 步、第 99 步,一直到第 100 步 —— 第 100 步意味着汽车已经完工了。已完工的汽车所对应的行仍然保留在表中,后续流程(例如投诉管理、质量保证等)仍要用到这些行。汽车制造商生产 10 种不同型号(“TYPE” 列)的汽车。为了简化问题,在这个示例表中,各种汽车型号命名为 A、B、C、D、...、J。除主键索引(在 “ID” 列上)之外,“STATE” 列上也有一个索引(“I_STATE”),在 “TYPE” 列上还有一个索引(“I_TYPE”)。实际上,一个 “CARS” 表包含的列远不止 “ID”、“STATE” 和 “TYPE”。为简单起见,示例表中没有出现其他这些列。
频率分布统计信息
假设表 CARS 现在有大约 1,000,000 条记录,不同的型号在表中出现的频率如下所示:
萨1.
表 CARS 中 TYPE 列的频率统计信息
TYPE | COUNT(TYPE) |
A | 506135 |
B | 301985 |
C | 104105 |
D | 52492 |
E | 19584 |
F | 10123 |
G | 4876 |
H | 4589 |
I | 4403 |
J | 3727 |
型号为 A 的汽车最受购买者的青睐,因此生产的汽车中大约有 50% 是这种型号。型号 B 和型号 C 仅次于型号 A ,分别占所有汽车的 30% 和 10%。其他所有型号加在一起仅占 10%。
上面的表显示了 “TYPE” 列的频率统计信息。通过基本统计信息,DB2 优化器只能了解到该表包含 1,000,000 行(表的基数)和 10 种不同的值(型号),即 A 到 J。如果没有分布统计信息,优化器会认为每种值以相同的频率出现,大约都是出现 100,000 次。而一旦生成了关于 “TYPE” 列的分布统计信息,优化器即可了解每种型号真正的出现频率。因此,优化器清楚各种已有型号出现的不同频率。
优化器使用频率统计信息来计算用于检查相等或不等的谓词的过滤因子。例如:
SELECT * FROM CARS WHERE TYPE = 'H';
分位数分布统计信息
与频率统计信息不同,分位数统计信息与不同值的出现频率无关,而与一个表中有多少行小于或大于某个值(或者有多少行介于两个值之间)相关。分位数统计信息提供关于一个列中的值是否聚合的信息。为获得这样的信息,DB2 假定列中的值是按升序排列的,并根据正则行间隔确定相应的值。
我们来看看表 CARS 中的 “STATE” 列,该列按升序排列。根据正则行间隔,即可确定 “STATE” 的对应值。
2CARS
表中 STATE 列的分位数统计信息
COUNT(row) | STATE ASC |
5479 | 1 |
54948 | 10 |
109990 | 21 |
159885 | 31 |
215050 | 42 |
265251 | 52 |
320167 | 63 |
370057 | 73 |
424872 | 84 |
475087 | 94 |
504298 | 100 |
... | 100 |
1012019 | 100 |
由于已完工的汽车仍然没有从表中删除,因此状态为 100 (=完工)的汽车数量比所有处于其他状态的汽车总和还多。已完工的汽车占表中所有记录的 50%。
注意: 在实际情况下,已完工的汽车数量甚至还要更多(例如超过 99%)。在后文中的具体例子中可看到这种情况。
上表显示了 “STATE” 列的分位数统计信息。有了这种关于有多少行分别小于和大于确定值的信息,优化器即可计算出用于测试小于(小于等于)、大于(大于等于)或介于两值之间的谓词的过滤因子。例如:
SELECT * FROM CARS WHERE STATE < 100
SELECT * FROM CARS WHERE STATE BETWEEN 50 AND 70
根据已有的分位数统计信息计算出来的过滤因子不是很精确,但即使只收集 20 个值,其误差仍然低于 5%。
DB2优化器对分布统计信息的使用--示例
如何创建访问计划
要为 SQL 语句创建访问计划,最简单的方法就是使用 Visual Explain Tool,这是 DB2 Command Center 中的一个工具。如果您想使用命令行创建一个更详细的访问计划,那么可以按如下步骤操作:
- 创建 Explain 表(只需创建一次):
从下面的目录中可以找到用于生成 Explain 表的脚本:
- UNIX/Linux:<DB2 instance owner>/sqllib/misc/EXPLAIN.DDL
- Windows:<DB2 install dir>\SQLLIB\MISC\EXPLAIN.DDL
- 用 DB2 CLP 执行该脚本:
db2 CONNECT TO <dbname> USER <userid>
db2 -tf EXPLAIN.DDL
db2 CONNECT RESET
- 执行以下 SQL 语句,填充 Explain 表:
db2 CONNECT TO <dbname> USER <userid>
db2 SET CURRENT EXPLAIN MODE EXPLAIN
db2 <sqlstmt>
db2 SET CURRENT EXPLAIN MODE NO
db2 CONNECT RESET
- 读取 Explain 表,并将访问计划写入一个文件:
db2exfmt -d <dbname> -u <userid> <pwd> -g -o <outfile> -w -1 -n % -s % -# 0
我们来看一个完整的示例,在此例中,DB2 优化器可以使用分布统计信息来更合理地估计过滤因子,以便生成更好的访问计划。
这个示例查询从已经定义好的 CARS 表中读取数据。对于表 CARS 中的汽车数据,有以下假设:
- 该表的基数为 1,000,000,也就是说该表包含 1,000,000 行。
- 表中 99.9% 的汽车是已经完工(“STATE” 列 = 100)的,这些汽车的信息相关必须保留,以用于后续处理(投述管理、质量保证等)。剩下的 1,000 辆汽车目前还处在制造流程中。
- 在该表中,制造商提供的从 A 到 J 的 10 种不同汽车型号(“TYPE” 列)几乎以相同的频率出现。
注意: 脚本 create_table_cars.sql 用于创建前述 CARS 表,包括 “STATE” 列和 “TYPE” 列上的索引,该脚本可以通过本文 下载。这个示例表是在
DB2 SAMPLE 数据库中(命令 db2sampl
),使用 DBM CFG 和 DB CFG 的默认设置创建的。
示例查询选择型号为 A 且正处在制造流程中、尚未完工的所有汽车:
SELECT * FROM CARS WHERE STATE < 100 AND TYPE = 'A'
首先来分析一下,在没有分布统计信息,而只有 CARS 表的基本统计信息及其索引的情况下,优化器选择的访问计划是怎样的。
1. 没有分布统计信息时示例查询的访问计划
由于优化器不知道 STATE 值不是均匀分布的,因此它决定使用索引 I_TYPE。使用这个索引会带来较高的成本,因为在访问 CARS 表之前,需要从索引中读取大约 100,000 个 RID(记录 ID)。此外,对于查询返回行数的假设也是错误的。因为优化器认为所有制造步骤(从 1 到 100)都有相同的频率,所以它无法预知谓词 STATE
< 100
在没有分布统计信息的情况下,执行该查询时,动态 SQL 的一个快照返回以下监视器值(假定所需的监视器开关已激活):
|
在此,我们不会进一步分析这些值,但是请记住它们,以便与有分布统计信息时相同查询得到的监视器值相比较。
接下来,为 CARS 表生成分布统计信息,并再次执行查询。此时,优化器选择了以下访问计划:
如何阅读访问计划
访问计划显示在执行某条 SQL 语句时,数据库引擎必须执行的所有操作。访问计划呈倒置树型,需要从下往上读。其中为各操作显示以下信息:
- 估计该操作将返回的行数
- 操作的类型。例如:
TBSCAN
- = 表扫描
IXSCAN
- = 索引扫描
FETCH
- = 从表中获取数据
NLJOIN
- = 嵌套的循环连接
SORT
- 用于惟一标识操作的一个数字
- 该操作的预计总成本
- 该操作的预计 I/O 成本
成本用 timerons 来表达,这是一个人造的单位,不能解释为实际时间单位,但有助于判断一个操作和整个访问计划的开销。成本是累积的,因为每个操作的成本都要加到之前所有操作的成本上。
这个访问计划的成本明显低于没有分布统计信息时的成本:前者为 203.809,而后者为 3242.63。这是因为优化器现在知道,谓词STATE
< 100
有一个较高的过滤因子,因而只会返回大约 1,000 辆正处在生产流程中尚未完工的汽车。因此,在这种情况下,CARS 表不是使用索引 I_TYPE
来访问的,而是使用索引 I_STATE
有分布统计信息时的访问计划要优于没有分布统计信息时的访问计划。但是,这是否会影响查询的执行时间?清单 2 包含相应的监视器数据快照:
|
下表 比较了有分布统计信息和没有分布统计信息这两种不同情况下的快照监视器值:.较快照监视器值
快照值 | 无分布统计信息 | 有分布统计信息 |
Rows read | 99,336 | 1,000 |
Buffer pool data logical reads | 8,701 | 11 |
Buffer pool index logical reads | 165 | 12 |
Total execution time (sec.ms) | 0.530903 | 0.014597 |
您可以看到,有分布统计信息的情况下,DB2 执行查询时需要计算的行数更少。这对于 CPU 成本和 I/O 成本都有积极的影响。最重要的是总执行成本,因为总执行成本关系到应用程序的响应时间。在具有分布统计信息的情况下,这个时间是 0.014597 秒,而在没有分布统计信息的情况下,这个时间是 0.530903,相差 36 倍之多。
在我们的示例中,两种情况下的执行时间分别为 0.014597 秒和 0.530903 秒,这个差距还不够明显,因为这两个值只是次秒级的。然而,这样的差距不应被忽略。如果要执行更复杂的查询,或者要连续执行多个查询,那么执行时间的差距就不是次秒级的,而是以秒甚至分钟来计算的。
分布统计信息的生成
如前所述,在使用 RUNSTATS
命令生成统计信息时,并不是 总会收集分布统计信息。这是有意义的,因为仅在存在很多重复值或者数据分布不均匀的情况下,分布统计信息才重要。而在其他情况下,分布统计信息并不能带来多大的好处。下面的 RUNSTATS
RUNSTATS ON TABLE SAMPLE.CARS AND INDEXES ALL
此外,如果需要收集 CARS 表中所有列的分布统计信息(频率统计信息和分位数统计信息),那么可以执行以下命令:
RUNSTATS ON TABLE SAMPLE.CARS WITH DISTRIBUTION AND
INDEXES ALL
生成分布统计信息意味着给 DB2 带来额外的、可观的开销,从而影响 RUNSTATS
RUNSTATS ON TABLE SAMPLE.CARS WITH DISTRIBUTION ON
COLUMNS (TYPE, STATE) AND INDEXES ALL
应为满足以下条件的列收集分布统计信息:
- 该列有很多重复的值(频率统计信息),或者该列的值分布不均匀,即它们在某些局部是聚合的(分位数统计信息)。
- 检查等于或不等于的谓词中使用到该列(频率统计信息),或者检查小于(小于等于)、大于(大于等于)或介于两个值之间的谓词中使用到该列(分位数统计信息)。
对于频率统计信息,重要的是定义好收集多少个值的重复数。如果为一个列中的所有值收集频率统计信息,那么成本就太高了。如果在执行 RUNSTATS
时没有显式定义数量,那么
DB2 将使用由数据库参数 NUM_FREQVALUES
提供的默认数量。由于NUM_FREQVALUES
的默认值为
10,DB2 将为列中出现最频率的 10 个值收集重复次数,这里假定 RUNSTATS
是在没有显式定义数量,且数据库参数 NUM_FREQVALUES
与频率统计信息类似,也必须为分位数统计信息定义一个数量,以保证精确性。分位数统计信息定义应该使用多少 “度量值(measurement)“。列中的值被认为是按升序排列的,并且有一个正则的行间隔,相应的值是确定的。使用的度量值越多,优化器对于检查范围(<
、>
、<=
、>=
、BETWEEN
)的谓词的过滤因子的估计就越准确。如果在执行 RUNSTATS
时没有明确指定一个值,那么
DB2 将使用由数据库参数 NUM_QUANTILES
提供的默认数量。NUM_QUANTILES
如果数据库配置(DB CFG
)不能提供 NUM_FREQVALUES
和 NUM_QUANTILES
的值,那么可以在执行 RUNSTATS
RUNSTATS ON TABLE SAMPLE.CARS WITH DISTRIBUTION ON
COLUMNS (TYPE NUM_FREQVALUES 10 NUM_QUANTILES 20, STATE NUM_FREQVALUES 15 NUM_QUANTILES 30) AND INDEXES ALL
如何检查是否存在分布统计信息
为检查某个表的分布统计信息是否已收集,可以查看分类视图 SYSCAT.COLDIST
的内容:SELECT * FROM SYSCAT.COLDIST WHERE TABSCHEMA = 'SAMPLE'
AND TABNAME = 'CARS'
视图 SYSCAT.COLDIST
列名 | 数据类型 | 是否可以为空 | 描述 |
TABSCHEMA | VARCHAR(128) | 不可以 | 本条目对应的表的限定符 |
TABNAME | VARCHAR(128) | 不可以 | 本条目对应的表的名称 |
COLNAME | VARCHAR(128) | 不可以 | 本条目对应的列的名称 |
TYPE | CHAR(1) | 不可以 | F = Frequency(最大频率) Q = 分位数值 |
SEQNO | SMALLINT | 不可以 | 如果 TYPE = F,则该列中的 N 表示第 N 频繁的值 如果 TYPE = Q,那么该列中的 N 表示第 N 个分位数值 |
COLVALUE | VARCHAR(254) | 可以 | 数据值,其形式为字符字面值,或者一个 NULL 值 |
VALCOUNT | BIGINT | 不可以 | 如果 TYPE = F,那么 VALCOUNT 是 COLVALUE 出现在该列中的次数 如果 TYPE = Q,那么 VALCOUNT 是其值小于或等于 COLVALUE 的行的数量 |
DISTCOUNT | BIGINT | 可以 | 如果 TYPE = Q,那么该列记录小于或等于 COLVALUE 的不同值的数量(如果没有,则为 NULL) |
仅在收集了一个表中至少一个列的分布统计信息时,SYSCAT.COLDIST
才会包含关于该表的条目。如果在没有 WITH
DISTRIBUTION
的情况下再次执行 RUNSTATS
,那么 SYSCAT.COLDIST
分布统计信息的参数标记/主变量
JDBC 提供了两种途径来执行动态 SQL,因而也提供了两种不同的接口:
java.sql.Statement
java.sql.PreparedStatement
PreparedStatement
是 Statement
的子接口,它允许使用参数标记(=
占位符;在其他编程语言中,此类占位符也被称为主变量) —— 而不是 Statement
。在使用 PreparedStatement
下面的代码片段显示使用 Statement
与使用 PreparedStatement
|
|
如果一条简单 SQL 语句执行多次(例如示例中的 INSERT
语句),那么使用 PreparedStatement
有优势,因为数据库只需编译该语句一次,即可多次执行,而不需要重复编译。假设在示例中需要插入数千行记录,那么使用 PreparedStatement
然而,Java 开发人员使用 PreparedStatement
|
|
使用 PreparedStatement
的那种代码片段在编码方面更为优雅,因为在将 STATE
和 TYPE
的值置入
SQL 语句时,不需要进行字符串运算。但这种方法存在一个缺点,在绑定 WHERE
子句中谓词的值之前,需要编译 SELECT
(创建访问计划)。为使优化器能够使用可用的分布统计信息,带有具体值的谓词极为重要。
让我们再次看看 CARS 表的查询示例,但这次使用参数标记,而不是具体值:
SELECT * FROM CARS WHERE STATE < ? AND TYPE = ?
CARS 表的分布统计信息仍然可用,因为在此期间这些统计信息也已经被收集。然而,使用参数标记时,将生成另一个访问计划。
虽然这个访问计划比使用具体值但没有分布统计信息情况下的访问计划好,但不如既使用具体值又有分布统计信息时的访问计划。在下表 中,动态 SQL 快照中的监视器输出也显示了这一点:
|
将这些值与之前收集到的值相比较,可以得到下表:
快照值 | 没有分布统计信息 | 有分布统计信息 | 既有分布统计信息又有参数标记 |
Rows read | 99,336 | 1,000 | 99 |
Buffer pool data logical reads | 8,701 | 11 | 11 |
Buffer pool index logical reads | 165 | 12 | 268 |
Total execution time (sec.ms) | 0.530903 | 0.014597 | 0.081908 |
在有参数标记时的 “Rows read” 值最小,但实际上这并不重要,因为此时 “Buffer pool index logical reads” 值较高。重要的是,与既有分布统计信息又有具体值的情况相比,“Total execution time” 更差一些,在本例中,其差距达到 8 倍。如前所述,在本例中,我们观察的是对少量数据执行的一个非常简单的查询。因此,执行时间上的差距并不是很明显。但是,如果要执行更复杂的查询,其执行时间以秒/分钟来计算,那么这个差距就会非常明显。
这个示例表明,如果将分布统计信息与参数标记一起使用,分布统计信息的作用就会受到限制。如果存在很多重复/不均匀的数据分布和复杂的查询,这可能会导致执行时间高于未使用参数标记情况下的执行时间。这并不意味着使用 PreparedStatement
总是很糟糕的。相反,如果要反复执行多次一条简单的语句,并且每次使用不同的值执行,建议使用 PreparedStatement
,因为语句只需编译一次。在处理复杂的查询时,由于这些查询常常要向系统和
DSS(决策支持系统)报告,因此使用参数标记很可能得不偿失。这是因为分布统计信息得不到充分的利用,编译时间只应用了一次,因为这些查询最多也只是执行一次。
结束语
在本文中,您看到了 DB2 优化器对分布统计信息的使用。如果一个数据库包含很多重复的值,或者数据分布不均匀,那么,如果除了基本统计信息外,另外还生成了分布统计信息,DB2
优化器就可为 SQL 语句产生更好的访问计划。本文解释了不同类型的分布统计信息,展示如何使用 RUNSTATS
命令生成这些统计信息,并提供了一个示例场景,说明分布统计信息在某些情况下的重要性。您还研究了参数标记/主变量的使用会为
DB2 优化器对分布统计信息的考虑带来怎样的限制。
下载
描述 | 名字 | 大小 | 下载方法 |
DDL for sample table CARS | create_table_cars.zip | 1KB | HTTP |