• SQLServer聚集索引键的选择-性能角度谈


    1. 基础资料

    1.1页

    SQL Server 中数据存储的基本单位是页。编号0-n,所有IO 操作在页级执行,不论是磁盘IO或是缓存池IO(Buffer Pool).
    下图是页的结构

    注:
    1 每个页固定大小是8K,其中标头固定占96字节,页尾部行偏移信息占36字节,剩余空间8060字节是有效空间,存放数据以及开销。
    2 如果数据行中包含数据类型为varchar、nvarchar、varbinary 和 sql_variant的列,导致该行超出8060字节,则该列会被移到ROW_OVERFLOW_DATA(行溢出数据) 分配单元中的页,而在原位置生成一个24字节的指针,此后该列长度减少,又会被移到原始数据页(IN_ROW_DATA,行内数据)。
    另,varchar(max)、nvarchar(max)、varbinary(max)、ntext、text、image 或 xml列由表选项控制,直接存放在LOB类型页(LOB_DATA)或者存储16字节指针在原页实际数据存放LOB类型页。

     

    开始验证,环境sqlserver 2008 r2,版本号10.50.1600,固定长度列的情况不再演示,仅仅演示下变长,且溢出的情况

    1 行溢出列

    CREATE TABLE test1(ID INT NOT NULL,col VARCHAR(8000),col1 CHAR(8000));
    INSERT INTO test1
    SELECT 1,REPLICATE('a',2000),'b'—col1是定长数据,为IN_ROW_DATA,col是变长,且此时该行数据长度超过8K页范围
    --查看页数

    --查看页ID以及实际数据情况

    注:PageType 1 数据页 2 索引页 3 LOB页 10 IAM页(Index Allocation Map,每个表或者索引均有此类页,用来表示表或索引使用的区(extent,每8个连续页组成一个区)信息)
    这几种页类型是数据页类型,其他管理类页此处暂不讨论。
    页头部信息

    数据信息
    Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 8039
    Memory Dump @0x000000000EDFC060
    0000000000000000: 3000481f 01000000 62202020 20202020 †0.H.....b
    ------此处略去n行
    0000000000001F50: 00000001 0000009f 150000d0 0700004f †...............O (此处的变长数据存储区,因长度超出范围,存储24字节指针,下面可见)
    0000000000001F60: 00000001 000000††††††††††††††††††††††.......
    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4
    ID= 1
    col = [BLOB Inline Root] Slot 0 Column 2 Offset 0x1f4f Length 24 Length (physical) 24
    Level = 0 Unused = 0 UpdateSeq = 1
    TimeStamp = 362741760
    Link 0
    Size = 2000 RowId = (1:79:0) (指针指示的该变长列位置,从第一个图亦可看到,页79是行溢出数据)

     

    2

    LOB类型列

    ,text,ntext,image已经被varchar(max),nvarchar(max)取代,不再建议使用,此处以varchar(max)为例

    --查看页数

    注:可以看到,虽然插入的数据为4000个字符,但仍分配了一个LOB页,因为表选项已经开启,值为1
    --查看页ID及实际数据

    --页头略去
    Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 31
    Memory Dump @0x000000000F99A060
    0000000000000000: 30000800 01000000 0200e001 001f8000 †0...............
    0000000000000010: 00d10700 00000072 00000001 000100††††.......r.......
    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4
    ID = 1
    col = [Textpointer] Slot 0 Column 2 Offset 0xf Length 16 Length (physical) 16 (此处是指针,16字节)
    TextTimeStamp = 131137536 RowId = (1:114:1)
    然而,表的这个选项默认是0,即未开启,此时情况同varchar类型的行溢出列

     

    1.2 索引

    以下图是索引的结构

    注:(1) 索引是一个B树结构,即从根节点到每一个叶子节点深度相同

    (2) 每一个层级(根节点,中间节点,叶子节点)的页均被链接在双向链表中

    (3) 不同的层级间,只能通过上级节点访问下一级节点

     

    索引的root_page(根页),first_page(首页)可以通过sys.system_internals_allocation_units查看,详细内容不作介绍

     

    2聚集索引键选择原则

    2.1聚集索引所在的列或列的组合最好是唯一的

    SQLServer操作数据的最小单元是页,所以,索引占用页数越少,读取索引速度越快
    CREATE TABLE test4(ID INT NOT NULL ,col CHAR(200))
    --创建聚集索引
    CREATE CLUSTERED INDEX IXC_test4 ON test4(ID)

    --插入20万条数据,每条重复2

    ;WITH

    L0 AS(SELECT 1 AS c UNION ALL SELECT 1),

    L1 AS(SELECT 1 AS c FROM L0 AS A, L0 AS B),

    L2 AS(SELECT 1 AS c FROM L1 AS A, L1 AS B),

    L3 AS(SELECT 1 AS c FROM L2 AS A, L2 AS B),

    L4 AS(SELECT 1 AS c FROM L3 AS A, L3 AS B),

    L5 AS(SELECT 1 AS c FROM L4 AS A, L4 AS B),

    Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY c) AS n FROM L5)

    INSERT INTO test4

    SELECT n,'a'

    FROM Nums CROSS JOIN (SELECT 1 AS col UNION ALL SELECT 1) t

    WHERE n <= 100000
    --查看页数

    --删除数据,再次插入20万条,不重复
    TRUNCATE TABLE test4
    ;WITH
    L0 AS(SELECT 1 AS c UNION ALL SELECT 1),
    L1 AS(SELECT 1 AS c FROM L0 AS A, L0 AS B),
    L2 AS(SELECT 1 AS c FROM L1 AS A, L1 AS B),
    L3 AS(SELECT 1 AS c FROM L2 AS A, L2 AS B),
    L4 AS(SELECT 1 AS c FROM L3 AS A, L3 AS B),
    L5 AS(SELECT 1 AS c FROM L4 AS A, L4 AS B),
    Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY c) AS n FROM L5)
    INSERT INTO test4
    SELECT n,'a'
    FROM Nums
    WHERE n <= 200000;
    --再次查看

    两次插入数据一样(int 4字节,char 200字节,都是定长),条数一样(200000),占用页数不同,为何?
    --查看聚集索引有重复键值的情况,某个索引页

     

    注:创建聚集索引不带UNIQUE关键字,则SQLServer会添加一个附加列uniquifier,用于区分唯一性,该列占用4字节
    而使用了uniquifier后,对性能产生的影响如下:
        (1)SQL Server必须在插入或者更新时对现在数据进行判断是否和现有的键重复,如果重复,则需要生成uniquifier。
        (2)因为需要对相同值的键添加额外的uniquifier来区分,所以键的大小被额外的增加了。因此无论是叶子节点和非叶子节点,都需要更多的页进行存储。(这就是上述现象出现的原因)
    从而还影响到了非聚集索引,使得非聚集索引的书签列变大,从而非聚集索引也需要更多的页进行存储。(因为非聚集索引要引用聚集索引的键列,下面环节2.3有演示)

     

    以下是数据片段

     

    0000000000000000: 1000d000 1d000000 61202020 20202020 †........a
    --数据略去
    00000000000000D0: 030000†††††††††††††††††††††††††††††††...
    Slot 0 Column 0 Offset 0x0 Length 4 Length (physical) 0—-第一个值不加uniquifier
    UNIQUIFIER = 0
    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4
    ID = 29
    Slot 0 Column 2 Offset 0x8 Length 200 Length (physical) 200
    col = a
    Slot 0 Offset 0x0 Length 0 Length (physical) 0
    KeyHashValue = (48f7cc7c34f7)

     

    0000000000000000: 3000d000 1d000000 61202020 20202020 †0.......a
    --数据略去
    00000000000000D0: 03000001 00db0001 000000†††††††††††††...........
    Slot 1 Column 0 Offset 0xd7 Length 4 Length (physical) 4—-第二个值加uniquifier,因为此时有重复数据了
    UNIQUIFIER = 1
    Slot 1 Column 1 Offset 0x4 Length 4 Length (physical) 4
    ID = 29
    Slot 1 Column 2 Offset 0x8 Length 200 Length (physical) 200
    col = a

     

    注:再次创建一样的表,建立唯一聚集索引 CREATE UNIQUE CLUSTERED INDEX IXC_test5 ON test5(ID)
    结果和上述test4插入唯一数据的情况一样,占用5264个页

     

    2.2使用窄列或窄列组合作为聚集索引列

        这个道理和上面减少页的原理一样,窄列使得键的大小变小。使得聚集索引的非叶子节点减少,而非聚集索引的书签变小,从而叶子节点页变得更少。最终提高了性能。

       

    2.3使用值很少变动的列或列的组合作为聚集索引列

       为表创建聚集索引后,SQL Server按照键查找行。因为在B树中,数据是有序的,所以当聚集索引键发生改变时,不仅仅需要改变值本身,还需要改变这个键所在行的位置(RID,即磁盘上的位置,即上述图示常见的slot),因此有可能使得行从一页移动到另一页。 因此会带来如下问题:
    1. 行从一页移动到另一页,这个操作是需要开销的,不仅如此,这个操作还可能影响到其他行,使得其他行也需要移动位置,由此产生分页。
    2. 行在页之间的移动会产生索引碎片。
    3. 键的改变会影响到非聚集索引,使得非聚集索引的书签列也需要改变。

            

            仅仅演示下非聚集索引引用聚集索引键列的情况,插入数据引起的行移动甚至页拆分暂不演示

            CREATE TABLE test5(ID INT NOT NULL PRIMARY KEY ,ID1 INT NOT NULL )--主键默认是聚集索引,只要创建的时候是该表不存在其他索引,而且不加NONCLUSTERED关键字

    --创建索引

    CREATE INDEX IX_test5 ON test5(ID1)

    --插入数据

    ;WITH

    L0 AS(SELECT 1 AS c UNION ALL SELECT 1),

    L1 AS(SELECT 1 AS c FROM L0 AS A, L0 AS B),

    L2 AS(SELECT 1 AS c FROM L1 AS A, L1 AS B),

    L3 AS(SELECT 1 AS c FROM L2 AS A, L2 AS B),

    L4 AS(SELECT 1 AS c FROM L3 AS A, L3 AS B),

    L5 AS(SELECT 1 AS c FROM L4 AS A, L4 AS B),

    Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY c) AS n FROM L5)

    INSERT INTO test5

    SELECT n,n+1000

    FROM Nums

    WHERE n <= 10000;

     

    --查看聚集索引

    DBCC IND(testdb,'test5',1)

    indexID>1的为非聚集索引,indexID=1的为聚集索引,indexlevel>0的为中间节点,PageType=2是索引页

    页ID=10780是聚集索引中间节点,通过这个页可以查看叶子节点及起始键值

    可以看到,没有uniquifier列,因为我们创建的聚集索引是主键,即PRIMARY KEY CLUSTERED

    再次查询叶子节点childpageid即可看到索引页内容,不再演示

     

    --查看非聚集索引

    DBCC IND(testdb,'test5',-1)

     

    indexID>1的为非聚集索引,indexlevel>0的为中间节点,10782

    DBCC PAGE(testdb,1,10782,3)

    可以看到,我们创建的非聚集索引键列是ID1,实际查到的有2个,其中一个是聚集索引的键列ID,正是通过这个键列才能找到不包含在非聚集索引中的其他列的数据

     

    2.4最好使用自增列作为聚集索引列

        同样推荐创建一个和数据本身无关的自增列作为聚集索引列。如上2.3所述,如果使用自增列,新行的插入则会大大的减少分页和碎片。

     

    总结,聚集索引键列选择原则,唯,窄,增

  • 相关阅读:
    Java常用的函数式接口
    Java网络编程的使用
    Java 多线程的使用
    VB.NET_DBUtil
    java 文件操作与IO流的常用方法
    JAVA Excel.xlsx 上传于下载
    java测试类
    Java11 HashMap源码分析(一、文档翻译)
    Canal实现Redis缓存实时更新(二)
    Canal实现Redis缓存实时更新(一)
  • 原文地址:https://www.cnblogs.com/Alum/p/3675215.html
Copyright © 2020-2023  润新知