SqlServer2005 海量数据 数据表分区解决难题

时间:2021-08-22 04:41:26

超大型数据库的大小常常达到数百GB,有时甚至要用TB来计算。而单表的数据量往往会达到上亿的记录,并且记录数会随着时间而增长。这不但影响着数据库的运行效率,也增大数据库的维护难度。除了表的数据量外,对表不同的访问模式也可能会影响性能和可用性。这些问题都可以通过对大表进行合理分区得到很大的改善。当表和索引变得非常大时,分区可以将数据分为更小、更容易管理的部分来提高系统的运行效率。如果系统有多个CPU或是多个磁盘子系统,可以通过并行操作获得更好的性能。所以对大表进行分区是处理海量数据的一种十分高效的方法。本文通过一个具体实例,介绍如何创建和修改分区表,以及如何查看分区表。

1    SQL Server 2005

  SQL Server 2005是微软在推出SQL Server 2000后时隔五年推出的一个数据库平台,它的数据库引擎为关系型数据和结构化数据提供了更安全可靠的存储功能,使用户可以构建和管理用于业务的高可用和高性能的数据应用程序。此外SQL Server 2005结合了分析、报表、集成和通知功能。这使企业可以构建和部署经济有效的BI解决方案,帮助团队通过记分卡、Dashboard、Web Services和移动设备将数据应用推向业务的各个领域。无论是开发人员、数据库管理员、信息工作者还是决策者,SQL Server 2005都可以提供出创新的解决方案,并可从数据中获得更多的益处。

  它所带来的新特性,如T-SQL的增强、数据分区、服务代理和与.Net Framework的集成等,在易管理性、可用性、可伸缩性和安全性等方面都有很大的增强。

2    表分区的具体实现方法

  表分区分为水平分区和垂直分区。水平分区将表分为多个表。每个表包含的列数相同,但是行更少。例如,可以将一个包含十亿行的表水平分区成 12 个表,每个小表表示特定年份内一个月的数据。任何需要特定月份数据的查询只需引用相应月份的表。而垂直分区则是将原始表分成多个只包含较少列的表。水平分区是最常用分区方式,本文以水平分区来介绍具体实现方法。

  水平分区常用的方法是根据时期和使用对数据进行水平分区。例如本文例子,一个短信发送记录表包含最近一年的数据,但是只定期访问本季度的数据。在这种情况下,可考虑将数据分成四个区,每个区只包含一个季度的数据。

2.1     创建文件组

  建立分区表先要创建文件组,而创建多个文件组主要是为了获得好的 I/O 平衡。一般情况下,文件组数最好与分区数相同,并且这些文件组通常位于不同的磁盘上。每个文件组可以由一个或多个文件构成,而每个分区必须映射到一个文件组。一个文件组可以由多个分区使用。为了更好地管理数据(例如,为了获得更精确的备份控制),对分区表应进行设计,以便只有相关数据或逻辑分组的数据位于同一个文件组中。使用 ALTER DATABASE,添加逻辑文件组名:

ALTER DATABASE [DeanDB] ADD FILEGROUP [FG1]

DeanDB为数据库名称,FG1文件组名。创建文件组后,再使用 ALTER DATABASE 将文件添加到该文件组中:

ALTER DATABASE [DeanDB] ADD FILE ( NAME = N'FG1', FILENAME = N'C:\DeanData\FG1.ndf' , SIZE = 3072KB , FILEGROWTH = 1024KB ) TO FILEGROUP [FG1]

  类似的建立四个文件和文件组,并把每一个存储数据的文件放在不同的磁盘驱动器里。

2.2     创建分区函数

  创建分区表必须先确定分区的功能机制,表进行分区的标准是通过分区函数来决定的。创建数据分区函数有RANGE “LEFT | / RIGHT”两种选择。代表每个边界值在局部的哪一边。例如存在四个分区,则定义三个边界点值,并指定每个值是第一个分区的上边界 (LEFT) 还是第二个分区的下边界 (RIGHT)[1]。代码如下:

CREATE PARTITION FUNCTION [SendSMSPF](datetime) AS RANGE RIGHT FOR VALUES ('20070401', '20070701', '20071001')

2.3     创建分区方案

  创建分区函数后,必须将其与分区方案相关联,以便将分区指向至特定的文件组。就是定义实际存放数据的媒体与各数据块的对应关系。多个数据表可以共用相同的数据分区函数,一般不共用相同的数据分区方案。可以通过不同的分区方案,使用相同的分区函数,使不同的数据表有相同的分区条件,但存放在不同的媒介上。创建分区方案的代码如下:

CREATE PARTITION SCHEME [SendSMSPS] AS PARTITION [SendSMSPF] TO ([FG1], [FG2], [FG3], [FG4])

2.4     创建分区表

  建立好分区函数和分区方案后,就可以创建分区表了。分区表是通过定义分区键值和分区方案相联系的。插入记录时,SQL SERVER会根据分区键值的不同,通过分区函数的定义将数据放到相应的分区。从而把分区函数、分区方案和分区表三者有机的结合起来。创建分区表的代码如下:

CREATE TABLE SendSMSLog

([ID] [int] IDENTITY(1,1) NOT NULL,

[IDNum] [nvarchar](50) NULL,

[SendContent] [text] NULL

[SendDate] [datetime] NOT NULL,

) ON SendSMSPS(SendDate)

2.5     查看分区表信息

  系统运行一段时间或者把以前的数据导入分区表后,我们需要查看数据的具体存储情况,即每个分区存取的记录数,那些记录存取在那个分区等。我们可以通过$partition.SendSMSPF来查看,代码如下:

SELECT $partition.SendSMSPF(o.SendDate)

                     AS [Partition Number]

       , min(o.SendDate) AS [Min SendDate]

       , max(o.SendDate) AS [Max SendDate]

       , count(*) AS [Rows In Partition]

FROM dbo.SendSMSLog AS o

GROUP BY $partition.SendSMSPF(o.SendDate)

ORDER BY [Partition Number]

在查询分析器里执行以上脚本,结果如图1所示:

 

图1 分区表信息

SqlServer2005 海量数据 数据表分区解决难题


2.6     
维护分区

  分区的维护主要设计分区的添加、减少、合并和在分区间转换。可以通过ALTER PARTITION FUNCTION的选项SPLIT,MERGE和ALTER TABLE的选项SWITCH来实现。SPLIT会多增加一个分区,而MEGRE会合并或者减少分区,SWITCH则是逻辑地在组间转换分区。

3    性能对比

  我们对2650万数据,存储空间占用约4G的单表进行性能对比,测试环境为IBM365,CPU 至强2.7G*2、内存16G、硬盘 136G*2,系统平台为Windows 2003 SP1+SQL Server 2005 SP1。测试结果如表1:

表1:分区和未分区性能对比表(单位:毫秒)

测试项目          分区            未分区

1             16546             61466

2                13                33

3             20140             61546

4             17140             61000

说明:

1:根据时间检索某一天记录所耗时间

2:单条记录插入所耗时间

3:根据时间删除某一天记录所耗时间

4:统计每月的记录数所需时间

  从表1可以看出,对分区表进行操作比未分区的表要快,这是因为对分区表的操作采用了CPU和I/O的并行操作,检索数据的数据量也变小了,定位数据所耗时间变短。

4    结束语

  对海量数据的处理一直是一个令人头痛的问题。分离的技术是所有设计者们首先考虑的问题,不管是分离应用程序功能还是分离数据访问,如果加以了合理规划,都能十分有效的解决大数据表的运行效率低和维护成本高等问题。SQL Server 2005新增的表分区功能,可以对数据进行合理分区,当用户在访问部分数据时,SQL Server最佳化引擎可以根据数据的实体存放,找出最佳的执行方案,而不至于大海捞针。

 

 

 

 当我们数据量比较大的时候,我们需要将大型表拆分为多个较小的表,则只访问部门数据的查询就可以更快的运行,基本原理就是,因为要扫描的数据变的更小。维护任务(例如,重新生成索引或备份表)也可以更快的运行。

   我们可以再不通过将表物理放置在多个磁盘驱动器上来拆分表的情况下获取分区。如果将某个表放置在一个物理驱动器上,将相关表放置在另一个驱动器上,则可以提高查询性能,因为当运行涉及表间连接的查询时,多个磁盘头同时读取数据。可以使用SQL Server文件组来指定放置表的磁盘。

  对于分区的方式,基本就三种方式:硬件分区、水平分区、垂直分区。相关方案可以参考SQL联机丛书

  这里我们介绍分区表的具体实战方法:

  第一步,首先建立我们要使用的数据库,最重要的是建立多个文件组。

  我们先新建立四个目录,来组成文件组,一个用来存放主文件的目录:Primary

  三个数据文件目录:FG1、FG2、FG3

  建立库: 

SqlServer2005 海量数据 数据表分区解决难题
create  database  Sales on primary
(
name=N'Sales',
filename=N'G:\data\Primary\Sales.mdf',
size=3MB,
maxsize=100MB,
filegrowth=10%
),
filegroup FG1
(
NAME = N'File1',
FILENAME = N'G:\data\FG1\File1.ndf',
SIZE = 1MB,
MAXSIZE = 100MB,
FILEGROWTH = 10%
),
FILEGROUP FG2
(
NAME = N'File2',
FILENAME = N'G:\data\FG2\File2.ndf',
SIZE = 1MB,
MAXSIZE = 100MB,
FILEGROWTH = 10%
),
FILEGROUP FG3
(
NAME = N'File3',
FILENAME = N'G:\data\FG3\File3.ndf',
SIZE = 1MB,
MAXSIZE = 100MB,
FILEGROWTH = 10%
)
LOG ON
(
NAME = N'Sales_Log',
FILENAME = N'G:\data\Primary\Sales_Log.ldf',
SIZE = 1MB,
MAXSIZE = 100MB,
FILEGROWTH = 10%
)
GO
SqlServer2005 海量数据 数据表分区解决难题

第二步:建立分区函数,目的是用来规范不同数据存放到不同目录的标准,简单讲就是如何分区。

USE Sales   
GO
CREATE PARTITION FUNCTION pf_OrderDate (datetime)
AS RANGE RIGHT
FOR VALUES ('2003/01/01', '2004/01/01')
GO

我们创建了一个用于数据类型为datetime的分区函数,按照时间段来划分
文件组 分区    取值范围
FG1    1        (过去某年, 2003/01/01)
FG2    2       [2003/01/01, 2004/01/01)
FG3    3        [2004/01/01,未来某年)

第三步:创建分区方案,关联到分区函数。目的就是我们将已经建立好的分区函数组织成一套方案,简单点将就是我们在哪里对数据进行分区。

Use Sales
go
create partition scheme ps_OrderDate
as partition pf_OrderDate
to(FG2,FG2,FG3)
go

很简单,就是将第二步建立的分区函数应用已经建立的分区组中。
第四步:创建分区表。创建表并将其绑定到分区方案上。我们首先建立两个表,一张原始表另一张用来归档数据,保存归档数据。

SqlServer2005 海量数据 数据表分区解决难题
Use Sales
go
create table Orders
(
OrderID int identity(10000,1),
OrderDate datetime not null,
CustomerID int not null,
constraint PK_Orders primary key(OrderID,OrderDate)
)
on ps_OrderDate(OrderDate)
go
create table OrdersHistory
(
OrderID int identity(10000,1),
OrderDate datetime not null,
CustomerID int not null,
constraint PK_OrdersHistory primary key(OrderID,OrderDate)
)
on ps_OrderDate(OrderDate)
go
SqlServer2005 海量数据 数据表分区解决难题

到这里,通过上面的四步我们已经完整的搭建好了一个带有分区表的库,我们来插入一些数据,来测试下我们建立是否好用。

首先,因为是用2003年1月1号作为区分点的,我们先向数据表中写入2002年的规范数据

SqlServer2005 海量数据 数据表分区解决难题
USE Sales   
GO
INSERT INTO dbo.Orders (OrderDate, CustomerID) VALUES ('2002/6/25', 1000)
INSERT INTO dbo.Orders (OrderDate, CustomerID) VALUES ('2002/8/13', 1000)
INSERT INTO dbo.Orders (OrderDate, CustomerID) VALUES ('2002/8/25', 1000)
INSERT INTO dbo.Orders (OrderDate, CustomerID) VALUES ('2002/9/23', 1000)
GO
SqlServer2005 海量数据 数据表分区解决难题

同样我们写入2003年四条数据

SqlServer2005 海量数据 数据表分区解决难题
USE Sales   
GO
INSERT INTO dbo.Orders (OrderDate, CustomerID) VALUES ('2003/6/25', 1000)
INSERT INTO dbo.Orders (OrderDate, CustomerID) VALUES ('2003/8/13', 1000)
INSERT INTO dbo.Orders (OrderDate, CustomerID) VALUES ('2003/8/25', 1000)
INSERT INTO dbo.Orders (OrderDate, CustomerID) VALUES ('2003/9/23', 1000)
GO
SqlServer2005 海量数据 数据表分区解决难题

我们来查看这些数据是否完整录入:

SqlServer2005 海量数据 数据表分区解决难题

因为OrdersHistory表我们还没有归档数据,所以为空。

我们来分条件查询下:

1、查询某个分区

这里我们要用到$partition函数。这个函数在联机丛书中是这样解释的:

SqlServer2005 海量数据 数据表分区解决难题
用法:
为任何指定的分区函数返回分区号,一组分区列值将映射到该分区号中。

语法:
[ database_name. ] $PARTITION.partition_function_name(expression)

参数:
database_name
包含分区函数的数据库的名称。

partition_function_name
对其应用一组分区列值的任何现有分区函数的名称。

expression
其数据类型必须匹配或可隐式转换为其对应分区列数据类型的表达式。expression 也可以是当前参与 partition_function_name 的分区列的名称。

返回类型:
int

备注:
$PARTITION 返回从 1 到分区函数的分区数之间的 int 值。

$PARTITION 将针对任何有效值返回分区号,无论此值当前是否存在于使用分区函数的分区表或索引中。
SqlServer2005 海量数据 数据表分区解决难题

我们来查询分区表Order的第一个分区,代码如下:
SqlServer2005 海量数据 数据表分区解决难题

可以看到我们查询出来的数据全部为2002年的,也就是说在第一分区中我们存入的数据都是小于2003年,按照此推断2003年的数据,就应该存在第二分区中:

SqlServer2005 海量数据 数据表分区解决难题

结果如我们所料,我们可以按照这个分区进行分组来查看各个分区的数据行多少,代码如下:

select $partition.pf_OrderDate(OrderDate) as Patition,COUNT(*) countRow from dbo.Orders
group by $partition.pf_OrderDate(OrderDate)

还可以通过$Partition函数获得一组分区标识列值的分区号,例如获得2002属于哪个分区,代码如下:

SqlServer2005 海量数据 数据表分区解决难题

 2、归档数据

假如现在是2003年年初,那么我们就可以把2002您所有的交易记录归档到我们刚才建立的历史订单表HistroryOrder中。代码如下:

Use Sales
go
alter table orders switch partition 1 to ordersHistory partition 1
go

现在我们再重新查看这两张表的数据:

SqlServer2005 海量数据 数据表分区解决难题

这时候Orders表只剩下2003年的数据,而OdersHistory表中包含了2002年的数据。

简单点讲就是把第一区的数据导入到另一张分区表的第一区中

当然如果到了2004年年初,我们就可以归档2003年的所有交易数据。

Use Sales
go
alter table orders switch partition 2 to ordersHistory partition 2
go

这里需要注意的是我们按照区进行数据修改的时候,必须是同一种分区函数下的分区表进行操作,并且分区结构相对应,如果不这样会报错,例如:

SqlServer2005 海量数据 数据表分区解决难题

  3、添加分区

当我们需要新添加分区的时候,我们需要修改分区方案,比如现在我们到了2005年年初,我们需要为2005年的交易记录准备分区,就需要添加分区:

SqlServer2005 海量数据 数据表分区解决难题
USE [master]
GO
ALTER DATABASE [Sales] ADD FILEGROUP [FG4]
GO
ALTER DATABASE [Sales] ADD FILE ( NAME = N'File4', FILENAME = N'G:\data\FG4\File4.ndf' , SIZE = 3072KB , FILEGROWTH = 1024KB ) TO FILEGROUP [FG4]
GO
SqlServer2005 海量数据 数据表分区解决难题

我们新建立了一个文件组,然我们同样按照上面的方法,进行修改分区函数和方案:

use Sales
go
alter partition scheme ps_OrderDate next used [FG4]
alter partition function pf_OrderDate() split range('2005/01/01')
go

我们这里用alter partition Scheme ps_OrderDate Next Used FG4用来指定新分区的数据在那个文件。这里Next Used FG4指定的就是我们刚才新建立的第四个文件组。当然我们可以放在原来已经建立的文件组,为了防治数据混乱存放我们大部分是新建立文件组。

alter partition function pf_OrderDate() split range('2005/01/01')代表我么创建一个新分区,而这里split range是创建新分区的关键语法。

至此,我们就有了四个分区,此时的区间如下:

文件组 分区    取值范围
FG1    1        (过去某年, 2003/01/01)
FG2    2       [2003/01/01, 2004/01/01)
FG3    3        [2004/01/01,2005/01/01]

FG4    4         [2004/01/01,未来某年)

4、删除分区

删除分区又称合并分区,简单讲就是两个分区的数据进行合并,比如我们想合并2002年的分区和2003年的分区到一个分区,我们可以用如下的代码:

use Sales
go
alter partition function pf_OrderDate() merge range('2003/01/01')
go

也就是将2003年这个分区点去掉,里面分区里面的数据会自动合并到一起。

执行完上面的代码,此时分区区间如下:
文件组 分区      取值范围
Fg2     1        [过去某年, 2004/01/01)
Fg3     2        [2004/01/01, 2005/01/01)
Fg2     3        [2005/01/01, 未来某年)

合并2002和2003年的数据到2003年之后,我们执行如下代码:

SELECT Sales.$PARTITION.pf_OrderDate('2003')

你会发现返回的结果是1。而原来返回的是2,原因是2002年以前数据所在的那个分区合并到了2003年这个分区中了。
此时我们执行下面代码:

SELECT *
FROM dbo.OrdersHistory
WHERE $PARTITION.pf_OrderDate(OrderDate) = 2

结果一行数据都没返回,事实就这样,因为OrderHistroy表中只存储了2002和2003年的历史数据,在没有合并分区之前,执行上面的代码肯定会查询出2003年的数据,但是合并了分区之后,上面代码实际查询的是第二个分区中2004年的数据。
不过我们改成如下代码:

SELECT *
FROM dbo.OrdersHistory
WHERE $PARTITION.pf_OrderDate(OrderDate) = 1

便会查询出8行数据,包括2002年和2003年的数据,因为合并分区后2002年和2003年的数据都成了第1分区的数据了。
5、查看元数据

我们可以通过三个系统视图来查看我们的分区函数,分区方案,边界值点等。

select * from sys.partition_functions   
select * from sys.partition_range_values
select * from sys.partition_schemes