随笔-27  评论-160  文章-10  trackbacks-15
 

如何提高增加包含大量记录的表的主键字段的效率

LazyBee

1 问题的提出:

在给客户升级数据库系统时,由于报表的需要,系统中每一个表都需要有主键字段。系统审计表自然也有这个要求需要增加一个identify的字段,但这个表中有2000多万条记录,使用以下SQLl语句:alter table erAuditEventTime add EventTime_ID int IDENTITY primary key clustered来增加时需要4个多小时。客户要求我们对此进行提速。

2 抛砖--解决过程

问题出来之后,我第一时间上google去狂搜了一番,可是没有一条是关于对增加主键提速的。都是说建表增加主键提高效率的。这可怎么办?刚开始我以为是日志增长过快的原因,因为我在测试数据库上执行此语句时发现数据库日志文件在以“光速”狂飙,一段时间下来就长到十几个G,于是将数据库的恢复模式改成Simple(简单),效率还是没有多大改善,于是认为会不会是锁的问题呢,但是由于SQL Server会根据情况自动将锁升级的,应该没有问题,不管怎么样还是给加上了with nocheck选项。好像依然没戏,不知什么时候“灵光”一现,我能不能采用拷贝策略呢,不管三七二十一先试试再说,于是将上面的语句改写成下面的方式:

 1--Rename table
 2exec sp_rename 'erAuditEventTime','zxg_erAuditEventTime'
 3go
 4--Copy table schema
 5select top 0 * into erAuditEventTime from zxg_erAuditEventTime 
 6go
 7--Add identify field
 8alter table erAuditEventTime 
 9add EventTime_ID int IDENTITY primary key clustered
10go
11--Copy data
12insert into erAuditEventTime select * from zxg_erAuditEventTime 
13go
14


改完之后,测试发现,完成这些语句需要3826秒。欣喜……

3 反思

为什么将语句改成这种形式之后,效率能提高这么多呢?我觉得可能跟SQL Server的数据库物理存储有关。对此,让我们先了解一下SQL Server的物理存储:

数据库文件:SQL Server包含三种类型的数据库文件主数据文件(Primary Data Files)、次要数据文件(Secondary data files)、日志文件(Log files)。主数据文件是数据库的起点,指向数据库中的其他文件。每个数据库都只有一个主数据文件(扩展名为.mdf)。除主数据文件以外的其他数据文件都是次要数据文件。有些数据库可能不含任何次要数据文件,而有些数据库则含有多个次要数据文件(扩展名为.ndf)。日志文件包含用于恢复数据库的所有日志信息。每个数据库至少有一个日志文件,当然也可以有多个(扩展名为.ldf)。数据库中所有文件的位置都记录在数据库的主文件和master数据库中。大多数情况下,SQL Server数据库引擎使用master数据库中的文件信息。只有在下列情况下,数据库引擎使用主数据文件的文件位置信息初始化master数据库中的文件位置项:还原master数据库时、使用带有For AttachFor ATTACH_REBUILD_LOG选项的Create Database语句来附加数据库时、从SQL Server2000升级到SQL Server2005时。

数据库文件组:文件组是命名的文件集合,为了便于分配和管理,可以将数据库对象和文件一起分成文件组。有两种类型的文件组:主文件组、用户定义文件组。主文件组包含主数据文件和没有明确分配给其他文件组的其他文件。系统表的所有页均分配在主文件组中。用户定义的文件组是通过在Create Database Alter DataBase语句中使用FILEGROUP关键字指定的任何文件组。(日志文件不包括在文件组内,日志空间和数据空间分开管理)。并且一个文件只能属于一个文件组。每个数据库中均有一个文件组被指定为默认文件组,如果创建表或索引时未指定文件组,则将假定所有页都从默认文件组分配。一次只能有一个文件组作为默认文件组。db_owner成员可以将默认文件组从一个文件组切换到另一个。如果没有指定默认文件组,则将主文件组作为默认文件组。

页(Page:是SQL Server中存储数据的基本单位是页(Page),页的大小是8K,也就是SQL Server数据库中每MB中有128页。每页的开头是96个字节的标头,用于存储有关页的系统信息,包括页码、页的类型、页的可用空间以及拥有该页的对象的分配单元ID。下表说明SQL Server的数据库文件中使用的页类型:

页类型

内容

Data

text in row设置为 ON 时,包含除 text ntextimagenvarchar(max)varchar(max)varbinary(max) xml数据之外的所有数据的数据行。

Index

索引条目。

Text/Image

大型对象数据类型:

·                      text ntextimagenvarchar(max)varchar(max)varbinary(max) xml数据。

数据行超过 8 KB 时为可变长度数据类型列:

·                      varcharnvarcharvarbinary sql_variant

Global Allocation MapShared Global Allocation Map

有关区是否分配的信息。

Page Free Space

有关页分配和页的可用空间的信息。

Index Allocation Map

有关每个分配单元中表或索引所使用的区的信息。

Bulk Changed Map

有关每个分配单元中自最后一条 BACKUP LOG 语句之后的大容量操作所修改的区的信息。

Differential Changed Map

有关每个分配单元中自最后一条 BACKUP DATABASE 语句之后更改的区的信息。

注意:日志文件不包含页,而是包含一系列的日志记录。数据库的每个文件都有一个唯一的文件ID号,并且数据文件中的页是按顺序编号的,文件的首页以0开始。若要唯一表示数据库中的页,需要同时使用文件ID和页码。

(Extents)8个物理上连续的页为一个区(即64k.区是SQL Server管理空间的基本单位,也就是说SQL Server为了提高效率,给对象分配空间时是以区为单位的,而不是以页为单位。为了使分配空间更有效,SQL Server不会将某一个区中的所有空间分配给包含少量数据的表。为此,SQL Server包含两种类型的区:统一区和混合区统一区由单个对象所有。区中的所有 8 页只能由所属对象使用。混合区,最多可由八个对象共享。区中八页的每页可由不同的对象所有。通常从混合区向新表或索引分配页。当表或索引增长到 8 页时,将变成使用统一区进行后续分配。如果对现有表创建索引,并且该表包含的行足以在索引中生成 8 页,则对该索引的所有分配都使用统一区进行。

结论:由于在对含有大量数据的erAuditEventTime表增加一个Identify字段的时,对每一行数据都需要进行变更,SQL Server为了保证同一行数据都位于同一页中,所以需要频繁移动原有页中的数据,导致大量而且频繁的IO操作;而采用另外新建一个表,然后使用insert into 语句来进行数据拷贝工作时,SQL Server只需要给新的表分配一系列的没有使用的统一区就可以了,大大减少了IO操作。而且这两种方式创建和保存索引的性能是一样的没有区别,这可能就是性能相差这么大的真正原因。

4 引玉

由于本人对SQL Server也不是特别懂,所以也请园子里的大虾们也发表发表高论,看看是否是这个原因导致的性能差异,以及针对这种案例是否有更好的解决方案?(转载请注明出处:http://lazybee.cnblogs.com/,谢谢!)

posted on 2008-07-15 16:07 懒蜜蜂 阅读(1783) 评论(39)  编辑 收藏 所属分类: SQL

评论:
#1楼  2008-07-15 16:20 | looping      
站位
  回复  引用  查看    
#2楼 [楼主] 2008-07-15 16:33 | 懒蜜蜂      
希望大家踊跃发表看法,谢谢!
  回复  引用  查看    
#3楼  2008-07-15 16:36 | 2008年的梦想      
学习
  回复  引用  查看    
#4楼  2008-07-15 17:24 | Tony Zhou      
good
  回复  引用  查看    
#5楼  2008-07-15 19:21 | 金色海洋(jyk)      
可能还有一个原因,硬盘的读写速度。

建议找一个软件测试一下硬盘的读写速度,如果低于 40MB/s的话,那么花费这么长的时间就比较正常了。

另外我猜测,使用的是scic硬盘吧,这种硬盘的弱点就是——连续的读写硬盘,最快的好像也只有80MB/s,在不做read的情况下。


还有就是记得由一个语句,可以不记录日志,就是想不起来怎么写了,不记录日志,应该能够更快一点。
  回复  引用  查看    
#6楼  2008-07-15 19:46 | progame      
这个很有意思 确实可能是对页的频繁分配导致的 这样直接新建复制的话更快
  回复  引用  查看    
#7楼  2008-07-15 21:03 | zqwuwei      
不错!
  回复  引用  查看    
#8楼  2008-07-15 22:12 | yuuhhe      
怀疑是建 primary key clustered 的关系,试着删掉试试,只执行:
alter table erAuditEventTime add EventTime_ID int IDENTITY
如果强烈要求EventTime_ID唯一,只加上唯一约束
个人觉得clustered很耗IO
  回复  引用  查看    
#9楼  2008-07-16 01:17 | whynot      
让我想到vba。
dim Tempsheet as ActiveWorkbook.Worksheet
操作变量表总比直接操作表效率高。
同上,操作临时表比操作正式表……
  回复  引用  查看    
#10楼  2008-07-16 08:39 | comdeng [未注册用户]
最近在看sqlserver2000的书,上边提到页面头大小为132个字节,而不是lz所说的96,希望能核实一下,或者是版本原因?
  回复  引用    
#11楼 [楼主] 2008-07-16 09:03 | 懒蜜蜂      
@金色海洋(jyk)
确实是使用的SCSI硬盘,可能SCSI硬盘确实存在兄台所说的弱点,不过文中两种方法都有读写操作,应该速度差不多。所以个人认为这个可能不是根本原因。
期待兄台的不记日志的语句,谢谢!
  回复  引用  查看    
#12楼 [楼主] 2008-07-16 09:04 | 懒蜜蜂      
@looping
@2008年的梦想
@Tony Zhou
@progame
@zqwuwei

谢谢捧场:)
  回复  引用  查看    
#13楼  2008-07-16 09:10 | 小龙3      
文章的错别字:用于存储有关页的系统"欣喜"

应该是“信息”吧?
  回复  引用  查看    
#14楼 [楼主] 2008-07-16 09:41 | 懒蜜蜂      
@yuuhhe
好的,我试试看
  回复  引用  查看    
#15楼 [楼主] 2008-07-16 09:42 | 懒蜜蜂      
@whynot
做了这么多年VB,还真没有发现这个:)
  回复  引用  查看    
#16楼 [楼主] 2008-07-16 09:45 | 懒蜜蜂      
@comdeng
页面标头肯定是96个字节,这个2000和2005都是一样的,每页能存储的是8060个字节,但页尾还有一个行偏移表。不知道兄台看的是什么书,不过我说的这个你可以去微软的MSDN上去查证。
  回复  引用  查看    
#17楼  2008-07-16 09:48 | zzzzz [未注册用户]
新建一个表,再Insert试试。
  回复  引用    
#18楼  2008-07-16 09:50 | 朝晖的.net      
楼主:

"混合区,最多可由八个对象共享。区中八页的每页可由不同的对象所有。通常从混合区向新表或索引分配页。当表或索引增长到 8 页时,将变成使用统一区进行后续分配。如果对现有表创建索引,并且该表包含的行足以在索引中生成 8 页,则对该索引的所有分配都使用统一区进行。"

混合区如果有8个对象,每个对象一个页,如果其中一个对象的容量超过一个页了(小于8的时候),接下来怎么分配表空间?如果其中一个对象的容量超过8页了,分配到统一区,原来页内容是不是会被垃圾收集器自动回收了?

楼主提供点资料吧,不懂得太多了。~~~
:^)
  回复  引用  查看    
#19楼  2008-07-16 10:18 | Ronie Deng      
--引用--------------------------------------------------
懒蜜蜂: @comdeng
<br/>页面标头肯定是96个字节,这个2000和2005都是一样的,每页能存储的是8060个字节,但页尾还有一个行偏移表。不知道兄台看的是什么书,不过我说的这个你可以去微软的MSDN上去查证。
--------------------------------------------------------
看了一下联机丛书,果然是96个,我看的书题目忘了,不过早上还专门对过那本书。看来还不如直接看联机丛书为好。
  回复  引用  查看    
#20楼 [楼主] 2008-07-16 10:35 | 懒蜜蜂      
@zzzzz
不懂,能不能详细点
  回复  引用  查看    
#21楼  2008-07-16 10:38 | 毁于随      
楼主有没有试试不建聚集索引会不会也会需要4个小时?我是没有办法做测试的,笔记本硬盘空间剩余不足2个G.....
  回复  引用  查看    
#22楼 [楼主] 2008-07-16 10:59 | 懒蜜蜂      

--引用--------------------------------------------------
朝晖的.net: 楼主:
<br/>
<br/>&quot;混合区,最多可由八个对象共享。区中八页的每页可由不同的对象所有。通常从混合区向新表或索引分配页。当表或索引增长到 8 页时,将变成使用统一区进行后续分配。如果对现有表创建索引,并且该表包含的行足以在索引中生成 8 页,则对该索引的所有分配都使用统一区进行。&quot;
<br/>
<br/>混合区如果有8个对象,每个对象一个页,如果其中一个对象的容量超过一个页了(小于8的时候),接下来怎么分配表空间?如果其中一个对象的容量超过8页了,分配到统一区,原来页内容是不是会被垃圾收集器自动回收了?
<br/>
<br/>楼主提供点资料吧,不懂得太多了。~~~
<br/>:^)
--------------------------------------------------------
混合区如果有8个对象,每个对象一个页,如果其中一个对象的容量超过一个页了(小于8的时候),接下来怎么分配表空间?--应该还是使用混合区
如果其中一个对象的容量超过8页了,分配到统一区,原来页内容是不是会被垃圾收集器自动回收了?--不会

整个分配过程是这样的:SQL Server对每一个表的前8页是按照每个对象一页的方式分配,在后续就是直接分配一个统一区给该对象了,当然是在计算给对象分配的空间不足8页的情况下的结构。SQL Server不会对以前分配的空间进行重新组织。

关于这方面的资料,我主要使用的是SQL Server的联机帮助、推荐你看看Inside.Microsoft.SQL.Server.2005.The.Storage.Engine,当然我自己还没有来得及看呢,有了心得以后一起交流。

  回复  引用  查看    
#23楼 [楼主] 2008-07-16 11:01 | 懒蜜蜂      
@Ronie Deng
有时候是这样的,现在很多出版社出的书都很垃圾,很多都是在校学生翻译一下就给出了,有时候作者都不懂出书的内容,唉,利益驱动啊。。。
  回复  引用  查看    
#24楼 [楼主] 2008-07-16 11:27 | 懒蜜蜂      
@毁于随
正在准备测试中,一旦有了结果我会放到这上面的,感谢关注
  回复  引用  查看    
#25楼  2008-07-16 11:44 | 朝晖的.net      
@懒蜜蜂

联机丛书很好很强大~~以后还得多看看了~~每次都是现用现查~~唉~懒~
  回复  引用  查看    
#26楼 [楼主] 2008-07-16 12:07 | 懒蜜蜂      
@朝晖的.net
是啊,不过有的讲得不够详细,可以找些其他资料补充:)
  回复  引用  查看    
#27楼  2008-07-16 12:55 | 金色海洋(jyk)      
我是很说 scic硬盘挺郁闷的,那么老贵,速度还不快!

我一直在找那个语句,一直没有找到。本来以为这里会有呢,呵呵。
  回复  引用  查看    
#28楼 [楼主] 2008-07-16 13:11 | 懒蜜蜂      
@金色海洋(jyk)
不过理论上SCSI硬盘应该快啊,不懂的说。

我只知道删除数据时不记log的语句,一直没听说其他插入时也不记日志的语句,如果兄台哪天知道了,别忘了告诉小弟一声,谢谢!
  回复  引用  查看    
#29楼 [楼主] 2008-07-16 13:46 | 懒蜜蜂      
@小龙3
刚看到兄台回复,已经改了,谢谢!
  回复  引用  查看    
#30楼 [楼主] 2008-07-16 14:17 | 懒蜜蜂      
@毁于随
@yuuhhe
刚针对不建聚族索引做了以下模拟测试(由于种种原因原数据已经得不到了),我首先从另外一个表中导出495条数据,然后用insert into erAuditEventTime select * from erAuditEventTime语句复制,使表中记录达到4055040条记录,数据库中的恢复模式均为:Simple.得到如下结果:
1 Alter table+聚集索引 耗时:2:39s 日志为1249M
2 Alter table(去掉primary key cluster选项) 耗时:1:16s 日志为:1135M

3 Insert Into方式 耗时:2:14s 日志为:1135M

结论:索引对插入数据的性能有比较大的影响。记得以前一个DBA曾经跟我讲过,如果一个表的索引所占空间和表记录所占的空间比达到20%时,这个索引就值得推敲,也就是说这个索引是不合适的。
  回复  引用  查看    
#31楼  2008-07-16 15:05 | 毁于随      
上面的结果证明非聚集索引更快?

还有,你最后一句话是什么意思?你从哪里得知索引所占的空间?
  回复  引用  查看    
#32楼 [楼主] 2008-07-16 15:25 | 懒蜜蜂      
@毁于随

非聚集索引不会比聚集索引快,反而应该说是在特定的情况下比聚集索引慢,聚集索引只是根据记录的物理顺序来建立起来的,对于某些排序提高的性能尤其明显。


使用以下语句可以得到空间使用情况:

exec sp_spaceused 表名

例如:

exec sp_spaceused erAuditEventTime
  回复  引用  查看    
#33楼 [楼主] 2008-07-16 15:32 | 懒蜜蜂      
"@毁于随

以下命令可以列出所有表空间的使用情况:

sp_msforeachtable "sp_spaceused '?'
  回复  引用  查看    
#34楼  2008-07-16 16:00 | PerfectDesign      
很明显楼主的fileactor是100%了。不然连个int怎么塞不进去
另外混合区内的对象是不可以做索引重建的
关于每个页头96个字节都有哪些东西,可以查看我以前写的这篇文章:
http://www.cnblogs.com/perfectdesign/archive/2008/01/31/mssqlpageheader.html
  回复  引用  查看    
#35楼 [楼主] 2008-07-16 16:11 | 懒蜜蜂      
@PerfectDesign

很明显楼主的fileactor是100%了。不然连个int怎么塞不进去

---------------------------------------------------

对此,能不能详细讲讲?以及怎样解决了?
  回复  引用  查看    
#36楼  2008-07-16 17:34 | Pelican [未注册用户]
This is an interesting post. It's good on you to dig deeper to understand the issue around your finding.

Before adding a primary key column to a table, for each data page, for example, it can hold 50 records. With the new column, each data page can only hold 48 records. SQL server has to re-arrange these 2 records to another data page or to another data extent.

The above operation takes time. Also you didn't mention what indexes were on original table. If there are 3 indexes, SQL has to update the index table too.

When you do insert to your new table, there is no additional index updating. Also you can review the primary key design and maybe you don't have to use clustered index on a sequential column as it doesn't provide much help on data retrieval.

Normally it is not a good idea to change database recovery mode to "Simple". I agree it might speed up data migration, but at the same time, this might leave your database suject to un-recoverable errors. Once data migtration is done, you need to change the recovery mode back.

If you are interested in physical data distribution or index size, you can use various DBCC commands to find out.

Cheers,

ERIC






  回复  引用    
#37楼  2008-07-16 19:38 | PerfectDesign      
@懒蜜蜂
@Pelican
Eric正好的提到的就是页拆分的问题,以及索引的维护,页拆分是sql非常大的一个性能开销,他不会导致整个表的数据页面重排,但是会引起部分页面IO不连续而导致的读取性能下降。
另外他提到的索引的维护,那么也是一个非常大的开销。开销包括两个方面,一个是索引闩锁,一个是日志,索引eric提到了将模式改为简单模式,这样便于截断日志以维持较小的开销。在堆表更新成B树的时候,需要更新非聚集索引的叶节点,所以开销更是不可想象。

每个页面都有一定的填充比例的,如果没有被完全填满,那么只要能够容纳下至少5个字节,那还是不会产生页拆分,性能也能提升很多。
  回复  引用  查看    
#38楼  2008-07-16 20:17 | Pelican [未注册用户]
Fill Factor

When you build cluster index, you can specify a fill factor, say 80%. If the table is empty (this has little impact), when data is added later on, the data page will be full and it will fill up or close to 100%. At regular basis, you might rebuild index again to spread data distribution.

There is no direct relationship between fill factor and data density. If you specify fill factor 50%, the data page will be full eventually.

In the example given out above, the primary key is sequentially numbered and cluster indexed, so fill fator has not much impact.

Another point here: the modified table only has 20M records, so you can make another working table. What do you do if there is no enough space to create a large working table?


ERIC






  回复  引用    
#39楼 [楼主] 2008-07-17 10:06 | 懒蜜蜂      
@Pelican
@PerfectDesign

非常感谢二位非常有价值的信息,感觉自己要学的东西真是太多了,惭愧啊...以后要更深入的学习这方面的知识了,如果要有什么问题还要向二位请教
  回复  引用  查看    

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-07-16 13:46 编辑过
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: