今天这篇文章我想谈下堆表上特有的性能问题:转发记录(Forwarding Records)。首先我们要澄清下什么是堆表:堆表就是没有聚集索引定义的表。它对插入新记录非常快,但当你读取数据时非常慢。读取数据会在你的存储子系统上引入随机存取(random I/O) ,有时候当你碰到转发记录,它会进一步降低你的读取性能。
为什么会有转发记录?
当堆表里的记录需要移动到不同的物理位置时,SQL Server使用转发记录。假设你有一个变长列的表,首先你在堆表里插入一些记录,这个时候你没有在变长列里存储任何数据:
1 -- Create a table to demonstrate forwarding records 2 CREATE TABLE HeapTable 3 ( 4 Col1 INT IDENTITY(1, 1), 5 Col2 CHAR(2000), 6 Col3 VARCHAR(1000) 7 ) 8 GO 9 10 -- Insert 4 records - those will fit into one page 11 INSERT INTO HeapTable VALUES 12 ( 13 REPLICATE('1', 2000), 14 '' 15 ), 16 ( 17 REPLICATE('2', 2000), 18 '' 19 ), 20 ( 21 REPLICATE('3', 2000), 22 '' 23 ), 24 ( 25 REPLICATE('4', 2000), 26 '' 27 ) 28 GO
当你在变长列执行UPDATE语句时,想象下会发生什么?在那个情况下SQL Server可能需要扩展这个记录,因为记录大小更长了,其他记录必须从同个数据页移走。
1 -- Let's update the table and expand each row of the table 2 UPDATE HeapTable 3 SET Col3 = REPLICATE('5', 1000) 4 GO
在那个情况下,SQL Server在原始位置留下称为转发记录,它指向记录最终存储的新位置。
SQL Server需要使用这个方法来避免更新同个表上所有非聚集索引。你可能知道,当你在堆表上创建非聚集索引时,在叶子层,非聚集索引指向记录数据存储的物理位置。没有抓发记录的话,所有这些指针都要改变,这会大幅度降低你的性能。
如何修正转发记录?
为了找出表里是否包含转发记录,你可以使用DMF sys.dm_db_index_physical_stats。当你在堆表上调用这个函数并传入DETAILED模式,SQL Server通过forwarded_record_count列告诉你表上的转发记录数。
1 -- Check the forwarding record count through sys.dm_db_index_physical_stats 2 SELECT 3 index_type_desc, 4 page_count, 5 avg_page_space_used_in_percent, 6 avg_record_size_in_bytes, 7 forwarded_record_count 8 FROM sys.dm_db_index_physical_stats 9 ( 10 DB_ID('ALLOCATIONDB'), 11 OBJECT_ID('HeapTable'), 12 NULL, 13 NULL, 14 'DETAILED' 15 ) 16 GO
可以看到,表上有2条转发记录,在4条记录之外。为了去掉这些转发记录,你可以重建表。
1 -- Rebuild the heap table to get rid of the Forwarding Records 2 ALTER TABLE HeapTable REBUILD 3 GO
再次运行刚才的查询,你会发现转发记录已经消失。
1 -- Check the forwarding record count through sys.dm_db_index_physical_stats 2 SELECT 3 index_type_desc, 4 page_count, 5 avg_page_space_used_in_percent, 6 avg_record_size_in_bytes, 7 forwarded_record_count 8 FROM sys.dm_db_index_physical_stats 9 ( 10 DB_ID('ALLOCATIONDB'), 11 OBJECT_ID('HeapTable'), 12 NULL, 13 NULL, 14 'DETAILED' 15 ) 16 GO
DBA总会考虑到索引碎片,索引重建和索引重新组织操作。但没有人想过堆表里的转发记录。如果你维护数据库,经常检查下堆表上的转发记录数,保证始终有最好的性能,这是个非常好的想法。
小结
在这个文章里你看到了堆表上转发记录是咋样,它是如何降低的记录读取,因为需要额外的逻辑读。当我为数据库进行健康检查时,我在检查堆表时,总会看下转发记录数。
相信我:数据库里会有巨大数量的堆表,在生产系统里也会有很多转发记录,但DBA们并没意识到这个副作用。作为第一经验,我经常推荐在表上建立聚集索引来避免转发记录。当然在一些特定场景里,例如最后页插入加锁竞争(Last Page Insert Latch Contention),在这里你可以使用堆表来避免这个问题,但大多数情况下,在表上建立聚集索引还是非常有用的。
参考文章:
https://www.sqlpassion.at/archive/2014/09/08/forwarding-records-on-heap-tables/