首页 > 代码库 > 运维脚本:索引统计

运维脚本:索引统计

数据库引擎是高度优化的闭环系统,基于执行计划的反馈,查询优化器在一定程度上自动优化现有的执行计划。查询优化的核心是索引优化,数据库引擎通过计数器统计关于索引操作的数据,统计的信息包括:使用次数、物理存储、底层操作的计数,以及缺失索引等,这些统计数据存储在内存中,是数据库引擎执行情况的真实反馈,高度概括了索引的执行情况,有意识地利用索引的统计信息,有针对性地优化现有的业务逻辑代码,调整查询的执行计划,能够提高数据库的查询性能。

一,统计索引的使用次数

在用户成功提交查询语句时,执行计划中每一个单独的索引操作(Seek,Scan,Lookup或Update)都会被统计到sys.dm_db_index_usage_stats 中,例如,user_updates 计数器统计索引执行Insert,Update或Delete操作的次数,查找计数器(user_seeks, user_scans, user_lookups)统计在索引上执行的seek,scan和lookup操作的次数,如果查找计数器远远小于user_updates 计数器,这说明基础表会执行大量的更新操作,维护索引更新的开销比较大,数据库引擎利用索引提升查询性能的空间有限。 

在计数时,每一个单独的seek、scan、lookup或update操作都被计算为对该索引的一次使用,并使该视图中的相应计数器加1。

索引的Seek,Scan,Lookup和Update的含义是:

  • Seek是Index Seek:通过该索引进行查找的次数
  • Scan是Index Scan:通过该索引执行扫描查找的次数
  • Lookup是Key Lookup:通过该索引查找到数据后,再到源数据表进行键值查找的次数,Key Lookup是非聚集索引特有的,查询性能低下,应避免这种查找方法;
  • Update是Index Update:由于源表数据更新导致索引页更新的次数

Index Seek和Index Scan的区别是:

  • Index Seek是从BTree的根节点开始,向子节点查找,直到叶子节点;
  • Index Scan是在Index的叶子节点上,从左到右,把整个BTree的叶子节点遍历一遍,类似于Table Scan。

如果索引的Seek,Scan,Lookup的计数值较多,那么说明索引被引用的次数多;如果查找计数器数值较小,但是Update数值较多,说明维护Index的开销高于查询带来的性能提升,应该考虑修改索引的结构,或者直接把索引删除。

技术分享
select db_name(us.database_id) as db_name    ,object_schema_name(us.object_id)+.+object_name(us.object_id) as table_name    ,i.name as index_name    ,i.type_desc as index_type_desc    ,us.user_seeks    ,us.user_scans    ,us.user_lookups    ,us.user_updatesfrom sys.dm_db_index_usage_stats us inner join sys.indexes i     on us.object_id=i.object_id and us.index_id=i.index_idwhere us.database_id=db_id()    --us.database_id=db_id(‘database_name‘)    --and us.object_id=object_id(‘schema_name.table_name‘)order by us.user_seeks desc
View Code

二,统计索引的物理存储

使用 sys.dm_db_index_physical_stats 函数统计索引的物理存储,例如,碎片的百分比,数据存储的集中和分散程度,以及page空间的利用率等:

  • avg_fragmentation_in_percent:索引外部碎片的百分比,值越大,说明索引的逻辑顺序和物理顺序差异越大,查找性能越低;
  • fragment_count:分段的数量,表示索引数据的集中/分散程度;
  • avg_fragment_size_in_pages:分段的大小
  • avg_page_space_used_in_percent:索引内部碎片的百分比,值越大,说明page空间的利用率越高;

请阅读《索引碎片的检测和整理》,以了解更多。

三,底层操作的计数

使用 sys.dm_db_index_operational_stats 函数统计底层IO、加锁(Locking)、Latch和数据访问模式的计数,通过这些数据,用户能够追踪到查询请求必须等待多长时间才能完成数据的读写、标识索引是否存在IO热点。

在统计索引的底层操作之前,先了解跟数据的物理存储相关的术语:

  • 幽灵数据(ghost)是指:在索引的叶子节点中,数据行被标记为删除,但是还没有从索引结构中物理删除,幽灵数据只存在于索引的叶子节点中,幽灵数据由后台进程定期执行物理删除。
  • 转发数据(forwarding):需要两次IO操作才能获取到指定的数据,转发操作只发生于堆表(Heap)中;当数据行被更新,导致行的Size增大,以致于该行无法存储在当前的page中,为了避免相关索引的更新,数据库引擎会把该数据行转存到一个新的Page中,并在新旧 Page中分别添加一个Pointer:在原Page中,Pointer指向新Page,该Pointer称作Forwarder Pointer;在新page中,Pointer指向原Page,称作Back Pointer。在读取数据时,数据库引擎首先从Forwarder Pointer中读取数据存储的指针,然后,根据指针到相应的地址空间中读取真正的数据。
  • 获取(Fetch)数据:用于从LOB或Row_Overflow的分配单元(Allocation Unit)中取回(Retrive)数据,大字段数据存储在特定的LOB或Row_Overflow类型的数据页中。
  • 剥离(Push Off)数据列:用于统计数据库引擎把LOB或Row-Overflow数据从原有的In-Row 数据页剥离的次数。在执行Insert或Update操作之后,数据行的Size增长,不能存储在当前的Page中,必须把大数据字段的数据从原来的数据行中分离,存储在指定的分配单元中,这个过程就是数据列的剥离。
  • 拉回(Pull In)数据行:是Push Off的逆过程,用于统计数据库引擎把数据从LOB或Row-Overflow数据页拉入到In-Row数据页的次数,拉入数据行一般发生在更新数据之后,数据行的Size减小,数据行在释放存储空间之后,能够存储在In-Row Page中,数据引擎把数据从LOB或Row-Overflow数据页拉入到In-Row数据页,这个过程是数据列的拉回。

This (pulled in-row) occurs when an update operation frees up space in a record and provides an opportunity to pull in one or more off-row values from the LOB_DATA or ROW_OVERFLOW_DATA allocation units to the IN_ROW_DATA allocation unit.

以下脚本用于统计索引底层的存储动作和锁/Latch的争用:

技术分享
select db_name(ops.database_id) as db_name    ,object_schema_name(ops.object_id)+.+object_name(ops.object_id) as table_name    ,i.name as index_name    ,ops.partition_number    ,ops.leaf_insert_count    ,ops.leaf_delete_count    ,ops.leaf_update_count    ,ops.leaf_ghost_count    ,ops.nonleaf_insert_count    ,ops.nonleaf_delete_count    ,ops.nonleaf_update_count    ,ops.range_scan_count    ,ops.singleton_lookup_count    ,ops.forwarded_fetch_count    ,iif(ops.row_lock_wait_count=0,0,ops.row_lock_wait_in_ms/ops.row_lock_wait_count) as avg_row_lock_wait_ms    ,iif(ops.page_lock_wait_count=0,0,ops.page_lock_wait_in_ms/ops.page_lock_wait_count) as avg_page_lock_wait_ms    ,iif(ops.page_latch_wait_count=0,0,ops.page_latch_wait_in_ms/ops.page_latch_wait_count) as avg_page_latch_wait_ms    ,iif(ops.page_io_latch_wait_count=0,0,ops.page_io_latch_wait_in_ms/ops.page_io_latch_wait_count) as avg_page_io_latch_wait_msfrom sys.dm_db_index_operational_stats(db_id(),object_id(dbo.FactThread),null,null) as opsinner join sys.indexes i     on ops.object_id=i.object_id        and ops.index_id=i.index_idorder by index_name
View Code

该函数统计的Latch征用数据主要分为PageLatch和PageIOLatch,其区别是:

  • PageLatch是指:施加在文件的数据页(File Page)上Latch,用于标识数据库引擎正在物理文件上执行数据的读写;
  • PageIOLatch是指:产生硬盘IO,用于把数据从索引或Heap中加载到内存。当数据页从物理文件中的Page中读取到内存时,需要对内存Buffer施加的Latch是PageIOLatch。

分析查询结果:如果发现字段leaf_ghost_count的数值特别大,说明索引中存储很多幽灵数据,可以通过重建索引(Rebuild)清理幽灵数据行:

alter index index_nameon table_namerebuild

四,缺失索引

查询优化器(Query Optimizer)在执行查询时,如果检测到执行计划缺失索引,会把缺失索引的相关信息存储在缓存中,通过  sys.dm_db_missing_index_details 可以检测查询优化器建议创建的缺失索引。

该视图返回的缺失索引的索引键及包含列信息,在索引列的顺序上,相等列(equality)应该排在不等列(inequality)之前,包含列(Included)应该添加到INCLUDE子句中,但是,该视图不会标识出相等列(equality)的排列顺序,需要根据查询语句和选择性来设置,索引键的第一列至关重要。

技术分享
select mid.index_handle    ,db_name(mid.database_id) as db_name    ,mid.object_id    ,object_name(mid.object_id) as object_name    ,mid.equality_columns    ,mid.included_columns    ,mid.included_columns    ,mid.statement    ,mic.column_id    ,mic.column_name    ,column_usagefrom sys.dm_db_missing_index_details as midcross apply sys.dm_db_missing_index_columns(mid.index_handle) as micorder by mid.object_id
View Code

 

参考文档:

An in-depth look at Ghost Records in SQL Server

Index Related Dynamic Management Views and Functions (Transact-SQL)

运维脚本:索引统计