SQL Server 2005 中的 分区 表和 索引 为什么要进行 分区 ? 什么是 分区 ?为什么要使用 分区 ?简单的回答是:为了改善大型表以及具有各种访问模式的表的可伸缩性和可管理性。通常,创建表是为了存储某种实体(例如客户或销售)的信息,并且每个表只具有描
SQL Server 2005 中的 分区 表和 索引
为什么要进行 分区 ?
什么是 分区 ?为什么要使用 分区 ?简单的回答是:为了改善大型表以及具有各种访问模式的表的可伸缩性和可管理性。通常,创建表是为了存储某种实体(例如客户或销售)的信息,并且每个表只具有描述该实体的属性。一个表对应一个实体是最容易设计和理解的,因此不需要优化这种表的性能、可伸缩性和可管理性,尤其是在表变大的情况下。
大型表是由什么构成的呢?超大型数据库 (VLDB) 的大小以数百 GB 计算,甚至以 TB 计算,但这个术语不一定能够反映数据库中各个表的大小。大型数据库是指无法按照预期方式运行的数据库,或者运行成本或维护成本超出预定维护要求或预算要求的数据库。这些要求也适用于表;如果其他用户的活动或维护操作限制了数据的可用性,则可以认为表非常大。例如,如果性能严重下降,或者每天、每周甚至每个月的维护期间有两个小时无法访问数据,则可以认为销售表非常大。有些情况下,周期性的停机时间是可以接受的,但是通过更好的设计和 分区 实现,通常可以避免或最大程度地减少这种情况的发生。虽然术语 VLDB 仅适用于数据库,但对 分区 来说,了解表的大小更重要。
除了大小之外,当表中的不同行集拥有不同的使用模式时,具有不同访问模式的表也可能会影响性能和可用性。尽管使用模式并不总是在变化(这也不是进行 分区 的必要条件),但在使用模式发生变化时,通过 分区 可以进一步改善管理、性能和可用性。还以销售表为例,当前月份的数据可能是可读写的,但以往月份的数据(通常占表数据的大部分)是只读的。在数据使用发生变化的类似情况下,或在维护成本随着在表中读写数据的次数增加而变得异常庞大的情况下,表响应用户请求的能力可能会受到影响。相应地,这也限制了服务器的可用性和可伸缩性。
此外,如果以不同的方式使用大量数据集,则需要经常对静态数据执行维护操作。这可能会造成代价高昂的影响,例如性能问题、阻塞问题、备份(空间、时间和运营成本),还可能会对服务器的整体可伸缩性产生负面影响。
分区 可以带来什么帮助?当表和 索引 变得非常大时, 分区 可以将数据分为更小、更容易管理的部分,从而提供一定的帮助。本文重点介绍横向 分区 ,在横向 分区 中,大量的行组存储在多个相互独立的 分区 中。 分区 集的定义根据需要进行自定义、定义和管理。Microsoft SQL Server 2005 允许您根据特定的数据使用模式,使用定义的范围或列表对表进行 分区 。SQL Server 2005 还围绕新的表和 索引 结构设计了几种新功能,为 分区 表和 索引 的长期管理提供了大量的选项。
此外,如果具有多个 CPU 的系统中存在一个大型表,则对该表进行 分区 可以通过并行操作获得更好的性能。通过对各个并行子集执行多项操作,可以改善在极大型数据集(例如数百万行)中执行大规模操作的性能。通过 分区 改善性能的例子可以从以前版本中的聚集看出。例如,除了聚集成一个大型表外,SQL Server 还可以分别处理各个 分区 ,然后将各个 分区 的聚集结果再聚集起来。在 SQL Server 2005 中,连接大型数据集的查询可以通过 分区 直接受益;SQL Server 2000 支持对子集进行并行连接操作,但需要动态创建子集。在 SQL Server 2005 中,已 分区 为相同 分区 键和相同 分区 函数的相关表(如 Order 和 OrderDetails 表)被称为已对齐。当优化程序检测到两个已 分区 且已对齐的表连接在一起时,SQL Server 2005 可以先将同一 分区 中的数据连接起来,然后再将结果合并起来。这使 SQL Server 2005 可以更有效地使用具有多个 CPU 的计算机。
返回页首
分区 的概念对 SQL Server 来说并不陌生。实际上,此产品的每个版本中都可以实现不同形式的 分区 。但是,由于没有为了帮助用户创建和维护 分区 架构而专门设计一些功能,因此 分区 一直是一个很繁琐的过程,没有得到充分的利用。而且,用户和开发人员对此架构存在误解(由于其数据库设计比较复杂),低估了它的优点。但是,由于概念中固有的重要性能改善,SQL Server 7.0 开始通过 分区 视图实现各种 分区 方式,以此来改进这种功能。现在,SQL Server 2005 为通过 分区 表对大型数据集进行 分区 又迈出了最大的一步。
对 SQL Server 7.0 之前的版本中的对象进行 分区
在 SQL Server 6.5 及以前的版本中, 分区 只能通过设计来完成,还必须内置到所有数据访问编码和查询方法中。通过创建多个表,然后通过存储过程、视图或客户端应用程序管理对正确表的访问,通常可以改善某些操作的性能,但代价是增加了设计的复杂性。每个用户和开发人员都必须知道(并正确引用)正确的表。单独创建和管理每个 分区 ,而使用视图来简化访问;但是这种解决方案对性能并没有太大的改善。使用联合视图简化用户和应用程序访问时,查询处理器必须访问每个基础表才能确定结果集所需的数据。如果只需要基础表的有限子集,则每个用户和开发人员都必须了解此设计,以便只引用相应的表。
SQL Server 7.0 中的 分区 视图
在 SQL Server 7.0 之前的版本中,手动创建 分区 所面临的挑战主要与性能有关。尽管视图可以简化应用程序设计、用户访问和查询的编写,但却无法改善性能。而在 SQL Server 7.0 版本中,视图结合了约束,允许查询优化程序从查询计划中删除不相关的表(即 分区 消除),大大降低了联合视图访问多个表时的总计划成本。
请参见图 1 中的 YearlySales 视图。您可以定义十二个单独的表(如 SalesJanuary2003 、 SalesFebruary2003 等),然后定义每个季度的视图以及全年的视图 YearlySales,而不是将所有销售数据放到一个大型表中。
图 1 : SQL Server 7.0/2000 中的 分区 视图
使用以下查询访问 YearlySales 视图的用户只会被引导至 SalesJanuary2003 表。
SELECT ys.* FROM dbo.YearlySales AS ys WHERE ys.SalesDate = '20030113'只要约束可信并且访问视图的查询使用 WHERE 子句根据 分区 键(定义约束的列)限制查询结果,SQL Server 就会只访问必需的基础表。 受信任的约束 是指 SQL Server 能够确保所有数据符合该约束所定义的属性的约束。创建约束时,默认行为是创建约束 WITH CHECK。此设置将导致对表执行架构锁定,以便根据约束验证数据。如果验证结果表明现有数据有效,则添加约束;一旦解除架构锁定,后续的插入、更新和删除操作都必须符合正在应用的约束。通过使用此过程创建受信任的约束,开发人员无需直接访问(甚至不需要知道)他们感兴趣的表,从而大大降低了使用视图的设计的复杂性。通过受信任的约束,SQL Server 可以从执行计划中删除不需要的表,从而改善性能。
注意: 约束可以通过各种方式变得“不可信任”;例如,如果未指定 CHECK_CONSTRAINTS 参数即执行批量插入,或者使用 NOCHECK 创建约束。如果约束不可信任,查询处理器将转而扫描所有基础表,因为它无法确定所请求的数据是否真的位于正确的基础表中。
SQL Server 2000 中的 分区 视图
尽管 SQL Server 7.0 大大简化了设计并改善了 SELECT 语句的性能,但是并没有为数据修改语句带来任何好处。INSERT、UPDATE 和 DELETE 语句只能针对基础表,而不能直接针对用于联合表的视图。在 SQL Server 2000 中,数据修改语句还可以受益于 SQL Server 7.0 中引入的 分区 视图功能。由于数据修改语句可以使用相同的 分区 视图结构,因此,SQL Server 可以通过视图将修改定向至相应的基础表。为了正确配置此设置,需要对 分区 键及其创建设置额外的限制;但是,基本原理是相同的,因为 SELECT 查询与修改都会直接发送给相应的基础表。有关在 SQL Server 2000 中进行 分区 的限制、设置、配置和最佳方法的详细信息,请参见 Using Partitions in a Microsoft SQL Server 2000 Data Warehouse 。
SQL Server 2005 中的 分区 表
尽管 SQL Server 7.0 和 SQL Server 2000 中的改进大大改善了使用 分区 视图时的性能,但是并没有简化 分区 数据集的管理、设计或开发。使用 分区 视图时,必须单独创建和管理每个基础表(在其中定义视图的表)。尽管简化了应用程序设计并为用户带来了好处(用户不再需要知道直接访问哪个基础表),但是由于要管理的表太多,而且必须为每个表管理数据完整性约束,管理工作变得更复杂。因为管理方面的问题,通常只有在需要存档或加载数据时才使用 分区 视图来分离表。当数据被移动到只读表或从只读表中删除后,操作的代价变得十分高昂,不仅花费时间、占据日志空间,通常还会导致系统阻塞。
另外,由于以前版本中的 分区 策略需要开发人员创建各个表和 索引 ,然后通过视图将它们联合起来,因此优化程序需要验证并确定每个 分区 的计划(因为 索引 可能已发生变化)。这样一来,SQL Server 2000 中的查询优化时间通常会随着处理的 分区 数增加而直线上升。
在 SQL Server 2005 中,从定义上讲,每个 分区 都拥有相同的 索引 。例如,请考虑这样一种方案,即当前月份的联机事务处理 (OLTP) 数据需要移动到每个月末的分析表中。分析表(用于只读查询)是具有一个群集 索引 和两个非群集 索引 的表;批量加载 1 GB 数据(加载到已建立 索引 并激活的一个表中)将使当前用户遭受系统阻塞的情况,因为表和/或 索引 变得支离破碎和/或被锁定。另外,因为每传入一行都需要维护表和 索引 ,所以加载过程还将耗费大量的时间。虽然可以通过多种方法加快批量加载的速度,但这些方法可能会直接影响所有其他用户,因为追求速度而无法实现并发操作。
如果将这些数据单独放到一个新创建的(空)且未建立 索引 (堆)的表中,则可以先加载数据,而在加载数据之后建立 索引 。通常情况下,使用这种架构可以获得十倍或更好的性能。实际上,通过加载未建立 索引 的表可以利用多个 CPU,因为可以并行加载多个数据文件或从同一个文件中加载多个数据块(通过开始和结束行位置来定义)。由于两个操作都可以通过并行获益,因此可以更进一步改善性能。
在 SQL Server 的任何版本中, 分区 都使您可以获得更精确的控制,而且不需要将所有数据放到一个位置;但是,需要创建和管理许多对象。在以前的版本中,通过动态创建表、删除表以及修改联合视图,可以实现功能性 分区 策略。但是,SQL Server 2005 中的解决方案更加完善:您可以轻松地移入新填充的 分区 (作为现有 分区 架构的额外 分区 ),还可以移出任何旧 分区 。整个过程只需要很短的时间即可完成,通过使用并行批量加载和并行 索引 建立,还可以进一步提高效率。更重要的是,因为 分区 是在表范围之外进行管理的,所以添加 分区 之前不会对所查询的表造成任何影响。结果是,添加一个 分区 通常只需要几秒钟。
需要删除数据时的性能改善也很显著。如果一个数据库需要一个滑动窗口数据集,用于移植新数据(例如当前月份的数据)并删除最早的数据(可能是上一年同一月份的数据),那么使用 分区 可以将数据移植的性能提高几个数量级。虽然这看起来好像很大,但考虑了未 分区 的区别;当所有数据位于一个表中时,删除 1 GB 的旧数据需要对表及其相关 索引 进行逐行处理。删除数据的过程将创建大量的日志活动,不允许在删除的过程中出现日志截断问题(注意,删除是一个自动提交的事务;但是,可以通过尽可能地执行多个删除操作来控制事务的大小),因此,可能需要更大的日志。但是,如果使用 分区 ,删除相同数量的数据需要从 分区 表中删除特定的 分区 (一种元数据操作),然后删除或截断独立的表。
此外,如果不知道如何才能最好地设计 分区 ,则不可能认识到将文件组与 分区 结合使用是实现 分区 的理想选择。文件组允许您将各个表放置到不同的物理磁盘上。如果一个表包含多个文件(使用文件组),则无法预测数据的物理位置。对于不需要使用并行操作的系统来说,SQL Server 可以在文件组之间更平均地使用所有磁盘,使数据具体放在什么位置变得不是那么重要,从而提高系统的性能。
注意: 在图 2 中,一个文件组包含三个文件。此文件组中放置了两个表,即 Orders 和 OrderDetails 。将表放置到文件组中时,SQL Server 将根据文件组中的对象需要的空间,从每个文件中获得盘区分配(64-KB 块,相当于八个 8-KB 页面),按比例填充文件组中的文件。创建 Orders 和 OrderDetails 表时,文件组是空的。创建订单时,数据被输入到 Orders 表中(每个订单占据一行),并且按照每个明细项一行的方式输入到 OrderDetails 表中。SQL Server 将一个盘区分配给文件 1 中的 Orders 表,将另一个盘区分配给文件 2 中的 OrderDetails 表。 OrderDetails 表的增长速度可能比 Orders 表快,后续的分配将转到下一个需要空间的表中。随着 OrderDetails 表的增长,它将从文件 3 中获取下一个盘区,而 SQL Server 将继续在文件组的文件之间“循环”下去。在图 2 中,就是从每个表到盘区,再从每个盘区到相应的文件组。盘区是按照需要的空间进行分配的,而根据流程进行编号。
图 2 :使用文件组进行 分区 填充
SQL Server 继续在文件组中的所有对象之间平衡分配。如果增加给定操作使用的磁盘数,虽然 SQL Server 可以更有效地运行,但从管理或维护的角度来说,增加磁盘数并非最佳选择,尤其是在使用模式几乎可以预测(且已隔离)的情况下。因为数据在磁盘上的位置并不明确,所以您无法隔离数据以执行备份等维护操作。
通过 SQL Server 2005 中的 分区 表,可以对表进行设计(使用函数和架构),从而将具有相同 分区 键的所有行都直接放置到(且总是转到)特定的位置。函数用于定义 分区 边界以及放置第一个值的 分区 。在使用 LEFT 分区 函数时,第一个值将作为第一个 分区 中的上边界。在使用 RIGHT 分区 函数时,第一个值将作为第二个 分区 的下边界(本文后面将更详细地介绍 分区 函数)。定义函数后即可创建 分区 架构,以定义 分区 到其数据库位置的物理映射(根据 分区 函数)。当多个表使用同一个函数(但不一定使用同一个架构)时,将按类似的方式对具有相同 分区 键的行进行分组。此概念称为对齐。通过将来自多个表但具有相同 分区 键的行对齐到相同或不同的物理磁盘上,SQL Server 可以(如果优化程序做出此选择)只处理每个表中必要的数据组。要实现对齐,两个 分区 表或 索引 所在的相应 分区 之间必须具有某种对应性。它们必须为 分区 列使用等效的 分区 函数。如果满足以下条件,两个 分区 函数则可以用来对齐数据:
两个 分区 函数使用相同数量的参数和 分区 。
每个函数中使用的 分区 键具有相同的类型(包括长度和精度,如果适用,还包括缩放和排序)。
边界值相等(包括 LEFT/RIGHT 边界标准)。
注意: 即使两个 分区 函数都用于对齐数据,但如果没有在与 分区 表相同的列上 分区 ,最后的 索引 也可能无法对齐。
排序是一种更强大的对齐方式,通过排序,两个对齐的对象将用一个 equi-join 谓词连接起来(equi-join 位于 分区 列上)。在可能出现 equi-join 谓词的查询、子查询或其他类似结构的上下文中,这变得很重要。排序之所以重要,因为在 分区 列上连接表的查询一般都非常快。以图 2 中的 Orders 和 OrderDetails 表为例,除了按比例填充文件之外,还可以创建映射到三个文件组的 分区 架构。定义 Orders 和 OrderDetails 表时,将它们定义为使用相同的架构。具有相同 分区 键值的相关数据将被放置到同一个文件中,而将必要的数据隔离出来以便进行连接。如果来自多个表的相关行都按照相同的方式进行 分区 ,SQL Server 则可以连接 分区 ,而无需在整个表或多个 分区 中(如果表使用了不同的 分区 函数)搜索匹配的行。在这种情况下,不仅可以对齐对象(因为它们使用相同的键),还可以按存储位置对齐(因为相同的数据位于相同的文件中)。
图 3 显示两个对象可以使用相同的 分区 架构,而具有相同 分区 键的所有数据行最后将位于同一个文件组中。对齐相关数据后,SQL Server 2005 可以有效地并行处理大型数据集。例如,1 月份的所有销售数据(包括 Orders 和 OrderDetails 表中的数据)都位于第一个文件组中,2 月份的数据位于第二个文件组中,依此类推。
图 3 :按存储位置对齐的表
SQL Server 允许根据范围进行 分区 ,还允许将表和 索引 都设计为使用相同的架构,以便更好地对齐。好的设计可以大大提高整体性能,但是,如果数据的使用随着时间而发生变化,该怎么办?如果需要额外的 分区 ,又该怎么办?简化从 分区 表外部添加 分区 、删除 分区 和管理 分区 等方面的管理工作是 SQL Server 2005 的主要设计目标。
SQL Server 2005 已经考虑了如何简化 分区 的管理、开发和使用。它在性能和可管理性方面有以下优点:
简化了需要进行 分区 以改善性能或可管理性的大型表的设计和实现。
将数据加载到现有 分区 表的新 分区 中时,最大程度地减少了对其他 分区 中的数据访问的影响。
将数据加载到现有 分区 表的新 分区 中时,性能相当于将同样的数据加载到新的空表中。
在存档和/或删除 分区 表的一个 分区 时,最大程度地减少了对表中其他 分区 的访问的影响。
允许通过将 分区 移入和移出 分区 表来维护 分区 。
提供了更好的伸缩性和并行性,可以对多个相关表执行大量操作。
改善了所有 分区 的性能。
缩短了查询优化时间,因为不需要单独优化每个 分区 。
返回页首
要在 SQL Server 2005 中实现 分区 ,必须了解一些新的概念、术语和语法。要理解这些新概念,首先我们看一下与创建和放置操作有关的表结构。在以前的版本中,表通常是一个物理和逻辑概念,但使用 SQL Server 2005 分区 表和 索引 ,您在存储表的方式和位置方面就有了多种选择。在 SQL Server 2005 中,可以使用以前版本中的相同语法创建表和 索引 ,作为一个表结构放置到 DEFAULT 文件组或用户定义的文件组中。另外,在 SQL Server 2005 中,还可以根据 分区 架构创建表和 索引 。 分区 架构可以将对象映射到一个或多个文件组。为了确定数据的相应物理位置, 分区 架构将使用了 分区 函数。 分区 函数定义了用来定向行的算法,而架构则将 分区 与其相应的物理位置(即文件组)相关联。换句话说,表仍然是一个逻辑概念,但与以前的版本相比,表在磁盘上的物理位置有了很大的不同;表还可以拥有架构。
范围 分区
范围 分区 是按照特定和可定制的数据范围定义的表 分区 。范围 分区 的边界由开发人员选择,还可以随着数据使用模式的变化而变化。通常,这些范围是根据日期或排序后的数据组进行划分的。
范围 分区 主要用于数据存档、决策支持(当通常只需要特定范围内的数据时,例如给定的月份或季度)以及组合的 OLTP 和决策支持系统 (DSS)(数据使用在行的生命周期内会发生变化)。SQL Server 2005 分区 表和 索引 的最大优点,尤其是在存档和维护方面,就是可以管理特定范围内的数据。通过范围 分区 ,可以非常快速地存档和替换旧的数据。当数据访问通常用于对大范围数据的决策支持时,最适合使用范围 分区 。在这种情况下,数据所在的具体位置至关重要,这样才能在需要时只访问相应的 分区 。另外,由于事务数据已经可用,因此可以轻松快捷地添加数据。范围 分区 最初定义起来很复杂,因为需要为每个 分区 定义边界条件。此外,还需要创建一个架构,将每个 分区 映射到一个或多个文件组。但是,它们通常具有一致的模式,因此,定义后很容易通过编程方式进行维护(参见图 4)。
图 4 :具有 12 个 分区 的范围 分区 表
定义 分区 键
对表和 索引 进行 分区 的第一步就是定义 分区 的关键数据。 分区 键必须作为一个列存在于表中,还必须满足一定的条件。 分区 函数定义键(也称为数据的逻辑分离)所基于的数据类型。函数只定义键,而不定义数据在磁盘上的物理位置。数据的位置由 分区 架构决定。换句话说,架构将数据映射到一个或多个文件组,文件组将数据映射到特定的文件,文件又将数据映射到磁盘。 分区 架构通常使用函数来实现此目的:如果函数定义了五个 分区 ,则架构必须使用五个文件组。文件组不需要各不相同;但是,如果拥有多个磁盘(最好是多个 CPU),使用不同的文件组可以获得更好的性能。将架构与表一起使用时,您需要定义用作 分区 函数的参数的列。
对于范围 分区 ,数据集可以根据逻辑和数据驱动的边界进行划分。实际上,数据 分区 不可能实现真正的平衡。当以定义分析的特定边界(也称为范围)的方式使用表时,数据的使用即表明范围 分区 。范围函数的 分区 键可以只包含一个列,而 分区 函数可以包含整个域,即使表中可能不存在数据(由于数据完整性/约束)。换句话说,可以为每个 分区 定义边界,但第一个 分区 和最后一个 分区 可能包含最左侧的行(小于最低边界条件的值)和最右侧的行(大于最高边界条件的值)。因此,要将值域限制到特定的数据集,必须将 分区 与 CHECK 约束结合使用。使用 CHECK 约束强制应用业务规则和数据完整性约束,使您可以将数据集限制到特定的范围,而不是不确定的范围。当维护和管理过程中需要定期存档大量数据,当查询访问范围子集内的大量数据时,范围 分区 是理想的选择。
索引 分区
除了对表的数据集进行 分区 之外,还可以对 索引 进行 分区 。使用相同的函数对表及其 索引 进行 分区 通常可以优化性能。当 索引 和表按照相同的顺序使用相同的 分区 函数和列时,表和 索引 将对齐。如果在已经 分区 的表中建立 索引 ,SQL Server 会自动将新 索引 与该表的 分区 架构对齐,除非该 索引 的 分区 明显不同。当表及其 索引 对齐后,SQL Server 则可以更有效地将 分区 移入和移出 分区 表,因为所有相关的数据和 索引 都使用相同的算法进行划分。
如果定义表和 索引 时不仅使用了相同的 分区 函数,还使用了相同的 分区 架构,则这些表和 索引 将被认为是 按存储位置对齐 。按存储位置对齐的一个优点是,相同边界内的所有数据都位于相同的物理磁盘上。在这种情况下,可以单独在某个时间段内执行备份操作,还可以根据数据的变化在备份频率和备份类型方面改变您的策略。如果连接或收集了相同文件或文件组中的表和 索引 ,则可以发现更多的好处。SQL Server 可以通过在多个 分区 中并行操作来获益。在按存储位置对齐和多 CPU 的情况下,每个处理器都可以直接处理特定的文件或文件组,而不会与数据访问产生任何冲突,因为所有需要的数据都位于同一个磁盘上。这样,可以并行运行多个进程,而不会相互干扰。
有关详细信息,请参见 SQL Server Books Online 中的“Special Guidelines for Partitioned Indexes”。
分区 的特殊情况:拆分、合并和移动
为了更好地使用 分区 表,需要了解与 分区 管理有关的几个新功能和概念。因为 分区 适用于可以缩放的大型表,所以创建 分区 函数时选择的 分区 数随着时间而变化。可以将 ALTER TABLE 语句与新的拆分选项结合使用,在表中添加一个 分区 。拆分 分区 时,可以将数据移动到新的 分区 中;但是为了维护性能,不应移动行。本文后面的案例研究将介绍这种方案。
相反,要删除 分区 ,请先移出数据,然后合并边界点。如果使用范围 分区 ,则通过指明应删除的边界点来发出合并请求。在只需要特定时段的数据并且定期进行数据存档(例如,每月一次)的情况下,您可能希望在当前月份的数据可用时存档部分数据(最早月份的数据)。例如,您可以选择获取一年的数据,而在每个月末移入当前月份,然后移出最早的月份,从而区分当前月份的读/写 OLTP 与以前月份的只读数据。如以下方案所述,可以通过一个特殊的操作流使处理更有效。
您保留了一年的只读数据。目前,表中的数据是从 2003 年 9 月到 2004 年 8 月的数据。而当前月份 2004 年 9 月位于另一个数据库中,并为 OLTP 性能而进行了优化。在只读版本的表中,共有 13 个 分区 :十二个 分区 包含数据(从 2003 年 9 月到 2004 年 8 月),最后一个 分区 是空的。最后这个 分区 为空的原因在于,范围 分区 总是包括整个域,即最左侧和最右侧。如果您打算在滑动窗口方案中管理数据,通常需要有一个可以拆分的空 分区 ,以便放置新数据。在使用 LEFT 边界点定义的 分区 函数中,空 分区 逻辑上位于最右侧。将最后一个 分区 保留为空使您可以拆分空 分区 (用来存储即将产生的新数据),而且不需要将最后一个 分区 的行(因为不存在)移动到添加(拆分空 分区 以存储其他数据块时进行此操作)的新文件组中。这是一个相当复杂的概念,将在本文后面的案例研究中进行更详细的介绍,但其核心思想是,所有数据添加或删除操作都应该只是元数据操作。为了确保只进行元数据操作,需要从策略上管理表中不断变化的那个 分区 。为了确保此 分区 为空的,您需要使用 CHECK 约束将此数据限定在基础表中。在这种情况下, OrderDate 应该在 2003 年 9 月 1 日之后(包括此日),在 2004 年 9 月 1 日之前。如果最后定义的边界点是 8 月 31 日 11:59:59.997(为什么是 997,后文有详细的说明),则 分区 函数与此约束的组合将使最后一个 分区 为空。虽然这些只是概念,但重要的是要了解拆分和合并是通过 ALTER PARTITION FUNCTION 进行的,而移动是通过 ALTER TABLE 进行的。
图 5 :加载 / 存档数据前的范围 分区 边界
进入 10 月份后(在 OLTP 数据库中),9 月份的数据应移到 分区 表中,用于进行分析。移入和移出表的过程非常快,而且准备工作可以在 分区 表外完成。后面的案例研究中会对此方案进行深入的解释,但核心思想是,您可以使用“分段表”,该表最终将成为 分区 表中的一个 分区 。本文后面的案例研究中会详细介绍此方案。在此过程中,您将表的一个 分区 移出(参见图 6)到相同文件组内的一个非 分区 表中。因为相同文件组中已经存在非 分区 表(这是成功的关键),SQL Server 可以将此移动视为元数据更改。因为只是元数据更改,所以可以在几秒钟内完成,而不需要执行可能需要几小时并在大型表中产生阻塞的删除操作。移出此 分区 后,您仍然拥有 13 个 分区 ;第一个(最旧的) 分区 现在是空的,最后一个(最近的,也是空的) 分区 是需要拆分的。
图 6 :移出 分区
要删除最旧的 分区 (2003 年 9 月),请将新的合并选项(如图 7 所示)与 ALTER TABLE 结合使用。有效地合并边界点将删除边界点,从而删除 分区 。这将加载数据的 分区 数减少到 n-1 (本例中为 12)。如果不需要移动行,合并 分区 应该是一个非常快的操作(因为要合并的边界点没有数据行)。在本例中,因为第一个 分区 为空,不需要从第一个 分区 向第二个 分区 中移动任何行。如果在第一个 分区 非空的情况下合并边界点,必须将第一个 分区 的行移动到第二个 分区 中,这可能是一个代价非常高昂的操作。但是,在最常见的滑动窗口方案中(空 分区 与活动 分区 合并,并且不移动任何行),不需要执行此操作。
图 7 :合并 分区
最后,必须将新表移入 分区 表。要将此操作作为元数据更改来执行,必须在新表中( 分区 表的边界之外)加载和建立 索引 。要移入 分区 ,请先将最后一个范围和最近一个空范围拆分为两个 分区 。另外,还需要更新表的约束以允许新的范围。 分区 表将再次拥有 13 个 分区 。在滑动窗口方案中,使用 LEFT 分区 函数的最后一个 分区 将始终为空。
图 8 :拆分 分区
最后,新加载的数据已准备就绪,可以移入第十二个 分区 ,即 2004 年 9 月。
图 9 :移入 分区
表的结果是:
图 10 :加载 / 存档数据后的范围 分区 边界
因为一次只能添加或删除一个 分区 ,所以应重新创建需要添加或删除多个 分区 的表。要更改为这种新的 分区 结构,请先创建新的 分区 表,然后将数据加载到新创建的表中。与每次拆分后重新平衡整个表相比,这种方法更好。此过程是使用新的 分区 函数和新的 分区 架构,然后将数据移动到新 分区 的表中来完成的。要移动数据,请先使用 INSERT newtable SELECT columnlist FROM oldtable 复制数据,然后删除原始表。用户不应在此过程中修改数据,以防数据丢失。
有关详细信息,请参见 SQL Server Books Online 中的“ALTER PARTITION FUNCTION”和“ALTER TABLE”。
返回页首
现在,您对 分区 表的价值有了一定的了解,下一节将详细介绍实现 分区 表的过程以及有助于完成此过程的功能。逻辑流程如下:
图 11 :创建 分区 表或 索引 的步骤
确定是否应为对象 分区
虽然 分区 可以带来众多的好处,但也增加了实现对象的管理费用和复杂性,这可能是得不偿失的。尤其是,您可能不需要为较小的表或目前满足性能和维护要求的表 分区 。前面提到的销售方案使用 分区 减轻了移动行和数据的负担,但在决定是否实现 分区 时,您应考虑您的方案是否存在这种负担。
确定 分区 键和 分区 数
如果您正在尝试改善大型数据子集的性能和可管理性,并且已经定义了访问模式,则可以使用范围 分区 减少数据争用的情况,同时减少只读数据不需要 分区 时的维护工作。要确定 分区 数,应先评估您的数据中是否存在逻辑分组和模式。如果您通常一次只处理这些已定义子集中的少数几个,则应定义范围以隔离查询,使其只处理相应的数据(即,只处理特定的 分区 )。
有关详细信息,请参见 SQL Server Books Online 中的“Designing Partitioned Tables and Indexes”。
确定是否应使用多个文件组
为了有助于优化性能和维护,应使用文件组分离数据。文件组的数目一定程度上由硬件资源决定:一般情况下,文件组数最好与 分区 数相同,并且这些文件组通常位于不同的磁盘上。但是,这主要适用于打算对整个数据集进行分析的系统。如果您有多个 CPU,SQL Server 则可以并行处理多个 分区 ,从而大大缩短处理大量复杂报表和分析的总体时间。这种情况下,可以获得并行处理以及在 分区 表中移入和移出 分区 的好处。
创建文件组
如果需要为多个文件放置一个 分区 表以获得更好的 I/O 平衡,则至少需要创建一个文件组。文件组可以由一个或多个文件构成,而每个 分区 必须映射到一个文件组。一个文件组可以由多个 分区 使用,但是为了更好地管理数据(例如,为了获得更精确的备份控制),应该对 分区 表进行设计,以便只有相关数据或逻辑分组的数据位于同一个文件组中。使用 ALTER DATABASE,可以添加逻辑文件组名,然后添加文件。要为 AdventureWorks 数据库创建名为 2003Q3 的文件组,请按以下方式使用 ALTER DATABASE:
ALTER DATABASE AdventureWorks ADD FILEGROUP [2003Q3]创建文件组后,使用 ALTER DATABASE 将文件添加到该文件组中。
ALTER DATABASE AdventureWorks ADD FILE (NAME = N'2003Q3', FILENAME = N'C:\AdventureWorks\2003Q3.ndf', SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO FILEGROUP [2003Q3]通过在 CREATE TABLE 的 ON 子句中指定一个文件组,可以为文件创建一个表。但是,如果表未 分区 ,则不能为多个文件组创建一个表。要为一个文件组创建表,请使用 CREATE TABLE 的 ON 子句。要创建 分区 表,必须先确定 分区 的功能机制。进行 分区 的标准以 分区 函数的形式从逻辑上与表相分离。此 分区 函数作为独立于表的定义存在,而这种物理分离将起到帮助作用,因为多个对象都可以使用该 分区 函数。因此,为表 分区 的第一步是创建 分区 函数。
为范围 分区 创建 分区 函数
范围 分区 必须使用边界条件进行定义。而且,即使通过 CHECK 约束对表进行了限制,也不能消除该范围任一边界的值。为了允许定期将数据移入该表,需要创建最后一个空 分区 。
在范围 分区 中,首先定义边界点:如果存在五个 分区 ,则定义四个边界点值,并指定每个值是第一个 分区 的上边界 (LEFT) 还是第二个 分区 的下边界 (RIGHT)。根据 LEFT 或 RIGHT 指定,始终有一个空 分区 ,因为该 分区 没有明确定义的边界点。
具体来讲,如果 分区 函数的第一个值(或边界条件)是 '20001001',则边界 分区 中的值将是:
对于 LEFT
第一个 分区 是所有小于或等于 '20001001' 的数据
第二个 分区 是所有大于 '20001001' 的数据
对于 RIGHT
第一个 分区 是所有小于 '20001001' 的数据
第二个 分区 是所有大于或等于 '20001001' 数据
由于范围 分区 可能在 datetime 数据中进行定义,因此必须了解其含义。使用 datetime 具有某种含义:即总是同时指定日期和时间。未定义时间值的日期表示时间部分为“0”的 12:00 A.M。如果将 LEFT 与此类数据结合使用,则日期为 10 月 1 日 12:00 A.M. 的数据将位于第一个 分区 ,而 10 月份的其他数据将位于第二个 分区 。从逻辑上讲,最好将开始值与 RIGHT 结合使用,而将结束值与 LEFT 结合使用。下面的三个子句将创建逻辑上相同的 分区 结构:
RANGE LEFT FOR VALUES ('20000930 23:59:59.997', '20001231 23:59:59.997', '20010331 23:59:59.997', '20010630 23:59:59.997')或
RANGE RIGHT FOR VALUES ('20001001 0.000', '20010101 0.000', '20010401 0.000', '20010701 0.000')
或
RANGE RIGHT FOR VALUES ('20001001', '20010101', '20010401', '20010701')注意: 此处使用 datetime 数据类型确实增加了一定的复杂性,但您需要确保设置正确的边界情况。请注意使用 RIGHT 的简单性,因为默认时间为 12:00:00.000 A.M。对于 LEFT,复杂性增加是因为 datetime 数据类型具有精度。必须选择 23:59:59.997 的原因在于, datetime 数据无法保证毫秒级别的精度。相反, datetime 数据的精度在 3.33 毫秒内。使用 23:59:59.999 这个确切的时间值是不行的,因为该值将被舍入到最接近的时间值,即第二天的 12:00:00.000 A.M。由于进行了这种舍入,将无法正确定义边界。对于 datetime 数据,必须对明确提供的毫秒值加倍小心。
注意: 分区 函数还允许将函数作为 分区 函数定义的一部分。您可以使用 DATEADD(ms,-3,'20010101') ,而不是使用 '20001231 23:59:59.997' 明确定义时间。
有关详细信息,请参见 SQL Server Books Online 的“Transact-SQL Reference”中的“Date and Time”部分。
要在四个活动 分区 (每个 分区 代表一个日历季度)中存储四分之一的 Orders 数据,并创建第五个 分区 以备将来使用(还是作为占位符,用于在 分区 表中移入和移出数据),请将 LEFT 分区 函数与以下四个边界条件结合使用:
CREATE PARTITION FUNCTION OrderDateRangePFN(datetime) AS RANGE LEFT FOR VALUES ('20000930 23:59:59.997', '20001231 23:59:59.997', '20010331 23:59:59.997', '20010630 23:59:59.997')记住,定义四个边界点将创建五个 分区 。通过查看以下数据集检查此 分区 创建的数据集:
边界点 '20000930 23:59:59.997' 作为 LEFT(设置模式):
最左侧的 分区 将包含所有小于或等于 '20000930 23:59:59.997' 的值
边界点 '20001231 23:59:59.997':
第二个 分区 将包含所有大于 '20000930 23:59:59.997' 但小于或等于 '20001231 23:59:59.997' 的值
边界点 '20010331 23:59:59.997':
第三个 分区 将包含所有大于 '20001231 23:59:59.997' 但小于或等于 '20010331 23:59:59.997' 的值
边界点 '20010630 23:59:59.997':
第四个 分区 将包含所有大于 '20010331 23:59:59.997' 但小于或等于 '20010630 23:59:59.997' 的值
最后,第五个 分区 将包含所有大于 '20010630 23:59:59.997' 的值。
创建 分区 架构
创建 分区 函数后,必须将其与 分区 架构相关联,以便将 分区 定向至特定的文件组。定义 分区 架构时,即使多个 分区 位于同一个文件组中,也必须为每个 分区 指定一个文件组。对于前面创建的范围 分区 (OrderDateRangePFN),存在五个 分区 ;最后一个空 分区 将在 PRIMARY 文件组中创建。因为此 分区 永远不包含数据,所以不需要指定特殊的位置。
CREATE PARTITION SCHEME OrderDatePScheme AS PARTITION OrderDateRangePFN TO ([2000Q3], [2000Q4], [2001Q1], [2001Q2], [PRIMARY])注意: 如果所有 分区 都位于同一个文件组中,则可以使用以下更简单的语法:
CREATE PARTITION SCHEME OrderDatePScheme AS PARTITION OrderDateRangePFN ALL TO ([PRIMARY])创建 分区 表
定义 分区 函数(逻辑结构)和 分区 架构(物理结构)后,即可创建表来利用它们。表定义应使用的架构,而架构又定义函数。要将这三者结合起来,必须指定应该应用 分区 函数的列。范围 分区 始终只映射到表中的一列,此列应与 分区 函数中定义的边界条件的数据类型相匹配。另外,如果表应明确限制数据集(而不是从负无穷大到正无穷大),则还应添加 CHECK 约束。
CREATE TABLE [dbo].[OrdersRange] ( [PurchaseOrderID] [int] NOT NULL, [EmployeeID] [int] NULL, [VendorID] [int] NULL, [TaxAmt] [money] NULL, [Freight] [money] NULL, [SubTotal] [money] NULL, [Status] [tinyint] NOT NULL , [RevisionNumber] [tinyint] NULL , [ModifiedDate] [datetime] NULL , [ShipMethodID] [tinyint] NULL, [ShipDate] [datetime] NOT NULL, [OrderDate] [datetime] NOT NULL CONSTRAINT OrdersRangeYear CHECK ([OrderDate] >= '20030701' AND [OrderDate] 建立 索引 :是否 分区 ?默认情况下, 分区 表中创建的 索引 也使用相同的 分区 架构和 分区 列。如果属于这种情况, 索引 将与表对齐。尽管未作要求,但将表与其 索引 对齐可以使管理工作更容易进行,对于滑动窗口方案尤其如此。
例如,要创建唯一的 索引 , 分区 列必须是一个关键列;这将确保对相应的 分区 进行验证,以保证 索引 的唯一性。因此,如果需要在一列上对表进行 分区 ,而必须在另一个列上创建唯一的 索引 ,这些表和 索引 将无法对齐。在这种情况下,可以在唯一的列(如果是多列的唯一键,则可以是任一关键列)中对 索引 进行 分区 ,或者根本就不进行 分区 。请注意,在 分区 表中移入和移出数据时,必须删除和创建此 索引 。
注意: 如果您打算使用现有数据加载表并立即在其中添加 索引 ,则通常可以通过以下方式获得更好的性能:先加载到未 分区 、未建立 索引 的表中,然后在加载数据后创建 分区 索引 。通过为 分区 架构定义群集 索引 ,可以在加载数据后更有效地为表 分区 。这也是为现有表 分区 的不错方法。要创建与未 分区 表相同的表并创建与已 分区 群集 索引 相同的群集 索引 ,请用一个文件组目标位置替换创建表中的 ON 子句。然后,在加载数据之后为 分区 架构创建群集 索引 。
返回页首
如果您阅读了与 分区 有关的概念、优点和代码示例,则可能已对此过程有了一个很好的理解;但是,对于每个步骤,都可以使用特定的设置和选项,而且在某些情况下,还必须满足各种条件。本节将帮助您将这些内容融会贯通起来进行理解。
范围 分区 :销售数据
销售数据的使用方式经常发生变化。当前月份的数据是事务数据,而上一个月份的数据主要用于进行分析。分析通常针对月份、季度和/或年度范围的数据进行。因为不同的分析人员可能希望同时查看大量不断变化的数据,所以通过 分区 可以更好地隔离此活动。在此方案中,活动数据来自 283 个分支位置,而且是通过两个标准格式的 ASCII 文件传输的。在每个月第一天的上午 3 点之前,所有文件均被放置到一台中央文件服务器上。所有文件按大小进行排列,但每月平均约有 86,000 份销售(订单)。每个订单平均包含 2.63 个明细项,因此, OrderDetails 文件平均包含 226,180 行。每月增加约 2,500 万个新的 Orders 和 6,400 万个 OrderDetails 行,而历史分析服务器要使两年的数据都处于活动状态以便进行分析。两年的数据刚好低于 6 亿个 Orders 和超过 15 亿个 OrderDetails 行。因为分析通常是在同一季度的不同月份之间进行比较,或与上一年度的相同月份进行比较,所以可以使用范围 分区 。每个范围的边界都是按月份确定的。
按照图 11 描述的步骤,使用基于 OrderDate 的范围 分区 对表进行 分区 。了解这台新服务器的要求后,分析人员打算收集和分析连续六个月的数据,或当前年度与上一年度三个月份(例如 2003 年 1 月到 3 月与 2004 年 1 月到 3 月)的数据。要使磁盘 分区 最大化并隔离大多数数据组,多个文件组将使用相同的物理磁盘,但是这些文件组将相差六个月以减少磁盘争用。当前数据是 2004 年 10 月,而所有 283 个存储位置都在本地管理其当前销售。服务器上存储了从 2002 年 10 月到 2004 年 9 月的数据。为了利用新的 16 向多处理器计算机和存储区域网络,每个月的文件存储在一个文件组中,同时位于一个 分区 镜像 (RAID 1+0) 磁盘集上。对于数据通过文件组在逻辑驱动器上的物理布局,下图(图 12)描述了每月数据的位置。
图 12 : 分区 表的顺序
12 个逻辑驱动器都位于 RAID 1+0 配置中,因此 Orders 和 OrderDetails 数据所需的总磁盘数为 48 个。存储区域网络支持 78 个磁盘,而另外 30 个用于事务日志、 TempDB 、系统数据库和其他更小的表,例如 Customers (900 万)和 Products (386,750 行)。 Orders 和 OrderDetails 表都使用相同的边界条件、磁盘位置和 分区 架构。结果是(只看图 13 中的两个逻辑驱动器 [驱动器 E:\ 和 F:\]),相同月份的 Orders 和 OrderDetails 的数据都存储在相同的磁盘上:
图 13 :磁盘阵列上盘区位置的范围 分区
虽然看起来很复杂,但创建过程非常简单。设计 分区 表最难的部分在于从大量数据源传输数据,即 283 个存储位置都必须使用一种标准的传输机制。但是,中央服务器上只定义了一个 Orders 表和一个 OrderDetails 表。要将两个表都创建为 分区 表,请先创建 分区 函数和 分区 架构。 分区 架构定义 分区 在磁盘上的物理位置,因此必须存在文件组。在此表中,文件组是必需的,因此下一步是创建文件组。每个文件组的语法都与下面的语法相同,但必须创建所有 24 个文件组。有关创建所有 24 个文件组的完整脚本,请参见 RangeCaseStudyFilegroups.sql 脚本。
注意:如果没有指定相应的驱动器号,将无法运行此脚本;但是此脚本包含一个“setup”表,可以修改此表以简化测试。您可以将驱动器号/位置更改为一个驱动器,以测试和学习语法。同时,确保将文件大小调整为 MB 而不是 GB,并根据可用的磁盘空间考虑指定一个较小的初始大小。
将为 SalesDB 数据库创建 24 个文件和文件组。每个文件和文件组都具有相同的语法,只是位置、文件名和文件组名不相同:
ALTER DATABASE SalesDB ADD FILE (NAME = N'SalesDBFG1File1', FILENAME = N'E:\SalesDB\SalesDBFG1File1.ndf', SIZE = 20GB, MAXSIZE = 35GB, FILEGROWTH = 5GB) TO FILEGROUP [FG1] GO创建所有 24 个文件和文件组后,即可定义 分区 函数和 分区 架构。要验证文件和文件组,请分别使用 sp_helpfile 和 sp_helpfilegroup。
分区 函数将在 OrderDate 列中进行定义。使用的数据类型为 datetime ,而且两个表都需要存储 OrderDate 才能根据此值对两个表进行 分区 。实际上,如果根据相同的键值对两个表进行 分区 ,则 分区 键值属于重复信息,但它对于获得对齐优点又是必需的。而且,在大多数情况下,应该是一个相当窄的列( datetime 数据类型为 8 个字节)。如本文前面的“为范围 分区 创建 分区 函数”部分所述,此函数将是一个范围 分区 函数,其中的第一个边界条件位于 LEFT(第一个) 分区 中。
CREATE PARTITION FUNCTION TwoYearDateRangePFN(datetime) AS RANGE LEFT FOR VALUES ('20021031 23:59:59.997', -- 2002 年 10 月 '20021130 23:59:59.997', -- 2002 年 11 月 '20021231 23:59:59.997', -- 2002 年 12 月 '20030131 23:59:59.997', -- 2003 年 1 月 '20030228 23:59:59.997', -- 2003 年 2 月 '20030331 23:59:59.997', -- 2003 年 3 月 '20030430 23:59:59.997', -- 2003 年 4 月 '20030531 23:59:59.997', -- 2003 年 5 月 '20030630 23:59:59.997', -- 2003 年 6 月 '20030731 23:59:59.997', -- 2003 年 7 月 '20030831 23:59:59.997', -- 2003 年 8 月 '20030930 23:59:59.997', -- 2003 年 9 月 '20031031 23:59:59.997', -- 2003 年 10 月 '20031130 23:59:59.997', -- 2003 年 11 月 '20031231 23:59:59.997', -- 2003 年 12 月 '20040131 23:59:59.997', -- 2004 年 1 月 '20040229 23:59:59.997', -- 2004 年 2 月 '20040331 23:59:59.997', -- 2004 年 3 月 '20040430 23:59:59.997', -- 2004 年 4 月 '20040531 23:59:59.997', -- 2004 年 5 月 '20040630 23:59:59.997', -- 2004 年 6 月 '20040731 23:59:59.997', -- 2004 年 7 月 '20040831 23:59:59.997', -- 2004 年 8 月 '20040930 23:59:59.997') -- 2004 年 9 月 GO因为包含了最左侧和最右侧的边界情况,所以此 分区 函数将创建 25 个 分区 。该表将保留第 25 个 分区 为空白。不需要为这个空 分区 指定特殊的文件组(因为其中永远不会包含数据)作为限制表数据的约束。要将数据定向至相应的磁盘,可以使用 分区 架构将 分区 映射到文件组。 分区 架构将为 24 个将要包含数据的文件组使用明确的文件组名,而为第 25 个空 分区 使用 PRIMARY 文件组。
CREATE PARTITION SCHEME [TwoYearDateRangePScheme] AS PARTITION TwoYearDateRangePFN TO ( [FG1], [FG2], [FG3], [FG4], [FG5], [FG6], [FG7], [FG8], [FG9], [FG10],[FG11],[FG12], [FG13],[FG14],[FG15],[FG16],[FG17],[FG18], [FG19],[FG20],[FG21],[FG22],[FG23],[FG24], [PRIMARY] ) GO通过使用默认的文件组或用户定义的文件组作为未 分区 的表,或者使用架构创建 分区 表,可以使用与以前的版本支持的相同语法创建表。哪种方法更好取决于表的填充方式和创建的 分区 数。从性能角度看,先填充堆再建立群集 索引 可能要胜过在已经建立 索引 的表中加载数据。另外,如果有多个 CPU,您可以通过并行 BULK INSERT 语句将数据加载到表中,然后也以并行方式建立 索引 。对于 Orders 表,按照正常的方式创建表,然后通过 INSERT SELECT 语句(从 AdventureWorks 示例数据库中提取数据)加载现有的数据。要将 Orders 表建为 分区 表,请在该表的 ON 子句中指定 分区 架构。 Orders 表是使用以下语法创建的:
CREATE TABLE SalesDB.[dbo].[Orders] ( [PurchaseOrderID] [int] NOT NULL, [EmployeeID] [int] NULL, [VendorID] [int] NULL, [TaxAmt] [money] NULL, [Freight] [money] NULL, [SubTotal] [money] NULL, [Status] [tinyint] NOT NULL, [RevisionNumber] [tinyint] NULL, [ModifiedDate] [datetime] NULL, [ShipMet查看更多关于SQLServer2005中的分区表和索引的详细内容...