DB2 存储的微观管理
上面的 DB2 存储的宏观管理一节讨论了对表的放置位置的控制。现在,让我们研究如何对数据进行微观管理,以及决定将行和列安置在哪里。控制每个字节在磁盘上占用哪个扇区的危险在于您所作的选择可能会比 DB2 的差。如果您能知道 DB2 根本无法知道的事情,您或许能够优化性能或磁盘空间的使用情况。例如,DB2 没有范围分区。相反,在集群机器环境中(或在大型 SMP 上使用多个逻辑分区),将行散列到所有分区以确保数据的平均分布。对于即席查询(ad hoc query)而言,这很理想。对于滚入和滚出仓库的数据而言,范围分区是有优势的。尤其是,如果您只需要保持最近 n 个十年、年、月、星期、日、小时、分钟或秒的数据联机,那么能够在您删除了 2000 年 7 月份数据之后添加 2001 年 7 月份数据的功能就很强大。这是节省磁盘空间的好方法。然而,如果所有 7 月份数据是集群的,则控制它的硬件可能会超负荷(考虑一下当银行客户在 8 月的第一个星期中收到他们的 7 月份月结单信件时,将要请求 7 月份数据的查询数)。7 月数据节点的超负荷摧毁了购买多台机器以建立数据仓库的意图:如果四个处理器都因为处理对 7 月数据的请求而饱和了(而 44 个处理器对其它十一个月的数据只进行很少的处理),则您购买的硬件比您正在使用的要多。有多少用户会在 8 月打电话查询 1 月份月结单呢?
范围分区还可能成为 DBA 的额外工作。如果您比较 http://www.tpc.org/tpch/results/h-ttperf.idc 上 FDR(完全公开报告(Full Disclosure Reports))中的基准测试工具箱,会发现在数据库管理系统中创建带范围分区的模式,所用的 DDL 行数是用 DB2 散列分区创建模式所需行数的四倍。在 TPC-H 的前身 TPC-D 中,差异甚至更惊人:10:1。TPC-D 允许实现视图、汇总表和多列索引(以及其它许多功能),因此其它因素也成为造成这一差异的原因。
使用 DB2,如果您希望所有 7 月份数据都在一个分区上,那么需要为每个月创建一个表,并使用 UNION 将所有数据合并到一起。这在 EEE 上并不是最佳方案(EEE 不共享任何东西,当数据随机分布时它工作得更好),而 UNION 也不象其它合并答案集的方法那样高效。更好的选择是让 DB2 使用散列分区来将数据分散到所有节点,并利用数据的到达来在保留散列分区好处的同时获得范围分区的好处。毕竟,7 月份数据往往在 7 月到达。ALTER TABLE 语句将使 7 月份数据在每个节点上保持连续,什么时候发出这条语句更好呢?
在此范围内
在我们揭示这个魔法之前,让我们都来回忆一下,关系表中的行是没有内在顺序的。用户和应用程序程序员都必须理解:如果要按他们希望的顺序获得数据,就必需使用 ORDER BY。按月(或者您选择的其它任意范围)的集群数据是一种合理的情况(如果 2001 年 7 月份的行数超过了 2000 年 7 月份的行数,而您又没有预留足够的额外空间,它就不会被重复)。这还使我们回忆起关系表中列也没有内在顺序。我们将等到本文结束时再推翻这一法则。
我们通过选择一个范围开始。既然已经使用了月,就让我们从它开始。在我们的仓库中,保持了 13 个月的数据。当 2001 年 7 月份数据到来时,它将取代 2000 年 6 月份数据。2001 年 7 月份数据应该更大:除了 7 月比 6 月多一天以外,我们通常希望业务随经济一起增长(或更快,如果我们有股票期权的话)。假设增长率为 15%,而且在我们为 2002 年和以后几年的复合增长担心之前,可能会对表进行重新组织。我们创建表时在每页上预留 15% 的可用空间,以应付未来的增长。通常,这样做是为了给插入预留空间,但在本例中这是为了在我们将数据滚入和滚出时用 2001 年 7 月的行代替 2000 年 6 月的行。下面的示例将使用单节点表来演示。在创建表之后,在我们关心的范围内使用索引来预留空间。我们在 PCTFREE 中指定保存 15% 的空间:
CREATE TABLE RECEIPTS (RECEIPT_DATE DATE NOT NULL, CUST_NUM INT NOT NULL, RECEIPT_KEY TIMESTAMP NOT NULL, AMOUNT DEC(10,2), PRIMARY KEY(CUST_NUM, RECEIPT_KEY)) PARTITIONING KEY (CUST_NUM, RECEIPT_KEY) CREATE INDEX DATE_IND ON RECEIPTS (RECEIPT_DATE) PCTFREE 15
现在逐月插入数据:
INSERT INTO RECEIPTS VALUES ('2000-06-06',1,CURRENT TIMESTAMP,1) INSERT INTO RECEIPTS VALUES ('2001-06-16',40,CURRENT TIMESTAMP,33)
当 2001 年 7 月份数据到来时,删除 2000 年 6 月份数据:
DELETE FROM RECEIPTS WHERE MONTH(RECEIPT_DATE)=6 AND YEAR(RECEIPT_DATE)=2000
现在可以开始插入 2001 年 7 月份数据:
INSERT INTO RECEIPTS VALUES ('2001-07-01',1,CURRENT TIMESTAMP,1)
如果 2001 年 7 月所拥有的数据比它覆盖的月(2000 年 6 月)的数据更多,我们就必须更改 PCTFREE 值。注:虽然初始 PCTFREE 是在 CREATE INDEX 中指定的,但是 DB2 并没有 ALTER INDEX 语句。您在 CREATE INDEX 中创建了初始 PCTFREE,但此后要用 ALTER TABLE 语句来调整它:
ALTER TABLE RECEIPTS PCTFREE 10 或 ALTER TABLE RECEIPTS PCTFREE 0
上述示例都很简单。我们没有研究节点组(nodegroup)的定义(它将指定将 RECEIPTS 散布在哪些分区),也没有研究如果成批地装入每月的数据,将如何处理装入问题。我们还避免了集群索引,而存储了冗余数据(时间戳记是 RECEIPT_DATE 列的超集)。我们为什么要经历这些痛苦呢?因为,在任何一个月,特定月的所有数据在每个节点中的磁盘上都是连续的,但那个月的所有数据都使用分区键(CUST_NUM,RECEIPT_KEY)上的散列键平均地分散到所有节点。注:我们查询的那个月的列(RECEIPT_DATE)并不是分区键的一部分。当我们在查询中寻找 2001 年 7 月的行时,每个节点提供相等的行数(因此所有硬件都参与了工作,并且从数据的分布中得到了最大利益),但是在每个节点内部,2001 年 7 月的行在磁盘上是连续的:它们将一起呆在缓冲池中,也可能在整个 2001 年 8 月都呆在内存里,以便在客户电话询问他们的 7 月份月结单时查询得更快。对 7 月份月结单的打印和查询也会更快,因为 7 月的行在磁盘上是连续的,所以使 I/O 更快。最终,通过腾出 2000 年 6 月的空间(当它们过时的时候,就被删除了)来保存 2001 年 7 月的行,节省了磁盘空间。我要第三次说这句话:2001 年 7 月的数据可能比 2000 年 6 月数据多 15%,因为我们使用索引上的 PCTFREE 预留了可用空间。我们可以通过改变表来设低 PCTFREE 的值,从而将行放置到这个可用空间中。
欲速则不达
上述示例利用了 DB2 的这种自发性:DB2 通过用 INSERT 扫描整个表将新行置入空槽。在本例中,我们删除了 6 月的数据,而当插入 2001 年 7 月的数据时 DB2 查找空槽。这使得 INSERT 变慢,因此 SELECT 可以更快。相反的情况:当我们希望 INSERT 变快而又不打算对数据使用太多 SELECT 语句时(而且可能不关心磁盘空间),会怎么样呢?继续我们的银行示例:数据仓库保持 13 个月的数据联机,但银行并不真的删除 2000 年 6 月数据。审计师喜欢追溯七年或十年以前的数据,并问您为什么不上缴在大街上捡到的 100 元钱却存入了您的银行帐户。虽然 99% 的请求都是关于过去 13 个月的,但应用程序还是会在一些较旧的慢速磁盘的表空间中为旧数据创建一个表:
CREATE TABLE OLD_RECEIPTS LIKE RECEIPTS IN SLOW_DISK_TBSP
在删除 2000 年 6 月的行之前,我们使用一个带子查询的 INSERT 语句将它们移到 OLD_RECEIPTS 表:
INSERT INTO OLD_RECEIPTS SELECT * FROM RECEIPTS WHERE MONTH(RECEIPT_DATE)=6 AND YEAR(RECEIPT_DATE)=2000
现在可以删除 2000 年 6 月的行了:
DELETE FROM RECEIPTS WHERE MONTH(RECEIPT_DATE)=6 AND YEAR(RECEIPT_DATE)=2000
注:在本例中,我们可以不关心为 SLOW_DISK_TBSP 表空间使用的设备节省磁盘空间。考虑一下因为我们只是将数据移到这个表中,而且只有少于 1% 的查询是针对它的,所以下列事情很可能是真的:






