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:
og ser slik ut:
Alle forespørsler genereres i følgende filer:
Index
Query
QueryEngine
ServerInfo
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:
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:
Deretter vil følgende spørringer til DBMS bli lansert:
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:
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:
Du kan også vise annen detaljert informasjon her, for eksempel:
database
antall seksjoner
dato og klokkeslett for siste samtale
kompresjon
filgruppe
og t. d.
Selve høyttalerne kan tilpasses:
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:
Du må velge de ønskede indeksene for behandling.
Ved å bruke hovedmenyen kan du lagre skriptet (den samme knappen starter selve indeksoptimaliseringsprosessen):
og lagre tabellen i forskjellige formater (den samme knappen lar deg åpne detaljerte innstillinger for å analysere og optimalisere indekser):
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:
I tillegg til alt beskrevet ovenfor, har hovedmenyen en søkelinje:
Når du starter indeksoptimaliseringsprosessen:
Du kan også se en logg over utførte handlinger nederst i vinduet:
I det detaljerte innstillingsvinduet for indeksanalyse og optimalisering kan du konfigurere mer subtile alternativer:
Forespørsler om søknaden:
gjøre det mulig å selektivt oppdatere statistikk, ikke bare for indekser og også på forskjellige måter (helt eller delvis)
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)
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:
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
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
søk etter dupliserte indekser (fullstendige og ufullstendige, som enten er litt forskjellige eller bare forskjellige i de inkluderte kolonnene)
Siden SQLIndexManager kun brukes for MS SQL Server DBMS, må dette gjenspeiles i navnet, for eksempel som følger: SQLIndexManager for MS SQL Server
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: