tisdag 24 maj 2016

Används din databas?

När man träffa kunder så är det vanligt att man inte riktigt vet om sina databaser faktiskt används eller inte och i vilken utsträckning. För att få en överblick av hur det ser ut så brukar jag schemalägga ett jobb som samlar information från DMV´en sys.dm_db_index_usage_stats. Denna dynamic viewe visa löpande information om hur index och tabeller (HEEP´s) används. Jag anser att detta bör kunna ge en bra bild på hur din databas används. Du kan även använda denna information för att se om index faktiskt kan tas bort.

Börja med att skapa en tabell i valfri databas för att logga datat över tiden.

CREATE TABLE [dbo].[IndexUsage](
            [CollectedTime] [datetime] NULL,
            [LastCollectedTime] [datetime] NULL,
            [DatabaseName] [varchar](256) NULL,
            [SchemaName] [varchar](50) NULL,
            [TableName] [varchar](256) NULL,
            [IndexName] [varchar](256) NULL,
            [user_seeks] [bigint] NULL,
            [init_user_seeks] [bigint] NULL,
            [user_scans] [bigint] NULL,
            [init_user_scans] [bigint] NULL,
            [user_updates] [bigint] NULL,
            [init_user_updates] [bigint] NULL,
            [row_count] [bigint] NULL,
            [init_row_count] [bigint] NULL,
            [size_in_mb] [decimal](18, 2) NULL,
            [init_size_in_mb] [decimal](18, 2) NULL
) ON [PRIMARY]

Sedan använder vi sp_MSForEachDB för att köra mot samtliga databaser. Det är två olika skript jag slår ihop i en union. Det ena är för att hämta användande från  sys.dm_db_index_usage_stats och det andra är för att hämta samtliga index från sys.indexes, även dom som då aldrig används. Kör följande kod via ett SQL jobb.

CREATE TABLE #Temp1 (
CollectedTime DATETIME,
DatabaseName VARCHAR(256),
SchemaName VARCHAR(50),
TableName VARCHAR(256),
IndexName VARCHAR(256),
user_seeks BIGINT,
user_scans BIGINT,
user_updates BIGINT,
row_count BIGINT,
size_in_mb DECIMAL(18,2)
)

EXEC sp_MSForEachDB 'USE [?];
WITH CTE AS (
SELECT d.name as DatabaseName,
OBJECT_SCHEMA_NAME(i.object_id) AS SchemaName,
t.name AS TableName,
ISNULL(i.name, ''HEEP'') AS IndexName,
ISNULL(s.user_seeks, 0) as user_seeks,
ISNULL(s.user_scans, 0) as user_scans,
ISNULL(s.user_updates, 0) as user_updates,
ps.row_count,
CAST((ps.reserved_page_count * 8)/1024. as decimal(12,2)) AS size_in_mb
FROM sys.dm_db_index_usage_stats AS s
            INNER JOIN sys.databases d
                         ON d.database_id = s.database_id
            INNER JOIN sys.indexes as i
                         ON s.object_id = i.object_id
                         AND s.index_id = i.index_id
            INNER JOIN sys.tables t
                         ON s.object_id = t.object_id
            INNER JOIN sys.dm_db_partition_stats ps
                         ON i.object_id = ps.object_id
                         AND i.index_id = ps.index_id
WHERE objectproperty(s.object_id,''IsUserTable'') = 1 AND s.database_id = DB_ID()
UNION
SELECT DB_NAME() AS DatbaseName,
OBJECT_SCHEMA_NAME(i.object_id) AS SchemaName,
OBJECT_NAME(i.object_id) AS TableName,
ISNULL(i.name, ''HEEP'') AS IndexName,
0 as user_seeks,
0 as user_scans,
0 as user_updates,
ps.row_count,
CAST((ps.reserved_page_count * 8)/1024. as decimal(12,2)) as size_in_mb
FROM sys.indexes i 
            INNER JOIN sys.objects o
                         ON i.object_id = o.object_id
            LEFT OUTER JOIN sys.dm_db_index_usage_stats s
                         ON s.object_id = i.object_id
                         AND i.index_id = s.index_id
            INNER JOIN sys.dm_db_partition_stats ps
                         ON i.object_id = ps.object_id AND i.index_id = ps.index_id
WHERE OBJECTPROPERTY(i.object_id, ''IsUserTable'') = 1
AND s.object_id IS NULL
)
INSERT INTO #Temp1
SELECT GETDATE(), DatabaseName, SchemaName, TableName, IndexName,
SUM(user_seeks) as user_seeks, SUM(user_scans) as user_scans, SUM(user_updates) as user_updates, SUM(row_count) as row_count, SUM(size_in_mb) as size_in_mb
FROM CTE
GROUP BY DatabaseName, SchemaName, TableName, IndexName
ORDER BY DatabaseName, TableName'

GO

MERGE INTO dbo.IndexUsage AS trg
USING #Temp1 AS src
            ON trg.DatabaseName = src.DatabaseName
            AND trg.SchemaName = src.SchemaName
            AND trg.TableName = src.TableName
            AND trg.IndexName = src.IndexName
WHEN MATCHED THEN
UPDATE SET
            trg.LastCollectedTime = src.CollectedTime,
            trg.user_seeks = src.user_seeks,
            trg.user_scans = src.user_scans,
            trg.user_updates = src.user_updates,
            trg.row_count = src.row_count,
            trg.size_in_mb = src.size_in_mb
WHEN NOT MATCHED BY TARGET THEN
INSERT (CollectedTime, DatabaseName, SchemaName, TableName, IndexName, init_user_seeks, init_user_scans, init_user_updates, init_row_count, init_size_in_mb)
VALUES (src.CollectedTime, src.DatabaseName, src.SchemaName, src.TableName, src.IndexName, src.user_seeks, src.user_scans, src.user_updates, src.row_count, src.size_in_mb);


DROP TABLE #Temp1

För att analysera datat kan man tex använda dessafrågor för att se förändring sedan starten.

SELECT CollectedTime, LastCollectedTime, DatabaseName, SchemaName, TableName, IndexName
,(user_seeks - init_user_seeks) AS user_seeks
,(user_scans - init_user_scans) AS user_scans
,(user_updates - init_user_updates) AS user_updates
,(row_count - init_row_count) AS row_count
,(size_in_mb - init_size_in_mb) AS size_in_mb
FROM [IndexUsage]
ORDER BY user_seeks DESC, user_scans DESC, user_updates DESC

WITH CTE AS (
SELECT DatabaseName, SchemaName, TableName, IndexName
,(User_seeks + user_scans + user_updates) as Activity
FROM IndexUsage
WHERE DatabaseName NOT IN ('Master','Tempdb','Model','MSDB')
)
SELECT DatabaseName, SchemaName, TableName, IndexName
,SUM(Activity) as Activity
FROM CTE
GROUP BY DatabaseName, SchemaName, TableName, IndexName

ORDER BY DatabaseName, SchemaName, TableName, IndexName