zoukankan      html  css  js  c++  java
  • 索引调优 第三篇:索引统计

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

    一,统计索引的使用次数

    用户向SQL Server提交查询请求,在查询请求生成的执行计划中,每一个单独的索引操作(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_updates
    from 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_id
    where 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 函数统计索引的物理存储,返回索引或堆的size和碎片信息,对于一个索引,每个分区中的B-Tree的每一个level都会返回一行。

    对于一个heap,每个分区的每个分配单元(allocation unit )都会返回一行:

    • 每个分区中的每个IN_ROW_DATA  分配单元返回一行;
    • 对于LOB(large object)数据,每个分区中的每个LOB_DATA 分配单元都会返回一行;
    • 如果表中存在row-overflow数据,每一个分区中的每个ROW_OVERFLOW_DATA 分配单元都会返回一行。

    注意:LOB数据是指字段为text, ntext, image, varchar(max), nvarchar(max), varbinary(max), and xml的数据。

    通过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空间的利用率越高;

    其他重要的字段:

    • alloc_unit_type_desc:分配单元
    • index_depth:索引级别的深度,1 = Heap, or LOB_DATA or ROW_OVERFLOW_DATA allocation unit.
    • index_level:索引的当前级别,0 表示叶级(leaf level)
    • forwarded_record_count:堆中指向其他位置的记录数量,该记录具有前向指针(forward pointer),意味着该记录存储的位置是在forward pointer。
    • ghost_record_count:幽灵记录(ghost record)的数量,ghost record是指分配单元中被标记为删除,正在等待 ghost cleanup task来清除的记录。
    • record_count:记录的数量
    • avg_record_size_in_bytes:每个记录平均的字节数量

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

    三,底层操作的计数

    使用 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.

    1,分析索引的访问模式

    在索引的级别上统计不同操作的次数:

    • leaf_insert_count 、 nonleaf_insert_count
    • leaf_delete_count 、 nonleaf_delete_count 
    • leaf_update_count 、 nonleaf_update_count 
    • leaf_ghost_count 、 nonleaf_ghost_count 
    • leaf_page_merge_count、nonleaf_page_merge_count:在叶级别或叶以上级别的数据页合并的次数

    数据访问的模式:

    • range_scan_count:表扫描或范围操作的次数
    • singleton_lookup_count:从索引或堆中获取单行的次数
    • forwarded_fetch_count:通过forwarding record 获取数据的次数
    • lob_fetch_in_pages:从LOB_DATA 分配单元中获取到的LOB page的数量
    • lob_fetch_in_bytes:LOB数据的字节数量
    • row_overflow_fetch_in_pages:从ROW_OVERFLOW_DATA分配单元中获取到的row-overflow数据页的数量
    • row_overflow_fetch_in_bytes:从ROW_OVERFLOW_DATA分配单元中获取到的row-overflow数据的字节数量
    • column_value_push_off_row_count: 在进行insert或update操作时,把 LOB data 和 row-overflow data 从IN_ROW_DATA分配单元中移除,使得其余的数据能够存储在一个page中,该字段用于统计那些移除LOB和row-overflow的行数。
    • column_value_pull_in_row_count:在进行update操作时,一个字段的空间被释放(free up),使得一个或多个LOB(LOB_DATA分配单元) 或 row-overflow(ROW_OVERFLOW_DATA分配单元)字段回归到IN_ROW_DATA分配单元中。

    2,分析Latch和lock 争用

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

    • PageLatch是指:在访问数据有关的数据页(Data Page或Index Page)时,如果相应的Page已经存在于Buffer Pool中,那么SQL Server先获取buffer的latch,这个Latch就是 PageLatch,然后读取Buffer中的数据。

      PageLatch是施加在Buffer上的Latch, 用来保护:Data page,Index Page, 系统page(PFS,GAM,SGAM,IAM等)的争用访问;在数据更新时,分配新的page,或拆分 索引页(Index Page),会产生PageLatch 等待。

    • PageIOLatch是指:用于把数据从索引或Heap中加载到内存。当数据页从物理文件中的Page中读取到内存时,申请对内存Buffer施加的Latch是PageIOLatch。当数据页不在内存里时,SQL Server 先在内存中预留一个Page,然后从硬盘读取,加载到内存Buffer中,此时,SQL Server申请并获取的latch类型是PAGEIOLATCH,PageIOLatch表示正在进行IO操作。PageIOLatch_EX表示正在将disk中的数据页加载到内存,PageIOLatch_SH表示在加载数据页到内存期间,试图读取内存中的数据页,此时加载数据页的过程没有完成,处于Loading状态。如果经常出现PageIOLatch_SH,表明Loading数据页的时间太长,可能出现IO bottleneck。

    用于统计Latch和Lock征用信息的字段:

    • page_latch_wait_count 、page_latch_wait_in_ms :
    • page_io_latch_wait_count、page_io_latch_wait_in_ms
    • row_lock_count 、row_lock_wait_count、row_lock_wait_in_ms:
    • page_lock_count、page_lock_wait_count、page_lock_wait_in_ms:

    以下脚本用于统计索引底层的存储动作和锁/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_ms
    from sys.dm_db_index_operational_stats(db_id(),object_id('dbo.FactThread'),null,null) as ops
    inner join sys.indexes i 
        on ops.object_id=i.object_id
            and ops.index_id=i.index_id
    order by index_name
    View Code

    3,分析查询结果

    根据计数器的数值,调整数据库,使系统达到最优状态

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

    alter index index_name
    on table_name
    rebuild

    case2,如果PageIOLatch等待较多,说明数据库频繁的执行硬盘IO操作,可能的原因是内存不足,或者数据文件没有分散到多个物理硬盘上

    case2,如果PageLatch等待较多,说明数据库存在IO热点,可以通过增加数据文件ndf,把数据库分散到不同的物理硬盘上,以减少IO热点

    四,查看表上创建的所有索引及其定义

    通过视图 sys.indexes 和 sys.index_columns 查看在基础表创建的所有索引:

    select o.name as table_name
        ,i.index_id
        ,i.name as index_name
        ,i.type_desc as index_type
        ,c.name AS index_column_name
        ,ic.key_ordinal as index_key_ordinal
        ,iif(ic.is_descending_key=1,'desc','asc') as sort_direction
        ,ic.index_column_id
        ,ic.is_included_column
        ,i.fill_factor
        ,i.is_padded
        ,i.has_filter
        ,i.filter_definition
        --,ic.partition_ordinal
    from sys.objects o
    inner join sys.indexes i
        on o.object_id = i.object_id
    inner join sys.index_columns ic
        on i.object_id = ic.object_id 
            and i.index_id = ic.index_id
    inner join sys.columns c
        on o.object_id = c.object_id 
            and ic.column_id = c.column_id
    where o.name = 'table_name'
        --and i.name='index_name'
    order by i.index_id,
        ic.index_column_id
    View Code

    参考文档:

    An in-depth look at Ghost Records in SQL Server

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

  • 相关阅读:
    poj1904 King's Quest
    ACM竞赛须掌握的知识 以及 2个版本的POJ推荐 @ NKOJ discuss 转载的
    poj1466
    C++23中设计模式的factory模式
    poj3667 hotel
    poj1505 Copying Books
    在linux系统中安装VSCode(Visual Studio Code)
    Spring_的jar详细说明
    java开发问题总结4Maven使用问题汇总
    线程同步之信号量(sem_init,sem_post,sem_wait)
  • 原文地址:https://www.cnblogs.com/ljhdo/p/4491911.html
Copyright © 2011-2022 走看看