企业级产品中大数据量已经是不可避免的问题,尤其对于监控行业,实时要求高,对此要求更为苛刻。故而数据的设计问题摆上日程。
此文重点在于介绍数据库设计时需要注意的问题,以及一些选择所带来的好处与附带效应。
主键:作为表中记录的区分标志。为聚集索引。至于索引的概念后续会讲到。
工作原理:类同书本目录。数据库维护主键表,主键
特征:不可重复,不可为空。
作用:简化查询条件,保证数据库数据的完整性
附带效应:额外的维护
性能对比:
1. 设置主键与不设置主键
CREATE TABLE [dbo].[STUDENT_TABLE1]( [StudentID] [int] NOT NULL, [Name] [varchar](255) COLLATE Chinese_PRC_CI_AS NOT NULL, [City] [varchar](255) COLLATE Chinese_PRC_CI_AS NULL ) ON [PRIMARY] |
CREATE TABLE [dbo].[STUDENT_TABLE2]( [StudentID] [int] NOT NULL, [Name] [varchar](255) COLLATE Chinese_PRC_CI_AS NULL, [City] [varchar](255) COLLATE Chinese_PRC_CI_AS NULL, PRIMARY KEY CLUSTERED ( [StudentID] ASC )WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] |
使用以下SQL语句插入同样记录:
declare @idx int set @idx = 10000 while @idx < 50000 begin insert into dbo.STUDENT_TABLE1 values (@idx, @idx, @idx) insert into dbo.STUDENT_TABLE2 values (@idx, @idx, @idx) set @idx = @idx+1 end |
执行SQL语句:
查询
select * from STUDENT_TABLE1 where [Name] = '38450' |
select * from STUDENT_TABLE2 where [Name] = '38450' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取177 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取179 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
即在查询条件语句中没有使用到主键作为条件查询时,均需要全表扫描一次,且可以看出设置主键的性能反而低于未设置主键的。
执行SQL语句
select * from STUDENT_TABLE1 where [Name] = '38450' and StudentID = 38450 |
select * from STUDENT_TABLE2 where [Name] = '38450' and StudentID = 38450 |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取177 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数0,逻辑读取2 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
插入
insert into STUDENT_TABLE1 values(50001,'50001','50001') |
insert into STUDENT_TABLE2 values(50001,'50001','50001') |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数0,逻辑读取1 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数0,逻辑读取2 次,物理读取1 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
删除
主键参与
delete STUDENT_TABLE1 where [Name] = '38450' and StudentID = 38450 |
delete STUDENT_TABLE2 where [Name] = '38450' and StudentID = 38450 |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取177 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数0,逻辑读取2 次,物理读取2 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
主键不参与
delete STUDENT_TABLE1 where [Name] = '38450' and StudentID = 38450 |
delete STUDENT_TABLE2 where [Name] = '38450' and StudentID = 38450 |
性能报告如下
表'STUDENT_TABLE1'。扫描计数1,逻辑读取177 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取179 次,物理读取0 次,预读176 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
更改
主键不参与
update dbo.STUDENT_TABLE1 set city = '45687' where [Name] = '38450' |
update dbo.STUDENT_TABLE2 set city = '45687' where [Name] = '38450' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取177 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取179 次,物理读取146 次,预读63 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
主键参与
update dbo.STUDENT_TABLE1 set city = '45687' where StudentID = '38450' |
update dbo.STUDENT_TABLE2 set city = '45687' where StudentID = '38450' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取177 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数0,逻辑读取2 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
总结:当有主键存在时,当主键参与条件语句时,SQL语句执行性能明显优越于无主键的数据表。当有主键存在时,主键未参与条件语句时,SQL语句执行性能略低于且趋近于无主键的数据表,且其中的更新操作相对相差最多。
注意点:
设计时注意点:
1. 不推荐使用记录本身属性作为主键,如产品编号。软件界的墨菲定律,纸包不住火,你越担心用户的需求,用户总是会提出,如更改产品编号。此时因为产品编号作为主键,也作为外键进行内部关联,改变产品编号则带来很多连带效应,不得不使用事务来进行原子性操作。一连串的操作将会带来性能的浪费
2. 不推荐使用联合主键,原因同上。联合主键其中的键部分必然是记录自身属性
3. 自增列做主键:
优点是数据库自动编号,速度快,而且是增量增长,聚集型主键按顺序存放,对于检索非常有利;数字型的,占用空间小,易排序,在程序中传递也方便;如果通过非系统增加记录(比如手动录入,或是用其他工具直接在表里插入新记录,或老系统数据导入)时,非常方便,不用担心主键重复问题。
缺点:与其他系统集成或者几个子系统汇总时,容易产生主键冲突。无法直接使用数据库的合并功能汇总,此时需要额外开发汇总或者转发程序。增加网络负载,因程序本身不知主键是多少,在插入成功之后不得不将该记录返回给程序。
4. 程序自动生成:程序自己生成管理,数据库仅将其设置成主键,而不对其进行维护。多线程并发冲突的问题交由程序管理。此时程序开发时负担较重。
5. GUID主键:强烈推荐。优点是不存在并发冲突,因为有NewId()方法,程序可以提前知道自己插入的记录的主键值,不需要回送,从而减少网络负载。便于数据库移植,不存在汇总时的主键冲突。缺点是性能不如自增列。
索引:对数据库表中一个或多个列的值进行排序的结构。有助于更快地获取信息。
补充:
此处索引分聚集索引和非聚集索引,概念如下:
聚集索引:确定表中数据的物理顺序。聚集索引类似于目录。由于聚集索引规定数据在表中的物理存储顺序,因此一个表只能包含一个聚集索引。但该索引可以包含多个列(组合索引)。聚合索引也即主键。主键如前所述,是为了维护,而不是为了性能,所以聚集索引虽然可以比索引能更好的提高查询速度,但这仅仅是其附带效应。不可以为了查询性能而定制聚集索引。
非聚集索引:下文中以索引直接指代非聚集索引。非聚集索引类似于书本后面的关键字。索引中的项目按索引键值的顺序存储,与表中的实际物理存储顺序无关。
工作原理:类似于书本后面的关键字部分。其中记录对应列的值以及对应页码,方便查询。DB在执行一条SQL语句的时候,如果存在索引,则回去索引直接定位到对应的页码行数,然后直接跳转到对应的位置进行搜索,相对全表遍历大大减少遍历的行数。
特征:
作用:简化查询条件,加快查询速度
附带效应:额外的维护,空间存储性能的浪费,以及增删改时由于额外维护带来的时间性能的浪费。每次增删改,字段的索引需要重新计算更新
性能对比:
有索引和无索引
对上表STUDENT_TABLE1中增加CITY的索引。
查询
select * from dbo.STUDENT_TABLE1 where city = '45687' |
select * from dbo.STUDENT_TABLE2 where city = '45687' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取177 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取179 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
插入
insert into STUDENT_TABLE1 values(70000,'50001','50001') |
insert into STUDENT_TABLE2 values(70000,'50001','50001') |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数0,逻辑读取11 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数0,逻辑读取2 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
删除
包含索引
delete STUDENT_TABLE1 where [CITY] = '40000' |
delete STUDENT_TABLE2 where [CITY] = '40000' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取7 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取179 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
不包含索引
delete STUDENT_TABLE1 where Name = '40000' |
delete STUDENT_TABLE2 where Name = '40000' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取177 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取179 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
删除 此处略去,与删除类似。
聚集索引和非聚集索引:
在STUDENT_TABLE2的CITY字段上创建聚集索引,在STUDENT_TABLE上创建非聚集索引。
对上表STUDENT_TABLE1中增加CITY的索引。
查询
select * from dbo.STUDENT_TABLE1 where city = '45687' |
select * from dbo.STUDENT_TABLE2 where city = '45687' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取3 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取2 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
插入
insert into STUDENT_TABLE1 values(80000,'50001','50001') |
insert into STUDENT_TABLE2 values(80000,'50001','50001') |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数0,逻辑读取3 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数0,逻辑读取12 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
删除
包含索引
delete STUDENT_TABLE1 where city = '50001' |
delete STUDENT_TABLE2 where city = '50001' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取7 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取4 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
不包含索引
delete STUDENT_TABLE1 where name = '30000' |
delete STUDENT_TABLE2 where name = '30000' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取179 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取194 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
更新
update dbo.STUDENT_TABLE1 set name = '100' where city = '25000' |
update dbo.STUDENT_TABLE2 set name = '100' where city = '25000' |
性能报告如下:
表'STUDENT_TABLE1'。扫描计数1,逻辑读取3 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
表'STUDENT_TABLE2'。扫描计数1,逻辑读取2 次,物理读取0 次,预读0 次,lob 逻辑读取0 次,lob 物理读取0 次,lob 预读0 次。 |
注意点:
1. 如果每次都需要取到所有表记录,无论如何都必须进行全表扫描了,那么增加索引不会带来查询速度的优化,仅仅是附带效应带来的性能的降低。
2. 对于存在大量重复值的字段如“性别”增加索引带来的实际意义不大
3. 对于记录比较少的表,增加索引优化带来的性能远不如索引开销的性能浪费
填充因子:索引的一个特性,定义该索引每页上的可用空间量。FILLFACTOR 适应以后表数据的扩展并减小了页拆分的可能性。FILLFACTOR 是从 1 到 100 之间的某个值,指定索引页保留为空的百分比。
创建索引的原则:性能优化,用于频繁搜索的列
总结:
对于查询性能的优化:
聚集索引 > 非聚集索引 > 无索引
分区:一种物理数据库设计技术,目的是为了在特定的SQL操作中减少数据读写的总量以缩减响应时间。
分区主要有两种形式:水平分区、垂直分区。
水平分区:对表的行进行分区,通过这样的方式不同分组里面的物理列分割的数据集得以组合,从而进行个体分割(单分区)或集体分割(1个或多个分区)。所有在表中定义的列在每个数据集中都能找到,所以表的特性依然得以保持。
垂直分区:这种分区方式一般来说是通过对表的垂直划分来减少目标表的宽度,使某些特定的列被划分到特定的分区,每个分区都包含了其中的列所对应的行。
操作步骤:
1. 增加文件组:
ALTER DATABASE TSET ADD FILEGROUP[fg1] ALTER DATABASE TEST ADD FILE (Name = ‘fg1’FILENAME = ‘D:\fg1.NDF’SIZE = 5MB, FILEGROWTH = 5MB) TO FILEGROUP fg1 |
ALTER DATABASE TSET ADD FILEGROUP[fg2] ALTER DATABASE TEST ADD FILE (Name = ‘fg2’FILENAME = ‘D:\fg2.NDF’SIZE = 5MB, FILEGROWTH = 5MB) TO FILEGROUP fg2 |
ALTER DATABASE TSET ADD FILEGROUP[fg3] ALTER DATABASE TEST ADD FILE (Name = ‘fg3’FILENAME = ‘D:\fg3.NDF’SIZE = 5MB, FILEGROWTH = 5MB) TO FILEGROUP fg3 |
2. 创建分区函数:用于定义你希望SQLSERVER如何对数据进行分区的参数值,这个操作并不涉及任何表格,只是单纯的定义了一项技术来分割数据
CREATE PARTITION FUNCTION custom_partFunc (int) AS RANGES RIGHT FOR VALUES (25000,50000) |
上面这个函数定义了三个分区。[0,25000)、[25000,50000)、[50000,无穷大)。此处RIGHT表示范围的开闭,RIGHT表示开),LEFT表示闭]。
3. 创建分区架构:一旦给出了描述如何分割数据的分区函数,接着就要创建一个分区架构,用来定义分区位置。
CREATE PARTITION SCHEME custom_partScheme AS PARTION custom_partFunc TO (fg1,fg2, fg3) |
这里将一个分区函数连接到了该分区架构,但并没有将分区架构连接到任何数据表。
4. 对表进行分区
CREATE TABLE customs(FirstName nvarchar(40), LastName nvachar(40), CustomerNumber int) ON custom_partScheme(CustomerNumber) |
此处未测试,因为分区仅企业版SQL可以使用,XP系统不允许安装企业版。
分表:此处为根据用户使用频率额外增加TABLE,其中放置最常用数据,以加快响应。采取该措施符合让用户最关心的数据更接近用户的原则。
优点:查询响应快
缺点:额外维护,浪费部分性能。