Revisión de la herramienta gratuita SQLIndexManager

Como sabe, los índices desempeñan un papel importante en un DBMS, ya que permiten una búsqueda rápida de los registros necesarios. Por eso es tan importante darles servicio de manera oportuna. Se ha escrito bastante material sobre análisis y optimización, incluso en Internet. Por ejemplo, este tema fue revisado recientemente en esta publicacion.

Hay muchas soluciones gratuitas y de pago para esto. Por ejemplo, hay un ya hecho. decisión, basado en un método de optimización de índice adaptativo.

A continuación, veamos la utilidad gratuita. Administrador de índice SQL, escrito por AlanDenton.

La principal diferencia técnica entre SQLIndexManager y otros análogos la indica el propio autor. aquí и aquí.

En este artículo, analizaremos desde afuera el proyecto y las capacidades operativas de esta solución de software.

Discutiendo esta utilidad aquí.
Con el tiempo, se corrigieron la mayoría de los comentarios y errores.

Entonces, pasemos ahora a la propia utilidad SQLIndexManager.

La aplicación está escrita en C# .NET Framework 4.5 en Visual Studio 2017 y utiliza DevExpress para formularios:

Revisión de la herramienta gratuita SQLIndexManager

y se ve así:

Revisión de la herramienta gratuita SQLIndexManager

Todas las solicitudes se generan en los siguientes archivos:

  1. Home
  2. Consulta
  3. Motor de consultas
  4. Información del servidor

Revisión de la herramienta gratuita SQLIndexManager

Al conectarse a una base de datos y enviar consultas al DBMS, la aplicación se firma de la siguiente manera:

ApplicationName=”SQLIndexManager”

Cuando inicie la aplicación, se abrirá una ventana modal para agregar una conexión:
Revisión de la herramienta gratuita SQLIndexManager

Aquí, cargar una lista completa de todas las instancias de MS SQL Server accesibles a través de redes locales aún no funciona.

También puedes agregar una conexión usando el botón más a la izquierda en el menú principal:

Revisión de la herramienta gratuita SQLIndexManager

A continuación se lanzarán las siguientes consultas al DBMS:

  1. Obtención de información sobre el DBMS
    SELECT ProductLevel  = SERVERPROPERTY('ProductLevel')
         , Edition       = SERVERPROPERTY('Edition')
         , ServerVersion = SERVERPROPERTY('ProductVersion')
         , IsSysAdmin    = CAST(IS_SRVROLEMEMBER('sysadmin') AS BIT)
    

  2. Obtener una lista de bases de datos disponibles con sus propiedades breves
    SELECT DatabaseName = t.[name]
         , d.DataSize
         , DataUsedSize  = CAST(NULL AS BIGINT)
         , d.LogSize
         , LogUsedSize   = CAST(NULL AS BIGINT)
         , RecoveryModel = t.recovery_model_desc
         , LogReuseWait  = t.log_reuse_wait_desc
    FROM sys.databases t WITH(NOLOCK)
    LEFT JOIN (
        SELECT [database_id]
             , DataSize = SUM(CASE WHEN [type] = 0 THEN CAST(size AS BIGINT) END)
             , LogSize  = SUM(CASE WHEN [type] = 1 THEN CAST(size AS BIGINT) END)
        FROM sys.master_files WITH(NOLOCK)
        GROUP BY [database_id]
    ) d ON d.[database_id] = t.[database_id]
    WHERE t.[state] = 0
        AND t.[database_id] != 2
        AND ISNULL(HAS_DBACCESS(t.[name]), 1) = 1
    

Después de ejecutar los scripts anteriores, aparecerá una ventana que contiene información breve sobre las bases de datos de la instancia seleccionada de MS SQL Server:

Revisión de la herramienta gratuita SQLIndexManager

Cabe destacar que se muestra información ampliada en función de derechos. Sí hay administrador de sistemas, luego puede seleccionar datos de la vista sys.archivos_maestros. Si no existen tales derechos, simplemente se devuelven menos datos para no ralentizar la solicitud.

Aquí debe seleccionar las bases de datos de interés y hacer clic en el botón "Aceptar".

A continuación, se ejecutará el siguiente script para cada base de datos seleccionada para analizar el estado de los índices:

Análisis del estado del índice

declare @Fragmentation float=15;
declare @MinIndexSize bigint=768;
declare @MaxIndexSize bigint=1048576;
declare @PreDescribeSize bigint=32768;
SET NOCOUNT ON
SET ARITHABORT ON
SET NUMERIC_ROUNDABORT OFF
IF OBJECT_ID('tempdb.dbo.#AllocationUnits') IS NOT NULL
DROP TABLE #AllocationUnits
CREATE TABLE #AllocationUnits (
ContainerID   BIGINT PRIMARY KEY
, ReservedPages BIGINT NOT NULL
, UsedPages     BIGINT NOT NULL
)
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
IF OBJECT_ID('tempdb.dbo.#ExcludeList') IS NOT NULL
DROP TABLE #ExcludeList
CREATE TABLE #ExcludeList (ID INT PRIMARY KEY)
INSERT INTO #ExcludeList
SELECT [object_id]
FROM sys.objects WITH(NOLOCK)
WHERE [type] IN ('V', 'U')
AND ( [is_ms_shipped] = 1 )
IF OBJECT_ID('tempdb.dbo.#Partitions') IS NOT NULL
DROP TABLE #Partitions
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)
IF OBJECT_ID('tempdb.dbo.#Indexes') IS NOT NULL
DROP TABLE #Indexes
CREATE TABLE #Indexes (
ObjectID         INT NOT NULL
, IndexID          INT NOT NULL
, IndexName        SYSNAME NULL
, PagesCount       BIGINT NOT NULL
, UnusedPagesCount BIGINT NOT NULL
, PartitionNumber  INT NOT NULL
, RowsCount        BIGINT NOT NULL
, IndexType        TINYINT NOT NULL
, IsAllowPageLocks BIT NOT NULL
, DataSpaceID      INT NOT NULL
, DataCompression  TINYINT NOT NULL
, IsUnique         BIT NOT NULL
, IsPK             BIT NOT NULL
, FillFactorValue  INT NOT NULL
, IsFiltered       BIT NOT NULL
, PRIMARY KEY (ObjectID, IndexID, PartitionNumber)
)
INSERT INTO #Indexes
SELECT ObjectID         = i.[object_id]
, IndexID          = i.index_id
, IndexName        = i.[name]
, PagesCount       = a.ReservedPages
, UnusedPagesCount = CASE WHEN ABS(a.ReservedPages - a.UsedPages) > 32 THEN a.ReservedPages - a.UsedPages ELSE 0 END
, 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
DECLARE @files TABLE (ID INT PRIMARY KEY)
INSERT INTO @files
SELECT DISTINCT [data_space_id]
FROM sys.database_files WITH(NOLOCK)
WHERE [state] != 0
AND [type] = 0
IF @@ROWCOUNT > 0 BEGIN
DELETE FROM i
FROM #Indexes i
LEFT JOIN sys.destination_data_spaces dds WITH(NOLOCK) ON i.DataSpaceID = dds.[partition_scheme_id] AND i.PartitionNumber = dds.[destination_id]
WHERE ISNULL(dds.[data_space_id], i.DataSpaceID) IN (SELECT * FROM @files)
END
DECLARE @DBID   INT
, @DBNAME SYSNAME
SET @DBNAME = DB_NAME()
SELECT @DBID = [database_id]
FROM sys.databases WITH(NOLOCK)
WHERE [name] = @DBNAME
IF OBJECT_ID('tempdb.dbo.#Fragmentation') IS NOT NULL
DROP TABLE #Fragmentation
CREATE TABLE #Fragmentation (
ObjectID         INT NOT NULL
, IndexID          INT NOT NULL
, PartitionNumber  INT NOT NULL
, Fragmentation    FLOAT NOT NULL
, PRIMARY KEY (ObjectID, IndexID, PartitionNumber)
)
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)
IF OBJECT_ID('tempdb.dbo.#Columns') IS NOT NULL
DROP TABLE #Columns
CREATE TABLE #Columns (
ObjectID     INT NOT NULL
, ColumnID     INT NOT NULL
, ColumnName   SYSNAME NULL
, SystemTypeID TINYINT NULL
, IsSparse     BIT
, IsColumnSet  BIT
, MaxLen       INT
, PRIMARY KEY (ObjectID, ColumnID)
)
INSERT INTO #Columns
SELECT ObjectID     = [object_id]
, ColumnID     = [column_id]
, ColumnName   = [name]
, SystemTypeID = [system_type_id]
, IsSparse     = [is_sparse]
, IsColumnSet  = [is_column_set]
, MaxLen       = [max_length]
FROM sys.columns WITH(NOLOCK)
WHERE [object_id] IN (SELECT DISTINCT i.ObjectID FROM #Indexes i)
IF OBJECT_ID('tempdb.dbo.#IndexColumns') IS NOT NULL
DROP TABLE #IndexColumns
CREATE TABLE #IndexColumns (
ObjectID   INT NOT NULL
, IndexID    INT NOT NULL
, OrderID    INT NOT NULL
, ColumnID   INT NOT NULL
, IsIncluded BIT NOT NULL
, PRIMARY KEY (ObjectID, IndexID, ColumnID)
)
INSERT INTO #IndexColumns
SELECT ObjectID   = [object_id]
, IndexID    = [index_id]
, OrderID    = CASE WHEN [is_included_column] = 0 THEN [key_ordinal] ELSE [index_column_id] END
, ColumnID   = [column_id]
, IsIncluded = ISNULL([is_included_column], 0)
FROM sys.index_columns ic WITH(NOLOCK)
WHERE EXISTS(
SELECT *
FROM #Indexes i
WHERE i.ObjectID = ic.[object_id]
AND i.IndexID = ic.[index_id]
AND i.IndexType IN (1, 2)
)
IF OBJECT_ID('tempdb.dbo.#Lob') IS NOT NULL
DROP TABLE #Lob
CREATE TABLE #Lob (
ObjectID    INT NOT NULL
, IndexID     INT NOT NULL
, IsLobLegacy BIT
, IsLob       BIT
, PRIMARY KEY (ObjectID, IndexID)
)
INSERT INTO #Lob (ObjectID, IndexID, IsLobLegacy, IsLob)
SELECT c.ObjectID
, IndexID     = ISNULL(i.IndexID, 1)
, IsLobLegacy = MAX(CASE WHEN c.SystemTypeID IN (34, 35, 99) THEN 1 END)
, IsLob       = 0
FROM #Columns c
LEFT JOIN #IndexColumns i ON c.ObjectID = i.ObjectID AND c.ColumnID = i.ColumnID
WHERE c.SystemTypeID IN (34, 35, 99)
GROUP BY c.ObjectID
, i.IndexID
IF OBJECT_ID('tempdb.dbo.#Sparse') IS NOT NULL
DROP TABLE #Sparse
CREATE TABLE #Sparse (ObjectID INT PRIMARY KEY)
INSERT INTO #Sparse
SELECT DISTINCT ObjectID
FROM #Columns
WHERE IsSparse = 1
OR IsColumnSet = 1
IF OBJECT_ID('tempdb.dbo.#AggColumns') IS NOT NULL
DROP TABLE #AggColumns
CREATE TABLE #AggColumns (
ObjectID        INT NOT NULL
, IndexID         INT NOT NULL
, IndexColumns    NVARCHAR(MAX)
, IncludedColumns NVARCHAR(MAX)
, PRIMARY KEY (ObjectID, IndexID)
)
INSERT INTO #AggColumns
SELECT t.ObjectID
, t.IndexID
, IndexColumns = STUFF((
SELECT ', [' + c.ColumnName + ']'
FROM #IndexColumns i
JOIN #Columns c ON i.ObjectID = c.ObjectID AND i.ColumnID = c.ColumnID
WHERE i.ObjectID = t.ObjectID
AND i.IndexID = t.IndexID
AND i.IsIncluded = 0
ORDER BY i.OrderID
FOR XML PATH(''), TYPE).value('(./text())[1]', 'NVARCHAR(MAX)'), 1, 2, '')
, IncludedColumns = STUFF((
SELECT ', [' + c.ColumnName + ']'
FROM #IndexColumns i
JOIN #Columns c ON i.ObjectID = c.ObjectID AND i.ColumnID = c.ColumnID
WHERE i.ObjectID = t.ObjectID
AND i.IndexID = t.IndexID
AND i.IsIncluded = 1
ORDER BY i.OrderID
FOR XML PATH(''), TYPE).value('(./text())[1]', 'NVARCHAR(MAX)'), 1, 2, '')
FROM (
SELECT DISTINCT ObjectID, IndexID
FROM #Indexes
WHERE IndexType IN (1, 2)
) t
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)
)

Como puede verse en las consultas mismas, las tablas temporales se utilizan con bastante frecuencia. Esto se hace para que no haya recompilaciones y, en el caso de un esquema grande, el plan se puede generar en paralelo al insertar datos, ya que insertar variables de tabla solo es posible en un hilo.

Después de ejecutar el script anterior, aparecerá una ventana con una tabla de índice:

Revisión de la herramienta gratuita SQLIndexManager

También puede mostrar otra información detallada aquí, como:

  1. base de datos
  2. número de secciones
  3. fecha y hora de la última llamada
  4. apretar
  5. grupo de archivos

etcétera
Los propios altavoces se pueden personalizar:

Revisión de la herramienta gratuita SQLIndexManager

En las celdas de la columna Corregir, puede seleccionar qué acción se realizará durante la optimización. Además, cuando se completa el análisis, se selecciona una acción predeterminada según la configuración seleccionada:

Revisión de la herramienta gratuita SQLIndexManager

Debe seleccionar los índices deseados para su procesamiento.

Usando el menú principal, puede guardar el script (el mismo botón inicia el proceso de optimización del índice):

Revisión de la herramienta gratuita SQLIndexManager

y guarde la tabla en diferentes formatos (el mismo botón le permite abrir configuraciones detalladas para analizar y optimizar índices):

Revisión de la herramienta gratuita SQLIndexManager

También puedes actualizar la información pulsando en el tercer botón de la izquierda en el menú principal junto a la lupa.

El botón con lupa le permite seleccionar las bases de datos deseadas para su consideración.

Actualmente no existe un sistema de ayuda integral. Por lo tanto, al presionar el botón “?” simplemente hará que aparezca una ventana modal que contiene información básica sobre el producto de software:

Revisión de la herramienta gratuita SQLIndexManager

Además de todo lo descrito anteriormente, el menú principal cuenta con una barra de búsqueda:

Revisión de la herramienta gratuita SQLIndexManager

Al iniciar el proceso de optimización del índice:

Revisión de la herramienta gratuita SQLIndexManager

También puedes ver un registro de las acciones realizadas en la parte inferior de la ventana:

Revisión de la herramienta gratuita SQLIndexManager

En la ventana de configuración detallada para el análisis y optimización del índice, puede configurar opciones más sutiles:

Revisión de la herramienta gratuita SQLIndexManager

Solicitudes para la aplicación:

  1. Permitir actualizar selectivamente las estadísticas no solo para los índices sino también de diferentes maneras (actualización total o parcial).
  2. permiten no solo seleccionar una base de datos, sino también diferentes servidores (esto es muy conveniente cuando hay muchas instancias de MS SQL Server)
  3. Para una mayor flexibilidad de uso, se sugiere empaquetar los comandos en bibliotecas y enviarlos a comandos de PowerShell, como se hace, por ejemplo, aquí:
  4. dbatools.io/comandos
  5. Permitir guardar y cambiar la configuración personal tanto para toda la aplicación como, si es necesario, para cada instancia de MS SQL Server y cada base de datos.
  6. De los puntos 2 y 4 se deduce que desea crear grupos por bases de datos y grupos por instancias de MS SQL Server, para los cuales la configuración es la misma.
  7. buscar índices duplicados (completos e incompletos, que son ligeramente diferentes o difieren sólo en las columnas incluidas)
  8. Dado que SQLIndexManager se usa solo para DBMS de MS SQL Server, es necesario reflejar esto en el nombre, por ejemplo, de la siguiente manera: SQLIndexManager para MS SQL Server
  9. Mueva todas las partes de la aplicación que no sean GUI a módulos separados y vuelva a escribirlas en .NET Core 2.1

En el momento de escribir este artículo, el punto 6 de los deseos se está desarrollando activamente y ya existe apoyo en forma de búsqueda de duplicados completos y similares:

Revisión de la herramienta gratuita SQLIndexManager

fuentes

Fuente: habr.com

Añadir un comentario