SQL索引管理器 - 用于SQL Server和Azure上的索引维护的免费GUI工具

我做为SQL Server DBA工做了8年多,管理和优化服务器的性能。在个人空闲时间,我想为宇宙和个人同事作一些有用的事情。这就是咱们最终为SQL Server和Azure 提供免费索引维护工具的方法。git

理念

每隔一段时间,人们在处理他们的优先事项时,可能就像一个手指式电池 - 一个激励充电只持续一闪,而后一切都消失了。直到最近,我在这一辈子活观察中也不例外。我常常被想法创造属于我本身的想法所困扰,但优先级从一个变为另外一个而且没有完成任何事情。sql

DevArt开发用于开发和管理SQL Server,MySQL和Oracle数据库的软件,对个人动机和专业成长产生了很大的影响。数据库

在他们来以前,我对建立本身的产品的具体细节知之甚少,但在此过程当中,我得到了不少关于SQL Server内部结构的知识。一年多以来,我一直致力于优化产品线中的查询,逐渐开始了解市场上哪些功能比另外一种功能更受欢迎。缓存

在某个阶段,制做一个新的利基产品的想法出如今我面前,但因为某些状况,这个想法没有成功。那时,基本上我没有为公司内部的新项目找到足够的资源而不影响核心业务。服务器

在一个崭新的地方工做,并试图本身建立一个项目让我不断妥协。制造一个拥有全部花里胡哨的大产品的最初想法很快就会中止并逐渐转变为一个不一样的方向 - 将计划的功能分解为单独的迷你工具并相互独立地实现它们。网络

所以,SQL Index Manager诞生了,它是SQL Server和Azure的免费索引维护工具。主要想法是将RedGate和Devart公司的商业替代品做为基础,并尝试在我本身的项目中改进其功能。函数

SQL索引管理器 - 用于SQL Server和Azure上的索引维护的免费GUI工具

SQL索引管理器 - 用于SQL Server和Azure上的索引维护的免费GUI工具

履行

口头上说,一切听起来都很简单......只需观看几个激励视频,打开“Rocky Balboa”模式,开始制做一款很酷的产品。但让咱们面对音乐,一切都不那么乐观,由于在使用系统表函数时存在许多陷阱,sys.dm_db_index_physical_stats同时,它是惟一能够从中获取有关索引碎片的最新信息的地方。工具

从开发的最初几天起,就有很好的机会在标准方案中制造沉闷的方式,并复制已经调试过的竞争应用程序的逻辑,同时添加一些自组织。但在分析了元数据的查询后,我想作一些更优化的事情,因为大公司的官僚主义,它们永远不会出如今他们的产品中。性能

在分析RedGate SQL索引管理器(v1.1.9.1378 - 每一个用户155美圆)时,您能够看到应用程序使用一种很是简单的方法:使用第一个查询,咱们得到用户表和视图的列表,而后第二个,咱们返回所选数据库中全部索引的列表。优化

SELECT objects.name AS tableOrViewName , objects.object_id AS tableOrViewId , schemas.name AS schemaName , CAST(ISNULL(lobs.NumLobs, 0) AS BIT) AS ContainsLobs , o.is_memory_optimized FROM sys.objects AS objects JOIN sys.schemas AS schemas ON schemas.schema_id = objects.schema_id LEFT JOIN ( SELECT object_id , COUNT(*) AS NumLobs FROM sys.columns WITH (NOLOCK) WHERE system_type_id IN (34, 35, 99) OR max_length = -1 GROUP BY object_id ) AS lobs ON objects.object_id = lobs.object_id LEFT JOIN sys.tables AS o ON o.object_id = objects.object_id WHERE objects.type = 'U' OR objects.type = 'V' SELECT i.object_id AS tableOrViewId , i.name AS indexName , i.index_id AS indexId , i.allow_page_locks AS allowPageLocks , p.partition_number AS partitionNumber , CAST((c.numPartitions - 1) AS BIT) AS belongsToPartitionedIndex FROM sys.indexes AS i JOIN sys.partitions AS p ON p.index_id = i.index_id AND p.object_id = i.object_id JOIN ( SELECT COUNT(*) AS numPartitions , object_id , index_id FROM sys.partitions GROUP BY object_id , index_id ) AS c ON c.index_id = i.index_id AND c.object_id = i.object_id WHERE i.index_id > 0 -- ignore heaps AND i.is_disabled = 0 AND i.is_hypothetical = 0

接下来,在while每一个索引分区循环中,发送请求以肯定其大小和碎片级别。在扫描结束时,客户端上会显示重量小于进入阈值的索引。

EXEC sp_executesql N' SELECT index_id, avg_fragmentation_in_percent, page_count FROM sys.dm_db_index_physical_stats(@databaseId, @objectId, @indexId, @partitionNr, NULL)' , N'@databaseId int,@objectId int,@indexId int,@partitionNr int' , @databaseId = 7, @objectId = 2133582639, @indexId = 1, @partitionNr = 1 EXEC sp_executesql N' SELECT index_id, avg_fragmentation_in_percent, page_count FROM sys.dm_db_index_physical_stats(@databaseId, @objectId, @indexId, @partitionNr, NULL)' , N'@databaseId int,@objectId int,@indexId int,@partitionNr int' , @databaseId = 7, @objectId = 2133582639, @indexId = 2, @partitionNr = 1 EXEC sp_executesql N' SELECT index_id, avg_fragmentation_in_percent, page_count FROM sys.dm_db_index_physical_stats(@databaseId, @objectId, @indexId, @partitionNr, NULL)' , N'@databaseId int,@objectId int,@indexId int,@partitionNr int' , @databaseId = 7, @objectId = 2133582639, @indexId = 3, @partitionNr = 1

在分析此应用程序的逻辑时,您可能会发现各类缺点。例如,在发送请求以前,不会检查当前分区是否包含任何行以从扫描中排除空分区。

可是问题在另外一个方面表现得更加尖锐 - 对服务器的请求数量大约等于来自的总行数sys.partitions鉴于真实数据库能够包含数万个分区,这种细微差异可能致使对服务器的大量相似请求。在数据库位于远程服务器上的状况下,因为每一个请求的执行中的网络延迟增长,扫描时间将更长,即便是最简单的一个。

与RedGate不一样,由DevArt开发的相似产品 - 用于SQL Server的dbForge索引管理器(v1.10.38 - 每用户99美圆)在一个大型查询中接收信息,而后在客户端上显示全部内容:

SELECT SCHEMA_NAME(o.[schema_id]) AS [schema_name] , o.name AS parent_name , o.[type] AS parent_type , i.name , i.type_desc , s.avg_fragmentation_in_percent , s.page_count , p.partition_number , p.[rows] , ISNULL(lob.is_lob_legacy, 0) AS is_lob_legacy , ISNULL(lob.is_lob, 0) AS is_lob , CASE WHEN ds.[type] = 'PS' THEN 1 ELSE 0 END AS is_partitioned FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL) s JOIN sys.partitions p ON s.[object_id] = p.[object_id] AND s.index_id = p.index_id AND s.partition_number = p.partition_number JOIN sys.indexes i ON i.[object_id] = s.[object_id] AND i.index_id = s.index_id LEFT JOIN ( SELECT c.[object_id] , index_id = ISNULL(i.index_id, 1) , is_lob_legacy = MAX(CASE WHEN c.system_type_id IN (34, 35, 99) THEN 1 END) , is_lob = MAX(CASE WHEN c.max_length = -1 THEN 1 END) FROM sys.columns c LEFT JOIN sys.index_columns i ON c.[object_id] = i.[object_id] AND c.column_id = i.column_id AND i.index_id > 0 WHERE c.system_type_id IN (34, 35, 99) OR c.max_length = -1 GROUP BY c.[object_id], i.index_id ) lob ON lob.[object_id] = i.[object_id] AND lob.index_id = i.index_id JOIN sys.objects o ON o.[object_id] = i.[object_id] JOIN sys.data_spaces ds ON i.data_space_id = ds.data_space_id WHERE i.[type] IN (1, 2) AND i.is_disabled = 0 AND i.is_hypothetical = 0 AND s.index_level = 0 AND s.alloc_unit_type_desc = 'IN_ROW_DATA' AND o.[type] IN ('U', 'V')

消除了竞争产品中相似请求的面纱的主要问题,可是这种实现的缺点是没有额外的参数传递给sys.dm_db_index_physical_stats能够限制对明显没必要要的索引的扫描函数。实际上,这会致使获取系统中全部索引的信息以及扫描阶段没必要要的磁盘负载。

值得一提的是,从中获取的源码数据sys.dm_db_index_physical_stats并未永久缓存在缓冲池中,所以在获取有关索引碎片的信息时最小化物理读取是个人应用程序开发过程当中的优先任务之一。

通过屡次实验,我设法将扫描分为两部分,将两种方法结合起来。最初,一个大型请求经过过滤那些未包含在过滤范围中的分区来预先肯定分区的大小:

INSERT INTO #AllocationUnits (ContainerID, ReservedPages, UsedPages) SELECT [container_id] , SUM([total_pages]) , SUM([used_pages]) FROM sys.allocation_units WITH(NOLOCK) GROUP BY [container_id] HAVING SUM([total_pages]) BETWEEN @MinIndexSize AND @MaxIndexSize

接下来,咱们只获取包含数据的分区,以免从空索引中进行没必要要的读取。

SELECT [object_id]
     , [index_id]
     , [partition_id]
     , [partition_number]
     , [rows]
     , [data_compression]
INTO #Partitions FROM sys.partitions WITH(NOLOCK) WHERE [object_id] > 255 AND [rows] > 0 AND [object_id] NOT IN (SELECT * FROM #ExcludeList)

根据设置,仅获取用户想要分析的索引类型(支持堆,群集/非群集索引和列存储)。

INSERT INTO #Indexes SELECT ObjectID = i.[object_id] , IndexID = i.index_id , IndexName = i.[name] , PagesCount = a.ReservedPages , UnusedPagesCount = a.ReservedPages - a.UsedPages , PartitionNumber = p.[partition_number] , RowsCount = ISNULL(p.[rows], 0) , IndexType = i.[type] , IsAllowPageLocks = i.[allow_page_locks] , DataSpaceID = i.[data_space_id] , DataCompression = p.[data_compression] , IsUnique = i.[is_unique] , IsPK = i.[is_primary_key] , FillFactorValue = i.[fill_factor] , IsFiltered = i.[has_filter] FROM #AllocationUnits a JOIN #Partitions p ON a.ContainerID = p.[partition_id] JOIN sys.indexes i WITH(NOLOCK) ON i.[object_id] = p.[object_id] AND p.[index_id] = i.[index_id] WHERE i.[type] IN (0, 1, 2, 5, 6) AND i.[object_id] > 255

以后,咱们添加了一些魔法,而且......对于全部小的索引,咱们经过重复调用sys.dm_db_index_physical_stats具备全部参数的完整指示的函数来肯定碎片的级别

INSERT INTO #Fragmentation (ObjectID, IndexID, PartitionNumber, Fragmentation) SELECT i.ObjectID , i.IndexID , i.PartitionNumber , r.[avg_fragmentation_in_percent] FROM #Indexes i CROSS APPLY sys.dm_db_index_physical_stats_ (@DBID, i.ObjectID, i.IndexID, i.PartitionNumber, 'LIMITED') r WHERE i.PagesCount <= @PreDescribeSize AND r.[index_level] = 0 AND r.[alloc_unit_type_desc] = 'IN_ROW_DATA' AND i.IndexType IN (0, 1, 2)

接下来,咱们经过过滤掉额外的数据将全部可能的信息返回给客户端:

SELECT i.ObjectID
     , i.IndexID
     , i.IndexName
     , ObjectName       = o.[name]
     , SchemaName       = s.[name]
     , i.PagesCount
     , i.UnusedPagesCount
     , i.PartitionNumber
     , i.RowsCount
     , i.IndexType
     , i.IsAllowPageLocks
     , u.TotalWrites
     , u.TotalReads
     , u.TotalSeeks
     , u.TotalScans
     , u.TotalLookups
     , u.LastUsage
     , i.DataCompression
     , f.Fragmentation
     , IndexStats       = STATS_DATE(i.ObjectID, i.IndexID) , IsLobLegacy = ISNULL(lob.IsLobLegacy, 0) , IsLob = ISNULL(lob.IsLob, 0) , IsSparse = CAST(CASE WHEN p.ObjectID IS NULL THEN 0 ELSE 1 END AS BIT) , IsPartitioned = CAST(CASE WHEN dds.[data_space_id] _ IS NOT NULL THEN 1 ELSE 0 END AS BIT) , FileGroupName = fg.[name] , i.IsUnique , i.IsPK , i.FillFactorValue , i.IsFiltered , a.IndexColumns , a.IncludedColumns FROM #Indexes i JOIN sys.objects o WITH(NOLOCK) ON o.[object_id] = i.ObjectID JOIN sys.schemas s WITH(NOLOCK) ON s.[schema_id] = o.[schema_id] LEFT JOIN #AggColumns a ON a.ObjectID = i.ObjectID AND a.IndexID = i.IndexID LEFT JOIN #Sparse p ON p.ObjectID = i.ObjectID LEFT JOIN #Fragmentation f ON f.ObjectID = i.ObjectID AND f.IndexID = i.IndexID AND f.PartitionNumber = i.PartitionNumber LEFT JOIN ( SELECT ObjectID = [object_id] , IndexID = [index_id] , TotalWrites = NULLIF([user_updates], 0) , TotalReads = NULLIF([user_seeks] + [user_scans] + [user_lookups], 0) , TotalSeeks = NULLIF([user_seeks], 0) , TotalScans = NULLIF([user_scans], 0) , TotalLookups = NULLIF([user_lookups], 0) , LastUsage = ( SELECT MAX(dt) FROM ( VALUES ([last_user_seek]) , ([last_user_scan]) , ([last_user_lookup]) , ([last_user_update]) ) t(dt) ) FROM sys.dm_db_index_usage_stats WITH(NOLOCK) WHERE [database_id] = @DBID ) u ON i.ObjectID = u.ObjectID AND i.IndexID = u.IndexID LEFT JOIN #Lob lob ON lob.ObjectID = i.ObjectID AND lob.IndexID = i.IndexID LEFT JOIN sys.destination_data_spaces dds WITH(NOLOCK) _ ON i.DataSpaceID = dds.[partition_scheme_id] AND i.PartitionNumber = dds.[destination_id] JOIN sys.filegroups fg WITH(NOLOCK) _ ON ISNULL(dds.[data_space_id], i.DataSpaceID) = fg.[data_space_id] WHERE o.[type] IN ('V', 'U') AND ( f.Fragmentation >= @Fragmentation OR i.PagesCount > @PreDescribeSize OR i.IndexType IN (5, 6) )

以后,点请求肯定大型索引的碎片级别。

EXEC sp_executesql N' DECLARE @DBID INT = DB_ID() SELECT [avg_fragmentation_in_percent] FROM sys.dm_db_index_physical_stats(@DBID, @ObjectID, @IndexID, @PartitionNumber, ''LIMITED'') WHERE [index_level] = 0 AND [alloc_unit_type_desc] = ''IN_ROW_DATA''' , N'@ObjectID int,@IndexID int,@PartitionNumber int' , @ObjectId = 1044198770, @IndexId = 1, @PartitionNumber = 1 EXEC sp_executesql N' DECLARE @DBID INT = DB_ID() SELECT [avg_fragmentation_in_percent] FROM sys.dm_db_index_physical_stats(@DBID, @ObjectID, @IndexID, @PartitionNumber, ''LIMITED'') WHERE [index_level] = 0 AND [alloc_unit_type_desc] = ''IN_ROW_DATA''' , N'@ObjectID int,@IndexID int,@PartitionNumber int' , @ObjectId = 1552724584, @IndexId = 0, @PartitionNumber = 1

因为这种方法,在生成请求时,我设法解决了竞争对手应用程序中遇到的扫描性能问题。这多是它的终结,但在开发过程当中,逐渐出现了各类新的想法,这使得扩大个人产品的应用范围成为可能。

最初,实现了对使用的支持WAIT_AT_LOW_PRIORITY,而后可使用DATA_COMPRESSIONFILL_FACTOR重建索引。

SQL索引管理器 - 用于SQL Server和Azure上的索引维护的免费GUI工具

该应用程序已被“撒上”之前未计划的功能,如维护列存储:

SELECT *
FROM ( SELECT IndexID = [index_id] , PartitionNumber = [partition_number] , PagesCount = SUM([size_in_bytes]) / 8192 , UnusedPagesCount = ISNULL(SUM(CASE WHEN [state] = 1 _ THEN [size_in_bytes] END), 0) / 8192 , Fragmentation = CAST(ISNULL(SUM(CASE WHEN [state] = 1 _ THEN [size_in_bytes] END), 0) * 100. / SUM([size_in_bytes]) AS FLOAT) FROM sys.fn_column_store_row_groups(@ObjectID) GROUP BY [index_id] , [partition_number] ) t WHERE Fragmentation >= @Fragmentation AND PagesCount BETWEEN @MinIndexSize AND @MaxIndexSize

或者根据如下信息建立非聚簇索引的能力dm_db_missing_index

SELECT ObjectID     = d.[object_id]
     , UserImpact   = gs.[avg_user_impact]
     , TotalReads   = gs.[user_seeks] + gs.[user_scans]
     , TotalSeeks   = gs.[user_seeks]
     , TotalScans   = gs.[user_scans]
     , LastUsage    = ISNULL(gs.[last_user_scan], gs.[last_user_seek])
     , IndexColumns =
                CASE WHEN d.[equality_columns] IS NOT NULL _AND d.[inequality_columns] IS NOT NULL THEN d.[equality_columns] + ', ' + d.[inequality_columns] WHEN d.[equality_columns] IS NOT NULL AND d.[inequality_columns] IS NULL THEN d.[equality_columns] ELSE d.[inequality_columns] END , IncludedColumns = d.[included_columns] FROM sys.dm_db_missing_index_groups g WITH(NOLOCK) JOIN sys.dm_db_missing_index_group_stats gs WITH(NOLOCK) _ ON gs.[group_handle] = g.[index_group_handle] JOIN sys.dm_db_missing_index_details d WITH(NOLOCK) _ ON g.[index_handle] = d.[index_handle] WHERE d.[database_id] = DB_ID()

结果和计划

关键的是,开发计划并无就此结束,由于我渴望进一步开发这个应用程序网站源码。下一步是添加查找重复(已完成)或未使用索引的功能,以及实现对在SQL Server中维护统计信息的彻底支持。

如今市场上有不少付费解决方案。我想相信,因为自由定位,更优化的查询以及各类有用的gismos的可用性,这个产品确定会在平常任务中变得有用。

相关文章
相关标签/搜索