简介 MQT(Materialized Query Table,物化查询表) 物化了涉及一个或多个表或昵称的查询的预先计算结果。而后续的查询可以通过全部或部分匹配 MQT,并由 DB2 来补偿剩余的查询功能,从而达到提高查询性能的目的。本文将会介绍 DB2 中 MQT 匹配的基本原理,并基于此指出如何设计 MQT 从而能使得查询获得更高的匹配率从而提高查询效率。 MQT 匹配原理 MQT 在 OLAP 场景下能够有效提高复杂查询响应时间,尤其是有下面几类数据操作需求的查询: 在一个或多个维度上聚合数据。 在多个表之间连接数据。 数据来自于一个常见的数据访问子集—也就是该子集会被频繁访问,MQT 能够避免重复计算。 MQT 对应用程序是完全透明的。MQT 的相关信息已经被整合进 DB2 SQL 编译器中,它们会判断是否 MQT 应该被用来响应一个完整查询或者查询的一部分。因此,用户可以在不改变应用程序代码的情况下,创建和删除 MQTs,就和创建和删除索引而不需要更改应用程序一样。 而如何做到上面的透明性,这是由 DB2 SQL 编译器的 MQT 匹配算法来完成。如果我们把自己作为 MQT 匹配算法的作者,最容易想到的就是 MQT 需要满足以下条件才能够被匹配:MQT 中包含查询需要的所有行 (Record);MQT 中包含查询需要的所有列 (Column);MQT 中行的冗余度与查询结果一致。或者通过某种程度的补偿能够达到上述 3 个条件,那么 MQT 才有可能匹配对应查询。在 DB2 中也是遵循上述基本原理来进行匹配。其大致步骤如下:1) 在查询重写 (Rewrite) 阶段,DB2 编译器会针对目前所有可能被匹配的 MQT 进行分析,并选择一个最优的 MQT 匹配执行方案和不用 MQT 的执行方案。2) 在查询优化 (Optimizer) 阶段,会计算上述两种方案的成本,并选择成本最优的方案作为最终执行方案。需要注意的一点是在第一步中选择最优 MQT 匹配方案是一种启发式的选择 (rule/heuristic based),并没有真正计算成本。而且在这个过程中,可能匹配的 MQT 数目越多,需要的匹配过程越复杂,对应的编译时间越长。所以说并不是 MQT 越多越好,一方面 MQT 会占用存储空间,同时会增加编译时间。用户需要针对性地创建 MQT,保证其能够真正带来性能上的提升。而匹配的具体算法就不在这里详细阐述。如果读者有兴趣,可以在参考资源 2 中找到具体细节。
根据上面介绍的原理,以下 6 种查询可以利用 MQT 来提高性能。本文将针对每种查询举例加以介绍: MQT 能够精确匹配查询; 查询结果集是 MQT 的子集; 查询中连接的表数目多于 MQT; 查询中连接的表是 MQT 中表的子集,需满足引用完整性 (Reference Integrity, RI); 查询中包含 MQT 中不存在的列,需满足功能依赖; 查询对应的聚集级别 (aggregation level) 高于 MQT。 为了对上面的 6 种情况进行详细介绍,先创建一些示例表以方便通过实例来阐述这些原理。如清单 1 所示,表 Product 和 Customer 是维表 (dimension table),且分别定义了唯一键;表 Sales 是事实表 (fact table),它通过 PROD_ID 和 CUST_ID 的外键约束来保证引用完整性。至于表 Product 上定义的函数依赖 (Functional Dependency),我们将在后面详细讨论。另外,在实验的过程中人为的设置了基本表和 MQT 表的统计信息,使得编译器在选择查询计划总认为使用 MQT 的代价低。这并不影响 MQT 匹配的过程,而且简化了讨论。 清单 1. 创建示例中的表 CREATE TABLE MQTSCH.PRODUCT(PROD_ID INT NOT NULL UNIQUE, PROD_DESC VARCHAR(64), CAT_ID INT NOT NULL, CAT_DESC VARCHAR(64), GROUP_ID INT NOT NULL, GROUP_DESC VARCHAR(64), CONSTRAINT FD1 CHECK (CAT_DESC DETERMINED BY CAT_ID) NOT ENFORCED ENABLE QUERY OPTIMIZATION, CONSTRAINT FD2 CHECK (GROUP_ID DETERMINED BY CAT_ID) NOT ENFORCED ENABLE QUERY OPTIMIZATION ); CREATE TABLE MQTSCH.CUSTOMER(CUST_ID INT NOT NULL UNIQUE, CUST_NAME VARCHAR(50), CUST_ADDRESS VARCHAR(100)); CREATE TABLE MQTSCH.SALES(PROD_ID INT NOT NULL REFERENCES MQTSCH.PRODUCT(PROD_ID), CUST_ID INT NOT NULL REFERENCES MQTSCH.CUSTOMER(CUST_ID), SALE_DATE DATE NOT NULL, AMOUNT DECIMAL(9,2) NOT NULL); runstats on table MQTSCH.CUSTOMER; runstats on table MQTSCH.PRODUCT; runstats on table MQTSCH.SALES; update syscat.tables set card=20 where tabname='CUSTOMER'; update syscat.tables set card=200 where tabname='PRODUCT'; update syscat.tables set card=2000 where tabname='SALES'; set current refresh age=any;
MQT 能够精确匹配查询 这种情况是最容易理解的。当 MQT 能够精确匹配查询时,通常情况下,从 MQT 中获取数据的性能会优于执行相应查询,故选择 MQT 的执行方案通常会胜出。清单 2 中给出一个示例。其中 MQT SALES_PROD 基于外键连接事实表 Sales 与维表 Product。而查询则是同样的 Join 操作。这时,MQT 能匹配这个查询。在清单 2 中可以看到 MQT 和查询的详细内容,并且打印出的执行计划和拓展诊断信息明确的显示了 SALES_PROD 在查询匹配时被利用了。 清单 2. 精确的 MQT 匹配 --MQT definition: join for SALES and PRODUCT CREATE TABLE MQTSCH.SALES_PROD AS (SELECT P.PROD_ID, PROD_DESC, AMOUNT FROM MQTSCH.PRODUCT P, MQTSCH.SALES S WHERE P.PROD_ID = S.PROD_ID) DATA INITIALLY DEFERRED REFRESH DEFERRED; refresh table MQTSCH.SALES_PROD; --artifical statistics runstats on table MQTSCH.SALES_PROD; update syscat.tables set card=10 where tabname='SALES_PROD'; --collect the explain information DELETE FROM EXPLAIN_INSTANCE; explain plan for SELECT P.PROD_ID, PROD_DESC, AMOUNT FROM MQTSCH.PRODUCT P, MQTSCH.SALES S WHERE P.PROD_ID = S.PROD_ID; !db2exfmt -1 -d mqtdb -o join.plan; 下面是 join.plan 打印出的执行计划和诊断信息: Access Plan: ----------- Total Cost: 10.3414 Query Degree: 1 Rows RETURN ( 1) Cost I/O | 10 TBSCAN ( 2) 10.3414 1 | 10 TABLE: MQTSCH SALES_PROD Q1 Extended Diagnostic Information: -------------------------------- Diagnostic Identifier: 1 Diagnostic Details: EXP0148W The following MQT or statistical view was considered in query matching: "MQTSCH ". "SALES_PROD". Diagnostic Identifier: 2 Diagnostic Details: EXP0149W The following MQT was used (from those considered) in query matching: "MQTSCH ". "SALES_PROD".
查询结果集是 MQT 的子集 这种情况也很容易理解。当查询结果集是 MQT 的子集时,这意味着查询需要的行与列在 MQT 中都能找到,而 DB2 只需要在对应 MQT 上执行剩余的谓词 (predicate) 及计算 (head expression) 即可。如图 2 所示,这种 MQT 只需要被计算一次就可以被多次重用。清单 3 则给出了该场景的一个具体例子。 图 1. 查询结果集是 MQT 子集示意图
查看原图(大图)
清单 3. 查询结果集是 MQT 子集匹配 --MQT definition: join for SALES and PRODUCT CREATE TABLE MQTSCH.SALES_PROD AS (SELECT P.PROD_ID, PROD_DESC, AMOUNT FROM MQTSCH.PRODUCT P, MQTSCH.SALES S WHERE P.PROD_ID = S.PROD_ID) DATA INITIALLY DEFERRED REFRESH DEFERRED; refresh table MQTSCH.SALES_PROD; --artifical statistics runstats on table MQTSCH.SALES_PROD; update syscat.tables set card=10 where tabname='SALES_PROD'; --collect the explain information DELETE FROM EXPLAIN_INSTANCE; explain plan for SELECT P.PROD_ID, PROD_DESC, AMOUNT FROM MQTSCH.PRODUCT P, MQTSCH.SALES S WHERE P.PROD_ID = S.PROD_ID and AMOUNT > 10000; !db2exfmt -1 -d mqtdb -o joinsub.plan;
下面是 join_sub.plan 打印出的执行计划和诊断信息。有一点需要注意的是清单 2 与清单 3 中的 TBSCAN 并不完全相同。清单 3 中的 TBSCAN 包含谓词 (10000 < Q1.AMOUNT)。 Access Plan: ----------- Total Cost: 10.5194 Query Degree: 1 Rows RETURN ( 1) Cost I/O | 3.33333 TBSCAN ( 2) 10.5194 1 | 10 TABLE: MQTSCH SALES_PROD Q1 Extended Diagnostic Information: -------------------------------- Diagnostic Identifier: 1 Diagnostic Details: EXP0148W The following MQT or statistical view was considered in query matching: "MQTSCH ". "SALES_PROD". Diagnostic Identifier: 2 Diagnostic Details: EXP0149W The following MQT was used (from those considered) in query matching: "MQTSCH ". "SALES_PROD". 查询中连接的表数目多于 MQT 根据前面介绍的 MQT 匹配原理,这种情况成立的前提是 MQT 完成所有连接后得到的结果集需要是查询中对应表完成连接后结果集的超集。如果 MQT 包含查询中没有的谓词并且过滤掉一部分结果集,则该 MQT 无法进行匹配。 图 2. 查询中连接的表数目多于 MQT 匹配示意图
查看原图(大图)
清单 4. 查询中连接的表数目多于 MQT 匹配 --MQT definition: join for SALES and PRODUCT CREATE TABLE MQTSCH.SALES_PROD AS (SELECT P.PROD_ID, PROD_DESC, AMOUNT, CUST_ID FROM MQTSCH.PRODUCT P, MQTSCH.SALES S WHERE P.PROD_ID = S.PROD_ID) DATA INITIALLY DEFERRED REFRESH DEFERRED; refresh table MQTSCH.SALES_PROD; --artifical statistics runstats on table MQTSCH.SALES_PROD; update syscat.tables set card=10 where tabname='SALES_PROD'; --collect the explain information DELETE FROM EXPLAIN_INSTANCE; explain plan for SELECT P.PROD_ID, PROD_DESC, AMOUNT, C.CUST_ID, CUST_NAME FROM MQTSCH.PRODUCT P, MQTSCH.SALES S, MQTSCH.CUSTOMER C WHERE P.PROD_ID = S.PROD_ID AND S.CUST_ID = C.CUST_ID; !db2exfmt -1 -d mqtdb -o join_rejoin.plan; 下面是 join_rejoin.plan 打印出的执行计划和诊断信息: Access Plan: ----------- Total Cost: 18.8629 Query Degree: 1 Rows RETURN ( 1) Cost I/O | 20 ^NLJOIN ( 2) 18.8629 2 /------+-------\ 1 20 TBSCAN FETCH ( 3) ( 4) 9.72148 36.0967 1 4 | /---+----\ 1 20 20 TABLE: MQTSCH IXSCAN TABLE: MQTSCH SALES_PROD ( 5) CUSTOMER Q2 35.3323 Q1 4 | 20 INDEX: SYSIBM SQL100124231518010 Q1 Extended Diagnostic Information: -------------------------------- Diagnostic Identifier: 1 Diagnostic Details: EXP0022W Index has no statistics. The index "SYSIBM "."SQL100124231518010" has not had runstats run on it. This can lead to poor cardinality and predicate filtering estimates. Diagnostic Identifier: 2 Diagnostic Details: EXP0148W The following MQT or statistical view was considered in query matching: "MQTSCH ". "SALES_PROD". Diagnostic Identifier: 3 Diagnostic Details: EXP0149W The following MQT was used (from those considered) in query matching: "MQTSCH ". "SALES_PROD".
查询中连接的表是 MQT 中的子集,需满足 RI 前面提到,MQT 匹配的前提是 MQT 仅包含且正好包含查询所需要的数据行。因此,如果一个 MQT 中连接 (Join) 的表多于查询中的表,一般不能用这个 MQT 来匹配,因为额外的 Join 操作会影响 MQT 所包含的行及对应的冗余度。然而,如果额外的 Join 操作是基于 RI(引用完整性)的,那么它不会增加或删除任何行,编译器能够利用这个事实在上述情况中匹配 MQT。这种基于 RI 的 Join 操作在事实表和维表之间是很常见的。 通过例子来说明。如图 4 所示,SALES_PROD_CUST 这个 MQT 基于外键连接了事实表 Sales 与两个维表 Product 和 Customer。而查询则是在 Sales 和 Product 的 Join 操作。这时,MQT 能匹配这个查询。 在清单 5 中可以看到 MQT 和查询的详细内容,并且打印出的执行计划和拓展诊断信息明确的显示了 SALES_PROD_CUST 在查询匹配时被利用了。 图 3. 含有额外 RI-Join 时的 MQT 匹配示意图
查看原图(大图)
清单 5. 含有额外 RI-Join 时的 MQT 匹配 --MQT definition: with extra RI-Joins CREATE TABLE MQTSCH.SALES_PROD_CUST AS (SELECT P.PROD_ID, CAT_DESC, AMOUNT, C.CUST_ID, CUST_NAME FROM MQTSCH.PRODUCT P, MQTSCH.SALES S, MQTSCH.CUSTOMER C WHERE P.PROD_ID = S.PROD_ID AND S.CUST_ID = C.CUST_ID ) DATA INITIALLY DEFERRED REFRESH DEFERRED; refresh table MQTSCH.SALES_PROD_CUST; --artifical statistics runstats on table MQTSCH.SALES_PROD_CUST; update syscat.tables set card=10 where tabname='SALES_PROD_CUST'; --collect the explain information DELETE FROM EXPLAIN_INSTANCE; explain plan for SELECT P.PROD_ID, CAT_DESC, AMOUNT FROM MQTSCH.PRODUCT P, MQTSCH.SALES S WHERE P.PROD_ID = S.PROD_ID; !db2exfmt -1 -d mqtdb -o join_redud.plan;
下面是 join_redun.plan 打印出的执行计划和诊断信息: Access Plan: ----------- Total Cost: 10.3414 Query Degree: 1 Rows RETURN ( 1) Cost I/O | 10 TBSCAN ( 2) 10.3414 1 | 10 TABLE: MQTSCH SALES_PROD_CUST Q1 Extended Diagnostic Information: -------------------------------- Diagnostic Identifier: 1 Diagnostic Details: EXP0148W The following MQT or statistical view was considered in query matching: "MQTSCH ". "SALES_PROD_CUST". Diagnostic Identifier: 2 Diagnostic Details: EXP0149W The following MQT was used (from those considered) in query matching: "MQTSCH ". "SALES_PROD_CUST". 查询中包含 MQT 中不存在的列,需满足功能依赖 在开始介绍接下来的 MQT 匹配原理之前,有必要先简单介绍一下功能依赖 (Functional Dependency) 这个概念。Y 功能依赖于 X,是指 Y 的值由 X 决定,即每个 X 的值精确的对应着一个 Y 的值,记作 X -> Y。 根据定义,关系表上的所有列都功能依赖于主键或唯一键。例如,清单 1 中 Product 表,PROD_ID ->CAT_ID。对于非键列之间的功能依赖,DB2 通过定义一个参考约束 (informational constraint) 来实现。如清单 1 中在 Product 表上 FD1 和 FD2,分别定义了 CAT_ID -> CAT_DESC 和 CAT_ID -> GROUP_ID 这两个函数依赖。在参考约束定义中:
关键字 DETERMETED BY 准确地表达了函数依赖的含义; NOT ENFORCED 表示在执行增删改时 DB2 并不验证数据来保证约束的完整性; ENABLE QUERY OPTIMIZATION 告诉编译器可以利用这个函数依赖来重写和优化查询。 由于 DB2 并不强制函数依赖的这种约束的完整性,根据这个函数依赖优化的查询结果可能是错误的,因此,需要注意函数依赖的定义和维护。 继续 MQT 匹配的讨论。我们知道,MQT 匹配时,要求查询中需要的列都能从 MQT 中找到。那么如果查询中包含 MQT 不存在的列呢?函数依赖让这种匹配也变成可能。DB2 根据这些函数依赖重写查询,通过 MQT 和基本表的 re-join 来获得 MQT 缺少的列。 在清单 6 的例子中,MQT SALES_BY_CAT 的定义包含列 CAT_ID,统计每类产品的销售总量,然而查询却希望获得 CAT_DESC 和销售总量,而 CAT_DESC 不在 MQT 中。如果没有函数依赖,这个 MQT 是不能匹配的。而正是 Product 上的函数依赖 CAT_ID -> CAT_DESC 让这个 MQT 的匹配变成可能。 拓展诊断信息 (Extended Diagnostic Information) 段揭示了编译器的整个处理过程:诊断信息 1 中 EXP0073W 说明由于查询中包含 MQT 没有的列,这个 MQT 不能匹配;诊断 3 中 EXP0149W 表示当编译器收集到函数依赖后,用 MQT 匹配了这个查询。优化后的语句和执行计划一致的显示 MQT 的匹配以及 re-join 操作的生成。 由于 CAT_ID 不是具有唯一性的键,优化后的语句中利用 DISTINCT 来去除重复,对应了查询计划的 SORT 操作。一般情况下,这个 DISTINCT 是作用在维表上,开销很小。 清单 6. 含有额外 RI-Join 时的 MQT 匹配 --MQT missing columns, with FD CREATE TABLE MQTSCH.SALES_BY_CAT AS (SELECT CAT_ID, SUM(AMOUNT) AS TOTAL, COUNT(*) AS CNT FROM MQTSCH.PRODUCT P, MQTSCH.SALES S WHERE P.PROD_ID = S.PROD_ID GROUP BY CAT_ID ) DATA INITIALLY DEFERRED REFRESH IMMEDIATE; refresh table MQTSCH.SALES_BY_CAT; runstats on table MQTSCH.SALES_BY_CAT; update syscat.tables set card=10 where tabname='SALES_BY_CAT'; --collect the explain information DELETE FROM EXPLAIN_INSTANCE; explain plan for SELECT CAT_DESC, SUM(AMOUNT) FROM MQTSCH.PRODUCT P, MQTSCH.SALES S WHERE P.PROD_ID = S.PROD_ID GROUP BY CAT_ID, CAT_DESC; !db2exfmt -1 -d mqtdb -o fd.plan;
下面是 fd.plan 打印出的执行计划和诊断信息: Optimized Statement: ------------------- SELECT Q3.CAT_DESC AS "CAT_DESC", Q1.TOTAL FROM MQTSCH.SALES_BY_CATAS Q1, (SELECT DISTINCTQ2.CAT_DESC, Q2.CAT_ID FROM MQTSCH.PRODUCT AS Q2) AS Q3 WHERE (Q1.CAT_ID = Q3.CAT_ID) Access Plan: ----------- Total Cost: 98.6553 Query Degree: 1 Rows RETURN ( 1) Cost I/O | 0.4 HSJOIN ( 2) 98.6553 10 /---+----\ 10 1 TBSCAN TBSCAN ( 3) ( 4) 74.9814 23.5881 9 1 | | 10 1 TABLE: MQTSCH SORT SALES_BY_CAT ( 5) Q1 23.5366 1 | 200 TBSCAN ( 6) 23.4286 1 | 200 TABLE: MQTSCH PRODUCT Q2 Extended Diagnostic Information: -------------------------------- Diagnostic Identifier: 1 Diagnostic Details: EXP0073W The following MQT or statistical view was not eligible because one or more data filtering predicates from the query could not be matched with the MQT: "MQTSCH "."SALES_BY_CAT". Diagnostic Identifier: 2 Diagnostic Details: EXP0148W The following MQT or statistical view was considered in query matching: "MQTSCH ". "SALES_BY_CAT". Diagnostic Identifier: 3 Diagnostic Details: EXP0149W The following MQT was used (from those considered) in query matching: "MQTSCH ". "SALES_BY_CAT". |