数据库优化问题分析


         企业级产品中大数据量已经是不可避免的问题,尤其对于监控行业,实时要求高,对此要求更为苛刻。故而数据的设计问题摆上日程。

         此文重点在于介绍数据库设计时需要注意的问题,以及一些选择所带来的好处与附带效应。

 

主键:作为表中记录的区分标志。为聚集索引。至于索引的概念后续会讲到。

工作原理:类同书本目录。数据库维护主键表,主键

特征:不可重复,不可为空。

作用:简化查询条件,保证数据库数据的完整性

附带效应:额外的维护

性能对比

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_TABLE2CITY字段上创建聚集索引,在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,其中放置最常用数据,以加快响应。采取该措施符合让用户最关心的数据更接近用户的原则。

优点:查询响应快

缺点:额外维护,浪费部分性能。

 

你可能感兴趣的:(数据库优化)