SQL Server 从 【索引梳理】 到【查询性能调优】
时 间:2015-06-18 09:27:33
作 者:易勋(转) ID:35404 城市:上海
摘 要:首先,笔者将从实际操作入手,介绍DBA在日常运维过程中,如何利用SQL语句去对线上业务数据库的索引进行梳理。
然后,开始认识索引,介绍索引的作用,索引的分类,索引的结构。
接着,介绍索引的选择,索引的创建,利用索引进行查询性能的优化。
最后,进行总结。
正 文:
----------------------------------------------------------------------------------------------
-------------------------------------- 笔者的初衷 -------------------------------------
----------------------------------------------------------------------------------------------
首先,笔者将从实际操作入手,介绍DBA在日常运维过程中,如何利用SQL语句去对线上业务数据库的索引进行梳理。
然后,开始认识索引,介绍索引的作用,索引的分类,索引的结构。
接着,介绍索引的选择,索引的创建,利用索引进行查询性能的优化。
最后,进行总结。
----------------------------------------------------------------------------------------------
------------------------------------- 华丽的分割线 ---------------------------------------
先给出几个索引梳理的常用脚本。
查询出所有的索引及主键信息
-- 索引及主键信息 Select TableId=O.[object_id], TableName=O.Name, IndexId=ISNULL(KC.[object_id],IDX.index_id), IndexName=IDX.Name, IndexType=ISNULL(KC.type_desc,'Index'), Index_Column_id=IDXC.index_column_id, ColumnID=C.Column_id, ColumnName=C.Name, Sort=CASE INDEXKEY_PROPERTY(IDXC.[object_id],IDXC.index_id,IDXC.index_column_id,'IsDescending') WHEN 1 THEN 'DESC' WHEN 0 THEN 'ASC' ELSE '' END, PrimaryKey=CASE WHEN IDX.is_primary_key=1 THEN N'√'ELSE N'' END, [UQIQUE]=CASE WHEN IDX.is_unique=1 THEN N'√'ELSE N'' END, Ignore_dup_key=CASE WHEN IDX.ignore_dup_key=1 THEN N'√'ELSE N'' END, Disabled=CASE WHEN IDX.is_disabled=1 THEN N'√'ELSE N'' END, Fill_factor=IDX.fill_factor, Padded=CASE WHEN IDX.is_padded=1 THEN N'√'ELSE N'' END FROM sys.indexes IDX INNER JOIN sys.index_columns IDXC ON IDX.[object_id]=IDXC.[object_id] AND IDX.index_id=IDXC.index_id LEFT JOIN sys.key_constraints KC ON IDX.[object_id]=KC.[parent_object_id] AND IDX.index_id=KC.unique_index_id INNER JOIN sys.objects O ON O.[object_id]=IDX.[object_id] INNER JOIN sys.columns C ON O.[object_id]=C.[object_id] AND O.type='U' AND O.is_ms_shipped=0 AND IDXC.Column_id=C.Column_id -- INNER JOIN -- 对于一个列包含多个索引的情况,只显示第1个索引信息 -- ( -- Select [object_id], Column_id, index_id=MIN(index_id) -- FROM sys.index_columns -- GROUP BY [object_id], Column_id -- ) IDXCUQ -- ON IDXC.[object_id]=IDXCUQ.[object_id] -- AND IDXC.Column_id=IDXCUQ.Column_id
Select OBJECT_NAME(i.object_id) AS object_name, o.type_desc AS ojbect_type, i.name AS index_name, i.type_desc AS index_type FROM sys.indexes i INNER JOIN sys.objects o ON i.object_id = o.object_id Where o.type_desc NOT IN ('INTERNAL_TABLE','SYSTEM_TABLE') AND i.type_desc <> 'HEAP' AND i.index_id NOT IN (Select s.index_id FROM sys.dm_db_index_usage_stats s Where s.object_id=i.object_id AND i.index_id=s.index_id AND database_id = DB_ID('QWorld') ) orDER BY OBJECT_NAME(i.object_id), o.type_desc
Select 'ObjectName', 'OjbectType', 'IndexId', 'UserSeeks', 'UserScans', 'UserLookups', 'UserUpdates', 'LastUserSeek', 'LastUserScan', 'LastUserLookup', 'LastUserUpdate', 'SystemSeeks', 'SystemScans', 'SystemLookups', 'SystemUpdates', 'LastSystemSeek', 'LastSystemScan', 'LastSystemLookup', 'LastSystemUpdate' Select object_name(d.object_id) AS object_name, o.type_desc AS ojbect_type, index_id, user_seeks, user_scans, user_lookups, user_updates, last_user_seek, last_user_scan, last_user_lookup, last_user_update, system_seeks, system_scans, system_lookups, system_updates, last_system_seek, last_system_scan, last_system_lookup, last_system_update FROM sys.dm_db_index_usage_stats d INNER JOIN sys.objects o ON d.object_id = o.object_id Where d.database_id = db_id('QWorld') AND o.type_desc <> 'SYSTEM_TABLE' orDER BY object_name(d.object_id)
Select N'索引名', N'表名', N'架构名', N'索引类型', N'索引密度', N'索引碎片', N'页数' Select i.name AS N'索引名', o.name AS N'表名', s.name AS N'架构名', f.index_type_desc AS N'索引类型', f.avg_page_space_used_in_percent AS N'索引密度', f.avg_fragmentation_in_percent AS N'索引碎片', f.page_count AS N'页数' FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'SAMPLED') f INNER JOIN sys.objects o ON o.object_id = f.object_id INNER JOIN sys.schemas s ON o.schema_id = s.schema_id INNER JOIN sys.indexes i ON f.object_id = i.object_id AND f.index_id = i.index_id Where page_count > 50 AND f.index_id > 0 orDER BY --o.name, i.index_id f.avg_fragmentation_in_percent DESC;
DECLARE @t table( tbl_name nvarchar(256), index_id int, index_name nvarchar(256), statistics_update_date datetime ); Insert @t EXEC sp_MSforeachtable ' Select OBJECT_NAME(object_id), index_id, name AS index_name, STATS_DATE(object_id, index_id) AS statistics_update_date FROM sys.indexes Where object_id = OBJECT_ID(''?'');'; Select N'表名', N'索引ID', N'索引名', N'统计信息更新时间'; Select tbl_name AS N'表名', index_id AS N'索引ID', index_name AS N'索引名', statistics_update_date AS N'统计信息更新时间' FROM @t orDER BY tbl_name;
查询SP执行效率和可能未建立索引表
Select d.name, OBJECT_NAME(object_id, database_id) 'proc name',
a.cached_time, a.last_execution_time,
a.execution_count,
a.execution_count/(DATEDIFF(ss,cached_time,last_execution_time)) as 'execution_count/s',
total_worker_time,
a.total_worker_time /a.execution_count avg_worker_time,
max_worker_time,
total_physical_reads,
a.total_physical_reads /a.execution_count avg_physical_reads,
max_physical_reads,
total_logical_reads,
a.total_logical_reads/ a.execution_count avg_logical_reads,
max_logical_reads,
a.total_logical_writes,
a.total_logical_writes/ a.execution_count avg_logical_writes,
max_logical_writes,
a.total_elapsed_time,
a.total_elapsed_time / a.execution_count avg_elapsed_time,
a.max_elapsed_time,
b.text
FROM sys.dm_exec_procedure_stats AS a
CROSS APPLY sys.dm_exec_sql_text(a.sql_handle) b
CROSS APPLY sys.dm_exec_query_plan(a.plan_handle) c
LEFT JOIN master.sys.sysdatabases d on a.database_id=d.dbid
Where DATEDIFF(ss,cached_time,last_execution_time)>3600
and d.name='Lin2World'
orDER BY (a.total_worker_time) DESC
--select o.name,d.*
-- , s.avg_total_user_cost
-- , s.avg_user_impact
-- , s.last_user_seek
-- ,s.unique_compiles
--from sys.dm_db_missing_index_group_stats s
-- ,sys.dm_db_missing_index_groups g
-- ,sys.dm_db_missing_index_details d
-- ,sys.sysobjects o
-- ,sys.sysdatabases db
--where s.group_handle = g.index_group_handle
--and d.index_handle = g.index_handle
--and d.object_id=o.id
--and d.database_id=db.dbid
--and db.name='lin2world'
--order by s.avg_user_impact desc
--go
Select N'对象名', N'对象ID', N'索引名', N'索引ID', N'seeks', N'scans', N'lookups', N'updates'; Select OBJECT_NAME(s.object_id) AS N'对象名', s.object_id AS N'对象ID', i.name AS N'索引名', i.index_id AS N'索引ID', user_seeks AS N'seeks', user_scans AS N'scans', user_lookups AS N'lookups', user_updates AS N'updates' FROM sys.dm_db_index_usage_stats s, sys.indexes i Where database_id = DB_ID() AND OBJECTPROPERTY(s.object_id,'isusertable') = 1 AND i.object_id = s.object_id AND i.index_id = s.index_id orDER BY (user_seeks + user_scans + user_lookups + user_updates) ASC;
索引整理和重建、统计信息更新
-- replace DBName with your dbname -- DBName 替换为 yourname use master go if OBJECTPROPERTY(OBJECT_ID('sp_indexdefragmentation'), 'IsProcedure') = 1 drop procedure sp_indexdefragmentation go Create PROC [dbo].[sp_indexdefragmentation] @p_dbname SYSNAME = 'NL' AS BEGIN SET NOCOUNT ON IF @p_dbname = 'NL' SET @p_dbname = DB_NAME(); DECLARE @db_name SYSNAME, @tab_name SYSNAME, @ind_name VARCHAR(500), @schema_name SYSNAME, @frag FLOAT, @pages INT, @min_id INT, @max_id INT SET @db_name=@p_dbname -------------------------------------------------------------------------------------------------------------------------------------- --inserting the Fragmentation details -------------------------------------------------------------------------------------------------------------------------------------- Create TABLE #tempfrag ( id INT IDENTITY, table_name SYSNAME, index_name VARCHAR(500), frag FLOAT, pages INT, schema_name SYSNAME ) EXEC ('USE ['+@db_name+']; Insert INTO #tempfrag (table_name,index_name,frag,pages,schema_name) select OBJECT_NAME(f.object_id) as obj , i.name as ind , f.avg_fragmentation_in_percent , f.page_count , TABLE_SCHEMA from sys.dm_db_index_physical_stats (DB_ID(), null, null, null, null) as f inner join sys.indexes as i on f.object_id = i.object_id and i.index_id = f.index_id inner join INFORMATION_SCHEMA.TABLES as s on s.TABLE_NAME = OBJECT_NAME(f.object_id) and f.database_id = DB_ID() and OBJECTPROPERTY(i.object_id, ''ISSYSTEMTABLE'') = 0 Where f.index_id > 0 and f.index_id <= 10000' ) Select @min_id=MIN(ID)FROM #tempfrag Select @max_id=MAX(ID)FROM #tempfrag -- TRUNCATE TABLE msdb.dbo.dba_defrag_maintenance_history WHILE (@min_id<=@max_id) BEGIN Select @tab_name=table_name, @schema_name=schema_name, @ind_name=index_name , @frag=frag , @pages=pages FROM #tempfrag Where id = @min_id -------------------------------------------------------------------------------------------------------------------------------------- --Check the fragmentation greater than 30% and pages greater than 1000 then rebuild -------------------------------------------------------------------------------------------------------------------------------------- IF (@ind_name IS NOT NULL) BEGIN IF (@frag>=30 AND @pages>1000) BEGIN EXEC ('USE ['+@db_name+'];Alter INDEX ['+@ind_name+'] ON ['+@schema_name+'].['+@tab_name +'] REBUILD ') --Insert INTO msdb.dbo.dba_defrag_maintenance_history --VALUES (@db_name,@tab_name,@ind_name,@frag,@pages,'REBUILD',GETDATE()) PRINT 'USE ['+@db_name+'];Alter INDEX ['+@ind_name+'] ON ['+@schema_name+'].['+@tab_name +'] REBUILD ' END -------------------------------------------------------------------------------------------------------------------------------------- --Check the fragmentation between 15% to 29% and pages greater than 1000 then reorganize -------------------------------------------------------------------------------------------------------------------------------------- ELSE IF((@frag BETWEEN 15 AND 29) AND @pages>1000 ) BEGIN BEGIN TRY EXEC ('USE ['+@db_name+'];Alter INDEX ['+@ind_name+'] ON ['+@schema_name+'].['+@tab_name +'] REORGANIZE ') EXEC ('USE ['+@db_name+'];Update STATISTICS ['+@schema_name+'].['+@tab_name+'] (['+@ind_name+']) ' ) --Insert INTO msdb.dbo.dba_defrag_maintenance_history --VALUES (@db_name,@tab_name,@ind_name,@frag,@pages,'REORGANIZE & UpdateSTATS',GETDATE()) PRINT 'USE ['+@db_name+'];Alter INDEX ['+@ind_name+'] ON ['+@schema_name+'].['+@tab_name +'] REORGANIZE ' PRINT 'USE ['+@db_name+'];Update STATISTICS ['+@schema_name+'].['+@tab_name+'] (['+@ind_name+']) ' END TRY BEGIN CATCH -------------------------------------------------------------------------------------------------------------------------------------- --Check the fragmentation between 15% to 29% and pages greater than 1000 and page level --lock disabled then rebuild -------------------------------------------------------------------------------------------------------------------------------------- IF ERROR_NUMBER()=2552 EXEC ('USE ['+@db_name+'];Alter INDEX ['+@ind_name+'] ON ['+@schema_name+'].['+@tab_name +'] REBUILD ') --Insert INTO msdb.dbo.dba_defrag_maintenance_history --VALUES (@db_name,@tab_name,@ind_name,@frag,@pages,'PLLD_REBUILD',GETDATE()) PRINT 'USE ['+@db_name+'];Alter INDEX ['+@ind_name+'] ON ['+@schema_name+'].['+@tab_name +'] REBUILD ' END CATCH END -------------------------------------------------------------------------------------------------------------------------------------- --Update the statistics for all indexes if the first three conditions is false -------------------------------------------------------------------------------------------------------------------------------------- ELSE BEGIN EXEC ('USE ['+@db_name+'];Update STATISTICS ['+@schema_name+'].['+@tab_name+'] (['+@ind_name+']) ' ) --Insert INTO msdb.dbo.dba_defrag_maintenance_history --VALUES (@db_name,@tab_name,@ind_name,@frag,@pages,'UpdateSTATS',GETDATE()) PRINT 'USE ['+@db_name+'];Update STATISTICS ['+@schema_name+'].['+@tab_name+'] (['+@ind_name+']) ' END END ELSE BEGIN -------------------------------------------------------------------------------------------------------------------------------------- --Update the statistics for all tables if the first three conditions is false -------------------------------------------------------------------------------------------------------------------------------------- EXEC ('USE ['+@db_name+'];Update STATISTICS ['+@schema_name+'].['+@tab_name+']') --Insert INTO msdb.dbo.dba_defrag_maintenance_history --VALUES (@db_name,@tab_name,'HEAP',@frag,@pages,'UpdateSTATS',GETDATE()) PRINT 'USE ['+@db_name+'];Update STATISTICS ['+@schema_name+'].['+@tab_name+']'; END SET @min_id=@min_id+1 END Drop TABLE #tempfrag --Insert INTO master.dbo.dba_defrag_maintenance_history --Select * FROM msdb.dbo.dba_defrag_maintenance_history END go --execute use master exec dbo.[sp_indexdefragmentation] DBName go实际操作建议:
相信,这批脚本对大家的工作会有帮助。
大家可以根据顺序,先列出所有的索引和主键情况,根据后面列出的无效索引、缺失索引、索引效率统计等等和之前的完整信息作对比,逐个分析。根据实际的应用,再去权衡是否需要该索引,该索引是否建得得当,是否需要优化。当然逐个工作可以定期去做。接下来,就要做好自动化维护索引和统计信息的工作。将索引的重建和整理、统计信息的更新做到自动化定期执行。所要注意的是,要考量维护窗口的线上压力,对主键、聚集索引、非聚集索引做不同的策略。最后要注意的是对于在线索引重建,若存在LOB字段,直到SQL SERVER 2012版本才真正实现了。
重建索引是为了减少数据碎片。数据碎片会导致SQL Server进行不必要的数据读,降低SQL Server的性能。重建索引也会同时更新列统计,而如果查询所使用的列缺少或遗漏统计信息,这可能导致SQL Server内部的优化器选择比预期效率低的查询计划。
如果您重建了某张表上的聚集索引,该表上的非聚集索引也同时会被更新。
要更新索引,您可以使用Maintenance Wizard(相关内容您可以参考http://msdn.microsoft.com/en-us/library/ms180074.aspx),或在SQL Server代理(Agent)中运行如下的自定义代码来更新某个数据库中所有表上的索引:
您可以根据您的需求修改DBREINDEX的参数。
需要注意的是,重建非聚集索引时该表会暂时加上共享锁,对用户不可进行Select以外的操作;重建聚集索引时该表会暂时加上排外锁,不允许任何用户访问。因此需要制定好计划来预防可能的访问问题。
REBUILD有一个fill factor参数,如果fill factor设置为100%,这意味着每一个索引页都是完全满的,如果fill factor设置为50%意味着每个索引页都是半满的。对于fill factor 100%,每次新插入或更新一个记录,由于当前页没有空间可用,可能有分页情况产生。过多的分页会降低SQL Server的性能。下面具体举个例子:
假设您在一张表上建立了一个使用默认fill factor的新索引。当SQL Server创建索引时,它会把索引放置在连续的物理页上,以使数据顺序地被读,I/O访问最优化。但当表因Insert,Update,Delete等操作增长改变时,分页发生,SQL Server在磁盘的其他地方分配新的页,导致新的页与原物理页不连续,增加了随机I/O,访问索引页变慢。
那么fill factor的合适值应该为多少?这取决于表的读/写比:
低更新表(读/写比:100比1):100% fill factor
高更新表(写超过读):50%-70% fill factor
居中:80%-90% fill factor
过低的fill factor会增加页的数量,也会导致更多的页需要被移至缓存,缓存中有用的数据减少。默认的fill factor为0(即100% fill factor),通常这不是个好的选择,特别是对于聚集索引。
如果您无法判断设置什么fill factor,您首先需要确定磁盘的读/写比.方法就是使用如下两个计数器:
Physical Disk Object: % Disk Read Time 和 Physical Disk Object: % Write Time。另外一个可能有用的计数器就是:SQL Server Access Methods: Pages Splits/Sec。这个计数器测量SQL Server内每秒分页的次数。如果该数值过高,您需要降低fill factor防止新的分页。
如果您想确认您的索引因分页产生的碎片程度,您可以运行DBCC SHOWCONTIG命令。如果看特定表和特定索引,您可以运行如下代码:
结果集中最重要的参数是Scan Density,越接近100%越好。如果Scan Density小于75%,那么您可能需要重建表中的索引。
对于小于100数据页,重建索引并不会有明显的性能改善。这是因为物理硬件缓存,SQL Server缓存和SQL Server预读机制隐藏了碎片的负面作用。但对于非常大的表,重建索引会使它受益匪浅,因为涉及大量磁盘I/O操作。
Rebuilding an index drops and re-creates the index. This removes fragmentation, reclaims disk space by compacting the pages based on the specified or existing fill factor setting, and reorders the index rows in contiguous pages. When ALL is specified, all indexes on the table are dropped and rebuilt in a single transaction.
重新生成索引将会删除并重新创建索引。 这将根据指定的或现有的填充因子设置压缩页来删除碎片、回收磁盘空间,然后对连续页中的索引行重新排序。 如果指定 ALL,将删除表中的所有索引,然后在单个事务中重新生成。
Reorganizing an index uses minimal system resources. It defragments the leaf level of clustered and nonclustered indexes on tables and views by physically reordering the leaf-level pages to match the logical, left to right, order of the leaf nodes. Reorganizing also compacts the index pages. Compaction is based on the existing fill factor value.
重新组织索引使用最少系统资源重新组织索引。 通过对叶级页以物理方式重新排序,使之与叶节点的从左到右的逻辑顺序相匹配,进而对表和视图中的聚集索引和非聚集索引的叶级进行碎片整理。 重新组织还会压缩索引页。 压缩基于现有的填充因子值。
Rebuilding an index can be executed online or offline. Reorganizing an index is always executed online. To achieve availability similar to the reorganize option, you should rebuild indexes online.
rebulid index既可以在online又可以在offline下执行,而reorganize index只能在online下执行的。
既然rebuild index既可以是online模式,也可以是offline模式,那么两者有什么区别呢。这个我们可以参考stackoverflow上面的一篇文章:What is the difference between OFFLINE and ONLINE index rebuild in SQL Server?
在这里我还是简要总结一下:
online模式下
rebuild index会复制旧索引来新建索引,此时旧的索引依然可以被读取和修改,但是所以在旧索引上的修改都会同步更新到新索引下。中间会有一些冲突解决机制,具体参考Online Index Operations 里面的Build Phase这一章节。然后在rebuild这个过程完整的时候,会对table上锁一段时间,在这段时间里会用新索引来替换旧索引,当这个过程完成以后再释放table上面的锁。如果索引列包含 LOB对象的话,在SQL Server 2005/2008/R2中rebuild index online会失败。在sql server 2012中,即使索引列包含LOB对象,也可以rebuild index online了,可以参考 Online Index Operations for indexes containing LOB columns.
offline模式下
rebuilde index会对table上锁,所有对这个table的读写操作都会被阻塞,在这期间新索引根据旧索引来创建,其实就是一个复制的过程,但是新索引没有碎片,最后使用新索引替换旧索引。当rebuild整个过程完成以后,table上面的锁才会被释放。
Access软件网QQ交流群 (群号:54525238) Access源码网店
常见问答:
技术分类:
源码示例
- 【源码QQ群号19834647...(12.17)
- Access对子窗体数据进行批...(10.30)
- 最精简的组合框行来源数据快速输...(10.25)
- Access仿平台的多值选择器...(10.24)
- 【Access日期区间段查询】...(10.22)
- 【Access源码示例】VBA...(10.12)
- Access累乘示例,Acce...(10.09)
- 数值8.88,把整数8去掉,转...(10.08)
- 【Access自定义函数】一个...(09.30)
- 【Access选项卡示例】Ac...(09.09)
学习心得
最新文章
- Access系统自带的日期选择器不...(11.08)
- 分享一下Access工程中的acw...(11.07)
- Access快速开发平台--让有权...(11.04)
- Access快速开发平台--审批选...(11.01)
- ACCESS两张表先各自排序,然后...(10.31)
- Access对子窗体数据进行批量+...(10.30)
- SqlServer中如何用SQL命...(10.29)
- Access报表中的分组功能用代码...(10.28)
- 用Access计算库存结余的一个方...(10.26)
- 最精简的组合框行来源数据快速输入(...(10.25)