经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » MS SQL Server » 查看文章
SQL索引管理器 - 用于SQL Server和Azure上的索引维护的免费GUI工具
来源:cnblogs  作者:九天飞翼  时间:2019/7/22 10:42:43  对本文有异议

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

理念

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

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美元)时,您可以看到应用程序使用一种非常简单的方法:使用第一个查询,我们获得用户表和视图的列表,然后第二个,我们返回所选数据库中所有索引的列表。

  1. SELECT objects.name AS tableOrViewName
  2. , objects.object_id AS tableOrViewId
  3. , schemas.name AS schemaName
  4. , CAST(ISNULL(lobs.NumLobs, 0) AS BIT) AS ContainsLobs
  5. , o.is_memory_optimized
  6. FROM sys.objects AS objects
  7. JOIN sys.schemas AS schemas ON schemas.schema_id = objects.schema_id
  8. LEFT JOIN (
  9. SELECT object_id
  10. , COUNT(*) AS NumLobs
  11. FROM sys.columns WITH (NOLOCK)
  12. WHERE system_type_id IN (34, 35, 99)
  13. OR max_length = -1
  14. GROUP BY object_id
  15. ) AS lobs ON objects.object_id = lobs.object_id
  16. LEFT JOIN sys.tables AS o ON o.object_id = objects.object_id
  17. WHERE objects.type = 'U'
  18. OR objects.type = 'V'
  19. SELECT i.object_id AS tableOrViewId
  20. , i.name AS indexName
  21. , i.index_id AS indexId
  22. , i.allow_page_locks AS allowPageLocks
  23. , p.partition_number AS partitionNumber
  24. , CAST((c.numPartitions - 1) AS BIT) AS belongsToPartitionedIndex
  25. FROM sys.indexes AS i
  26. JOIN sys.partitions AS p ON p.index_id = i.index_id
  27. AND p.object_id = i.object_id
  28. JOIN (
  29. SELECT COUNT(*) AS numPartitions
  30. , object_id
  31. , index_id
  32. FROM sys.partitions
  33. GROUP BY object_id
  34. , index_id
  35. ) AS c ON c.index_id = i.index_id
  36. AND c.object_id = i.object_id
  37. WHERE i.index_id > 0 -- ignore heaps
  38. AND i.is_disabled = 0
  39. AND i.is_hypothetical = 0

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

  1. EXEC sp_executesql N'
  2. SELECT index_id, avg_fragmentation_in_percent, page_count
  3. FROM sys.dm_db_index_physical_stats(@databaseId, @objectId, @indexId, @partitionNr, NULL)'
  4. , N'@databaseId int,@objectId int,@indexId int,@partitionNr int'
  5. , @databaseId = 7, @objectId = 2133582639, @indexId = 1, @partitionNr = 1
  6. EXEC sp_executesql N'
  7. SELECT index_id, avg_fragmentation_in_percent, page_count
  8. FROM sys.dm_db_index_physical_stats(@databaseId, @objectId, @indexId, @partitionNr, NULL)'
  9. , N'@databaseId int,@objectId int,@indexId int,@partitionNr int'
  10. , @databaseId = 7, @objectId = 2133582639, @indexId = 2, @partitionNr = 1
  11. EXEC sp_executesql N'
  12. SELECT index_id, avg_fragmentation_in_percent, page_count
  13. FROM sys.dm_db_index_physical_stats(@databaseId, @objectId, @indexId, @partitionNr, NULL)'
  14. , N'@databaseId int,@objectId int,@indexId int,@partitionNr int'
  15. , @databaseId = 7, @objectId = 2133582639, @indexId = 3, @partitionNr = 1

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

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

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

  1. SELECT SCHEMA_NAME(o.[schema_id]) AS [schema_name]
  2. , o.name AS parent_name
  3. , o.[type] AS parent_type
  4. , i.name
  5. , i.type_desc
  6. , s.avg_fragmentation_in_percent
  7. , s.page_count
  8. , p.partition_number
  9. , p.[rows]
  10. , ISNULL(lob.is_lob_legacy, 0) AS is_lob_legacy
  11. , ISNULL(lob.is_lob, 0) AS is_lob
  12. , CASE WHEN ds.[type] = 'PS' THEN 1 ELSE 0 END AS is_partitioned
  13. FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL) s
  14. JOIN sys.partitions p ON s.[object_id] = p.[object_id]
  15. AND s.index_id = p.index_id
  16. AND s.partition_number = p.partition_number
  17. JOIN sys.indexes i ON i.[object_id] = s.[object_id]
  18. AND i.index_id = s.index_id
  19. LEFT JOIN (
  20. SELECT c.[object_id]
  21. , index_id = ISNULL(i.index_id, 1)
  22. , is_lob_legacy = MAX(CASE WHEN c.system_type_id IN (34, 35, 99) THEN 1 END)
  23. , is_lob = MAX(CASE WHEN c.max_length = -1 THEN 1 END)
  24. FROM sys.columns c
  25. LEFT JOIN sys.index_columns i ON c.[object_id] = i.[object_id]
  26. AND c.column_id = i.column_id
  27. AND i.index_id > 0
  28. WHERE c.system_type_id IN (34, 35, 99)
  29. OR c.max_length = -1
  30. GROUP BY c.[object_id], i.index_id
  31. ) lob ON lob.[object_id] = i.[object_id]
  32. AND lob.index_id = i.index_id
  33. JOIN sys.objects o ON o.[object_id] = i.[object_id]
  34. JOIN sys.data_spaces ds ON i.data_space_id = ds.data_space_id
  35. WHERE i.[type] IN (1, 2)
  36. AND i.is_disabled = 0
  37. AND i.is_hypothetical = 0
  38. AND s.index_level = 0
  39. AND s.alloc_unit_type_desc = 'IN_ROW_DATA'
  40. AND o.[type] IN ('U', 'V')

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

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

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

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

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

  1. SELECT [object_id]
  2. , [index_id]
  3. , [partition_id]
  4. , [partition_number]
  5. , [rows]
  6. , [data_compression]
  7. INTO #Partitions
  8. FROM sys.partitions WITH(NOLOCK)
  9. WHERE [object_id] > 255
  10. AND [rows] > 0
  11. AND [object_id] NOT IN (SELECT * FROM #ExcludeList)

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

  1. INSERT INTO #Indexes
  2. SELECT ObjectID = i.[object_id]
  3. , IndexID = i.index_id
  4. , IndexName = i.[name]
  5. , PagesCount = a.ReservedPages
  6. , UnusedPagesCount = a.ReservedPages - a.UsedPages
  7. , PartitionNumber = p.[partition_number]
  8. , RowsCount = ISNULL(p.[rows], 0)
  9. , IndexType = i.[type]
  10. , IsAllowPageLocks = i.[allow_page_locks]
  11. , DataSpaceID = i.[data_space_id]
  12. , DataCompression = p.[data_compression]
  13. , IsUnique = i.[is_unique]
  14. , IsPK = i.[is_primary_key]
  15. , FillFactorValue = i.[fill_factor]
  16. , IsFiltered = i.[has_filter]
  17. FROM #AllocationUnits a
  18. JOIN #Partitions p ON a.ContainerID = p.[partition_id]
  19. JOIN sys.indexes i WITH(NOLOCK) ON i.[object_id] = p.[object_id]
  20. AND p.[index_id] = i.[index_id]
  21. WHERE i.[type] IN (0, 1, 2, 5, 6)
  22. AND i.[object_id] > 255

之后,我们添加了一些魔法,并且......对于所有小的索引,我们通过重复调用sys.dm_db_index_physical_stats具有所有参数的完整指示的函数来确定碎片的级别

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

接下来,我们通过过滤掉额外的数据将所有可能的信息返回给客户端:

  1. SELECT i.ObjectID
  2. , i.IndexID
  3. , i.IndexName
  4. , ObjectName = o.[name]
  5. , SchemaName = s.[name]
  6. , i.PagesCount
  7. , i.UnusedPagesCount
  8. , i.PartitionNumber
  9. , i.RowsCount
  10. , i.IndexType
  11. , i.IsAllowPageLocks
  12. , u.TotalWrites
  13. , u.TotalReads
  14. , u.TotalSeeks
  15. , u.TotalScans
  16. , u.TotalLookups
  17. , u.LastUsage
  18. , i.DataCompression
  19. , f.Fragmentation
  20. , IndexStats = STATS_DATE(i.ObjectID, i.IndexID)
  21. , IsLobLegacy = ISNULL(lob.IsLobLegacy, 0)
  22. , IsLob = ISNULL(lob.IsLob, 0)
  23. , IsSparse = CAST(CASE WHEN p.ObjectID IS NULL THEN 0 ELSE 1 END AS BIT)
  24. , IsPartitioned = CAST(CASE WHEN dds.[data_space_id] _
  25. IS NOT NULL THEN 1 ELSE 0 END AS BIT)
  26. , FileGroupName = fg.[name]
  27. , i.IsUnique
  28. , i.IsPK
  29. , i.FillFactorValue
  30. , i.IsFiltered
  31. , a.IndexColumns
  32. , a.IncludedColumns
  33. FROM #Indexes i
  34. JOIN sys.objects o WITH(NOLOCK) ON o.[object_id] = i.ObjectID
  35. JOIN sys.schemas s WITH(NOLOCK) ON s.[schema_id] = o.[schema_id]
  36. LEFT JOIN #AggColumns a ON a.ObjectID = i.ObjectID
  37. AND a.IndexID = i.IndexID
  38. LEFT JOIN #Sparse p ON p.ObjectID = i.ObjectID
  39. LEFT JOIN #Fragmentation f ON f.ObjectID = i.ObjectID
  40. AND f.IndexID = i.IndexID
  41. AND f.PartitionNumber = i.PartitionNumber
  42. LEFT JOIN (
  43. SELECT ObjectID = [object_id]
  44. , IndexID = [index_id]
  45. , TotalWrites = NULLIF([user_updates], 0)
  46. , TotalReads = NULLIF([user_seeks] + [user_scans] + [user_lookups], 0)
  47. , TotalSeeks = NULLIF([user_seeks], 0)
  48. , TotalScans = NULLIF([user_scans], 0)
  49. , TotalLookups = NULLIF([user_lookups], 0)
  50. , LastUsage = (
  51. SELECT MAX(dt)
  52. FROM (
  53. VALUES ([last_user_seek])
  54. , ([last_user_scan])
  55. , ([last_user_lookup])
  56. , ([last_user_update])
  57. ) t(dt)
  58. )
  59. FROM sys.dm_db_index_usage_stats WITH(NOLOCK)
  60. WHERE [database_id] = @DBID
  61. ) u ON i.ObjectID = u.ObjectID
  62. AND i.IndexID = u.IndexID
  63. LEFT JOIN #Lob lob ON lob.ObjectID = i.ObjectID
  64. AND lob.IndexID = i.IndexID
  65. LEFT JOIN sys.destination_data_spaces dds WITH(NOLOCK) _
  66. ON i.DataSpaceID = dds.[partition_scheme_id]
  67. AND i.PartitionNumber = dds.[destination_id]
  68. JOIN sys.filegroups fg WITH(NOLOCK) _
  69. ON ISNULL(dds.[data_space_id], i.DataSpaceID) = fg.[data_space_id]
  70. WHERE o.[type] IN ('V', 'U')
  71. AND (
  72. f.Fragmentation >= @Fragmentation
  73. OR
  74. i.PagesCount > @PreDescribeSize
  75. OR
  76. i.IndexType IN (5, 6)
  77. )

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

  1. EXEC sp_executesql N'
  2. DECLARE @DBID INT = DB_ID()
  3. SELECT [avg_fragmentation_in_percent]
  4. FROM sys.dm_db_index_physical_stats(@DBID, @ObjectID, @IndexID, @PartitionNumber, ''LIMITED'')
  5. WHERE [index_level] = 0
  6. AND [alloc_unit_type_desc] = ''IN_ROW_DATA'''
  7. , N'@ObjectID int,@IndexID int,@PartitionNumber int'
  8. , @ObjectId = 1044198770, @IndexId = 1, @PartitionNumber = 1
  9. EXEC sp_executesql N'
  10. DECLARE @DBID INT = DB_ID()
  11. SELECT [avg_fragmentation_in_percent]
  12. FROM sys.dm_db_index_physical_stats(@DBID, @ObjectID, @IndexID, @PartitionNumber, ''LIMITED'')
  13. WHERE [index_level] = 0
  14. AND [alloc_unit_type_desc] = ''IN_ROW_DATA'''
  15. , N'@ObjectID int,@IndexID int,@PartitionNumber int'
  16. , @ObjectId = 1552724584, @IndexId = 0, @PartitionNumber = 1

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

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

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

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

  1. SELECT *
  2. FROM (
  3. SELECT IndexID = [index_id]
  4. , PartitionNumber = [partition_number]
  5. , PagesCount = SUM([size_in_bytes]) / 8192
  6. , UnusedPagesCount = ISNULL(SUM(CASE WHEN [state] = 1 _
  7. THEN [size_in_bytes] END), 0) / 8192
  8. , Fragmentation = CAST(ISNULL(SUM(CASE WHEN [state] = 1 _
  9. THEN [size_in_bytes] END), 0)
  10. * 100. / SUM([size_in_bytes]) AS FLOAT)
  11. FROM sys.fn_column_store_row_groups(@ObjectID)
  12. GROUP BY [index_id]
  13. , [partition_number]
  14. ) t
  15. WHERE Fragmentation >= @Fragmentation
  16. AND PagesCount BETWEEN @MinIndexSize AND @MaxIndexSize

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

  1. SELECT ObjectID = d.[object_id]
  2. , UserImpact = gs.[avg_user_impact]
  3. , TotalReads = gs.[user_seeks] + gs.[user_scans]
  4. , TotalSeeks = gs.[user_seeks]
  5. , TotalScans = gs.[user_scans]
  6. , LastUsage = ISNULL(gs.[last_user_scan], gs.[last_user_seek])
  7. , IndexColumns =
  8. CASE
  9. WHEN d.[equality_columns] IS NOT NULL
  10. _AND d.[inequality_columns] IS NOT NULL
  11. THEN d.[equality_columns] + ', ' + d.[inequality_columns]
  12. WHEN d.[equality_columns] IS NOT NULL AND d.[inequality_columns] IS NULL
  13. THEN d.[equality_columns]
  14. ELSE d.[inequality_columns]
  15. END
  16. , IncludedColumns = d.[included_columns]
  17. FROM sys.dm_db_missing_index_groups g WITH(NOLOCK)
  18. JOIN sys.dm_db_missing_index_group_stats gs WITH(NOLOCK) _
  19. ON gs.[group_handle] = g.[index_group_handle]
  20. JOIN sys.dm_db_missing_index_details d WITH(NOLOCK) _
  21. ON g.[index_handle] = d.[index_handle]
  22. WHERE d.[database_id] = DB_ID()

结果和计划

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

现在市场上有很多付费解决方案。我想相信,由于自由定位,更优化的查询以及各种有用的gismos的可用性,这个产品肯定会在日常任务中变得有用。

原文链接:http://www.cnblogs.com/langda/p/11219420.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号