Gjennomgang av gratisverktøyet SQLIndexManager

Som du vet, spiller indekser en viktig rolle i et DBMS, og gir et raskt søk til de nødvendige postene. Det er derfor det er så viktig å betjene dem i tide. Det er skrevet ganske mye stoff om analyse og optimalisering, blant annet på Internett. For eksempel ble dette emnet nylig gjennomgått i denne publikasjonen.

Det finnes mange betalte og gratis løsninger for dette. For eksempel er det en ferdig avgjørelse, basert på en adaptiv indeksoptimaliseringsmetode.

La oss deretter se på gratisverktøyet SQLIndexManager, forfattet av AlanDenton.

Den viktigste tekniske forskjellen mellom SQLIndexManager og en rekke andre analoger er gitt av forfatteren selv her и her.

I denne artikkelen skal vi se på prosjektet og de operasjonelle egenskapene til denne programvareløsningen.

Diskuterer dette verktøyet her.
Over tid ble de fleste kommentarene og feilene rettet.

Så la oss nå gå videre til selve SQLIndexManager-verktøyet.

Applikasjonen er skrevet i C# .NET Framework 4.5 i Visual Studio 2017 og bruker DevExpress for skjemaer:

Gjennomgang av gratisverktøyet SQLIndexManager

og ser slik ut:

Gjennomgang av gratisverktøyet SQLIndexManager

Alle forespørsler genereres i følgende filer:

  1. Index
  2. Query
  3. QueryEngine
  4. ServerInfo

Gjennomgang av gratisverktøyet SQLIndexManager

Når du kobler til en database og sender spørringer til DBMS, signeres applikasjonen som følger:

ApplicationName=”SQLIndexManager”

Når du starter programmet, åpnes et modalt vindu for å legge til en tilkobling:
Gjennomgang av gratisverktøyet SQLIndexManager

Her fungerer det ennå ikke å laste en fullstendig liste over alle MS SQL Server-instanser som er tilgjengelige over lokale nettverk.

Du kan også legge til en tilkobling ved å bruke knappen lengst til venstre på hovedmenyen:

Gjennomgang av gratisverktøyet SQLIndexManager

Deretter vil følgende spørringer til DBMS bli lansert:

  1. Innhenting av informasjon om DBMS
    SELECT ProductLevel  = SERVERPROPERTY('ProductLevel')
         , Edition       = SERVERPROPERTY('Edition')
         , ServerVersion = SERVERPROPERTY('ProductVersion')
         , IsSysAdmin    = CAST(IS_SRVROLEMEMBER('sysadmin') AS BIT)
    

  2. Få en liste over tilgjengelige databaser med deres korte egenskaper
    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
    

Etter å ha utført skriptene ovenfor, vises et vindu som inneholder kort informasjon om databasene til den valgte forekomsten av MS SQL Server:

Gjennomgang av gratisverktøyet SQLIndexManager

Det er verdt å merke seg at utvidet informasjon vises basert på rettigheter. Hvis det er sysadmin, så kan du velge data fra visningen sys.master_files. Hvis det ikke er slike rettigheter, returneres mindre data ganske enkelt for ikke å bremse forespørselen.

Her må du velge databasene av interesse og klikke på "OK"-knappen.

Deretter vil følgende skript bli utført for hver valgt database for å analysere tilstanden til indeksene:

Indeksstatusanalyse

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)
)

Som det fremgår av selve spørringene, brukes midlertidige tabeller ganske ofte. Dette gjøres slik at det ikke er noen rekompileringer, og ved et stort opplegg kan planen genereres parallelt ved innsetting av data, siden innsetting av tabellvariabler kun er mulig i en tråd.

Etter å ha utført skriptet ovenfor, vises et vindu med en indekstabell:

Gjennomgang av gratisverktøyet SQLIndexManager

Du kan også vise annen detaljert informasjon her, for eksempel:

  1. database
  2. antall seksjoner
  3. dato og klokkeslett for siste samtale
  4. kompresjon
  5. filgruppe

og t. d.
Selve høyttalerne kan tilpasses:

Gjennomgang av gratisverktøyet SQLIndexManager

I cellene i Korriger-kolonnen kan du velge hvilken handling som skal utføres under optimalisering. Når skanningen er fullført, velges også en standardhandling basert på de valgte innstillingene:

Gjennomgang av gratisverktøyet SQLIndexManager

Du må velge de ønskede indeksene for behandling.

Ved å bruke hovedmenyen kan du lagre skriptet (den samme knappen starter selve indeksoptimaliseringsprosessen):

Gjennomgang av gratisverktøyet SQLIndexManager

og lagre tabellen i forskjellige formater (den samme knappen lar deg åpne detaljerte innstillinger for å analysere og optimalisere indekser):

Gjennomgang av gratisverktøyet SQLIndexManager

Du kan også oppdatere informasjonen ved å klikke på den tredje knappen til venstre i hovedmenyen ved siden av forstørrelsesglasset.

Knappen med forstørrelsesglass lar deg velge de ønskede databasene for vurdering.

Det finnes foreløpig ikke noe omfattende hjelpesystem. Derfor trykker du på "?"-knappen vil ganske enkelt føre til at det vises et modalt vindu som inneholder grunnleggende informasjon om programvareproduktet:

Gjennomgang av gratisverktøyet SQLIndexManager

I tillegg til alt beskrevet ovenfor, har hovedmenyen en søkelinje:

Gjennomgang av gratisverktøyet SQLIndexManager

Når du starter indeksoptimaliseringsprosessen:

Gjennomgang av gratisverktøyet SQLIndexManager

Du kan også se en logg over utførte handlinger nederst i vinduet:

Gjennomgang av gratisverktøyet SQLIndexManager

I det detaljerte innstillingsvinduet for indeksanalyse og optimalisering kan du konfigurere mer subtile alternativer:

Gjennomgang av gratisverktøyet SQLIndexManager

Forespørsler om søknaden:

  1. gjøre det mulig å selektivt oppdatere statistikk, ikke bare for indekser og også på forskjellige måter (helt eller delvis)
  2. gjør det mulig ikke bare å velge en database, men også forskjellige servere (dette er veldig praktisk når det er mange forekomster av MS SQL Server)
  3. For større fleksibilitet i bruk, foreslås det å pakke kommandoene inn i biblioteker og sende dem til PowerShell-kommandoer, slik det for eksempel gjøres her:
  4. dbatools.io/commands
  5. gjøre det mulig å lagre og endre personlige innstillinger både for hele applikasjonen og om nødvendig for hver forekomst av MS SQL Server og hver database
  6. Fra punkt 2 og 4 følger det at du ønsker å opprette grupper etter databaser og grupper etter MS SQL Server-instanser, der innstillingene er de samme
  7. søk etter dupliserte indekser (fullstendige og ufullstendige, som enten er litt forskjellige eller bare forskjellige i de inkluderte kolonnene)
  8. Siden SQLIndexManager kun brukes for MS SQL Server DBMS, må dette gjenspeiles i navnet, for eksempel som følger: SQLIndexManager for MS SQL Server
  9. Flytt alle ikke-GUI-deler av applikasjonen til separate moduler og skriv dem om i .NET Core 2.1

I skrivende stund er punkt 6 i ønskene under aktivt utvikling og det er allerede støtte i form av søk etter komplette og lignende duplikater:

Gjennomgang av gratisverktøyet SQLIndexManager

kilder

Kilde: www.habr.com

Legg til en kommentar