3. 分区函数与分区方案设计实战

分区表的核心并不是“把表拆开”,而是让 SQL Server 在物理存储层面按一套明确的边界规则,把同一张逻辑表的数据分散到不同文件组。这套规则由两个对象共同定义:分区函数(Partition Function)负责描述“边界在哪里、区间怎么闭合”,分区方案(Partition Scheme)负责描述“每个分区落到哪个文件组”。本节围绕时间维度与数值范围两种典型场景,完整走一遍从边界设计、函数创建、方案绑定到边界映射验证的过程。

一、先确定边界方向:RANGE LEFT 与 RANGE RIGHT 的语义差异

创建分区函数时,边界值列表和 LEFT/RIGHT 关键字共同决定每个分区的取值区间。SQL Server 的约定是:RANGE LEFT 表示边界值归属于左侧(较小)分区,即第一个分区为 (-∞, b1];RANGE RIGHT 表示边界值归属于右侧(较大)分区,即第一个分区为 (-∞, b1)。这个差异在时间维度上尤其关键:如果按“每月1日”作为边界,RANGE RIGHT 会让每月1日的数据落入新月份分区,符合大多数业务对“月份归属”的直觉;而 RANGE LEFT 会让每月1日的数据留在上一个月分区,容易造成对账口径偏差。

因此,时间维度通常优先选择 RANGE RIGHT,边界取每个周期的起始时刻;数值范围则视业务语义而定,例如按“金额区间 [0,1000)、[1000,5000)、[5000,+∞)”划分时,用 RANGE RIGHT 更自然。下面这张图展示了同一组边界值在两种方向下的区间归属差异,以及分区方案如何把分区映射到文件组。

分区函数边界方向与分区方案映射 边界值:2024-01-01、2024-02-01、2024-03-01(时间维度示例) RANGE RIGHT:边界值归右侧分区(推荐用于时间维度) P1 (-∞, 2024-01-01) P2 [2024-01-01, 2024-02-01) P3 [2024-02-01, 2024-03-01) P4 [2024-03-01, +∞) 2024-01-01 的数据 → P2;2024-02-01 的数据 → P3;边界值始终进入新周期分区。 适用:按月/按日归档、账单周期、日志时间戳等“周期起点归属新周期”的业务。 RANGE LEFT:边界值归左侧分区(需谨慎用于时间维度) P1 (-∞, 2024-01-01] P2 (2024-01-01, 2024-02-01] P3 (2024-02-01, 2024-03-01] P4 (2024-03-01, +∞) 2024-01-01 的数据 → P1;2024-02-01 的数据 → P2;边界值留在旧周期分区。 适用:区间上限闭合的数值分段,如 (0,100]、(100,500]、(500,+∞)。 分区方案绑定:每个分区 → 独立文件组 P1 → FG_2023 P2 → FG_2024Q1 P3 → FG_2024Q2 P4 → FG_ARCHIVE 文件组

二、时间维度分区函数与分区方案的创建

假设业务对象是订单表 dbo.Orders,按订单创建时间 OrderDate(datetime2)做月度分区,覆盖 2024 年 1 月至 2024 年 6 月,并为边界外数据预留归档分区。先创建文件组,再创建分区函数与分区方案。

-- 1. 创建文件组(实际环境需先为文件组添加数据文件)
ALTER DATABASE SalesDB ADD FILEGROUP FG_2024Q1;
ALTER DATABASE SalesDB ADD FILEGROUP FG_2024Q2;
ALTER DATABASE SalesDB ADD FILEGROUP FG_ARCHIVE;

-- 2. 创建分区函数:RANGE RIGHT,边界为每月1日
CREATE PARTITION FUNCTION PF_OrderDate_Month (datetime2)
AS RANGE RIGHT FOR VALUES
(
    '2024-01-01', '2024-02-01', '2024-03-01',
    '2024-04-01', '2024-05-01', '2024-06-01'
);

-- 3. 创建分区方案:分区数 = 边界数 + 1 = 7
CREATE PARTITION SCHEME PS_OrderDate_Month
AS PARTITION PF_OrderDate_Month
TO (FG_2023, FG_2024Q1, FG_2024Q1, FG_2024Q1,
    FG_2024Q2, FG_2024Q2, FG_2024Q2, FG_ARCHIVE);

这里有一个容易出错的点:分区方案的文件组数量必须等于分区数。6 个边界值产生 7 个分区,因此 TO 列表必须给出 7 个文件组。如果写成 6 个,SQL Server 会直接报错。文件组可以重复使用,例如 Q1 的三个分区都放在 FG_2024Q1,但分区本身仍然是独立的物理单元。

三、数值范围分区函数的写法

数值范围场景常见于按金额、评分、编号分段。例如按订单金额分为 [0,1000)、[1000,5000)、[5000,10000)、[10000,+∞) 四段,使用 RANGE RIGHT 时边界为 1000、5000、10000,分区数为 4。

CREATE PARTITION FUNCTION PF_OrderAmount (decimal(18,2))
AS RANGE RIGHT FOR VALUES (1000, 5000, 10000);

CREATE PARTITION SCHEME PS_OrderAmount
AS PARTITION PF_OrderAmount
TO (FG_SMALL, FG_MID, FG_LARGE, FG_HUGE);

如果业务定义是“1000 属于第一段”,则应改用 RANGE LEFT,边界仍为 1000、5000、10000,此时区间变为 (-∞,1000]、(1000,5000]、(5000,10000]、(10000,+∞)。可见边界值本身不变,方向关键字改变的是闭合侧。

四、验证分区边界映射

创建完成后不能只看“命令成功”,必须验证边界映射是否符合预期。最直接的方式是查询系统目录视图,观察每个分区的编号、边界值和文件组。

SELECT
    p.partition_number,
    p.rows,
    rv.value AS boundary_value,
    ds.name AS filegroup_name
FROM sys.partitions AS p
JOIN sys.destination_data_spaces AS dds
    ON p.partition_number = dds.destination_id
JOIN sys.data_spaces AS ds
    ON dds.data_space_id = ds.data_space_id
LEFT JOIN sys.partition_range_values AS rv
    ON rv.function_id = p.partition_id
   AND rv.boundary_id = p.partition_number
WHERE p.object_id = OBJECT_ID('dbo.Orders')
ORDER BY p.partition_number;

更精确的边界验证可以使用 $PARTITION 配合 sys.partition_range_values,或直接调用 sys.fn_phys_loc_to_partition 做映射测试。实际教学中更推荐用“插入测试值 + 查询分区号”的方式,因为它能直接回答“某条数据到底落在哪个分区”。

-- 插入边界测试数据
INSERT INTO dbo.Orders (OrderID, OrderDate, Amount)
VALUES (1, '2023-12-31', 100),
       (2, '2024-01-01', 100),
       (3, '2024-02-01', 100),
       (4, '2024-06-01', 100),
       (5, '2024-07-01', 100);

-- 查看每条数据所在分区
SELECT OrderID, OrderDate, $PARTITION.PF_OrderDate_Month(OrderDate) AS PartitionNo
FROM dbo.Orders
ORDER BY OrderID;

在 RANGE RIGHT 下,预期结果是:2023-12-31 → P1,2024-01-01 → P2,2024-02-01 → P3,2024-06-01 → P7,2024-07-01 → P7。如果 2024-01-01 落到了 P1,说明分区函数被误建成了 RANGE LEFT,需要重建函数与方案。注意:分区函数一旦被分区方案引用,不能直接修改边界方向,必须先解除引用或重建。

验证时建议同时检查三件事:分区总数是否等于边界数 + 1;每个分区的文件组是否与设计一致;边界值本身是否落在预期分区。三者都通过,才能认为分区方案设计正确。

五、常见设计陷阱与检查清单

  • 边界方向与业务口径不一致:时间维度误用 RANGE LEFT,导致每月1日数据归入上月,对账时出现跨月差异。
  • 文件组数量与分区数不匹配:TO 列表少写或多写,创建分区方案时直接失败。
  • 边界值类型与分区列类型不一致:分区列为 datetime2,边界写成字符串虽可隐式转换,但建议显式匹配类型,避免精度截断。
  • 未预留归档分区:最后一个分区应覆盖 +∞,否则超出边界的数据无法插入。
  • 忽略文件组物理文件:只建文件组不添加数据文件,分区仍会集中写入主文件组,失去分散 I/O 的意义。

完成本节操作后,应能独立回答三个问题:给定一组边界值,RANGE LEFT 与 RANGE RIGHT 分别产生哪些区间;分区方案的文件组列表为什么必须等于分区数;如何用 $PARTITION 验证某条数据实际落入哪个分区。这三项能力是后续分区表创建、分区切换与归档治理的基础。