forked from bulentgucuk/DBA-Scripts
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathCollect Index Usage Statistics.sql
More file actions
338 lines (307 loc) · 10.9 KB
/
Collect Index Usage Statistics.sql
File metadata and controls
338 lines (307 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
-----------------------------------------------------------
-- Name: CollectIndexUsageStats.sql
--
-- Description: Collects index usage statistics over time.
-- Schedule a job to run dbo.CollectIndexUsageStats once a
-- day after midnight to collect daily Index Usage Stats
-- The job named A DBA Collect Index Daily Usage Statistics
-- does what it needs
-----------------------------------------------------------
USE [AdminTools];
GO
-- stores cumulative data from sys.dm_db_index_usage_stats DMV
CREATE TABLE [dbo].[IndexUsageStats_LastCumulative] (
[ServerNameID] [int] NOT NULL,
[DatabaseID] [smallint] NOT NULL,
[ObjectID] [int] NOT NULL,
[IndexID] [int] NOT NULL,
[LoadTime] [datetime2](0) NOT NULL,
[User_Seeks] [bigint] NOT NULL,
[User_Scans] [bigint] NOT NULL,
[User_Lookups] [bigint] NOT NULL,
[User_Updates] [bigint] NOT NULL,
[System_Seeks] [bigint] NOT NULL,
[System_Scans] [bigint] NOT NULL,
[System_Lookups] [bigint] NOT NULL,
[System_Updates] [bigint] NOT NULL,
CONSTRAINT [PK_IUS_C] PRIMARY KEY CLUSTERED ([ServerNameID],[DatabaseID],[ObjectID],[IndexID])
);
GO
-- used for Server/DB/Schema/Table/Index name mapping
CREATE TABLE [dbo].[Names] (
[ID] [int] IDENTITY(1,1) NOT NULL,
[Value] [nvarchar](260) NOT NULL,
CONSTRAINT [PK_Names] PRIMARY KEY CLUSTERED ([ID])
);
GO
-- stores historical usage statistics
CREATE TABLE [dbo].[IndexUsageStats] (
[StatsDate] [datetime2](0) NOT NULL,
[ServerNameID] [int] NOT NULL,
[DatabaseID] [smallint] NOT NULL,
[ObjectID] [int] NOT NULL,
[IndexID] [int] NOT NULL,
[DatabaseNameID] [int] NOT NULL,
[SchemaNameID] [int] NOT NULL,
[TableNameID] [int] NOT NULL,
[IndexNameID] [int] NULL,
[User_Seeks] [bigint] NOT NULL,
[User_Scans] [bigint] NOT NULL,
[User_Lookups] [bigint] NOT NULL,
[User_Updates] [bigint] NOT NULL,
[System_Seeks] [bigint] NOT NULL,
[System_Scans] [bigint] NOT NULL,
[System_Lookups] [bigint] NOT NULL,
[System_Updates] [bigint] NOT NULL,
CONSTRAINT [PK_IUS] PRIMARY KEY CLUSTERED ([StatsDate],[ServerNameID],[DatabaseID],[ObjectID],[IndexID]),
CONSTRAINT [FK_IUS_Names_DB] FOREIGN KEY([DatabaseNameID]) REFERENCES [dbo].[Names] ([ID]),
CONSTRAINT [FK_IUS_Names_Index] FOREIGN KEY([IndexNameID]) REFERENCES [dbo].[Names] ([ID]),
CONSTRAINT [FK_IUS_Names_Schema] FOREIGN KEY([SchemaNameID]) REFERENCES [dbo].[Names] ([ID]),
CONSTRAINT [FK_IUS_Names_Table] FOREIGN KEY([TableNameID]) REFERENCES [dbo].[Names] ([ID]),
CONSTRAINT [CK_IUS_PositiveValues] CHECK ([User_Seeks]>=(0) AND [User_Scans]>=(0) AND [user_Lookups]>=(0)
AND [user_updates]>=(0) AND [system_seeks]>=(0) AND [system_scans]>=(0) AND [system_lookups]>=(0)
AND [system_updates]>=(0))
);
GO
-- collects usage statistics
-- I run this once daily (can be run more often if you like)
CREATE PROCEDURE [dbo].[CollectIndexUsageStats]
AS
BEGIN
BEGIN TRY
SET NOCOUNT ON;
-- get current stats for all online databases
SELECT database_id, name
INTO #dblist
FROM sys.databases
WHERE [state] = 0
AND database_id != 2; -- skip TempDB
CREATE TABLE #t (
StatsDate DATETIME2(0),
ServerName SYSNAME,
DatabaseID SMALLINT,
ObjectID INT,
IndexID INT,
DatabaseName SYSNAME,
SchemaName SYSNAME,
TableName SYSNAME,
IndexName SYSNAME NULL,
User_Seeks BIGINT,
User_Scans BIGINT,
User_Lookups BIGINT,
User_Updates BIGINT,
System_Seeks BIGINT,
System_Scans BIGINT,
System_Lookups BIGINT,
System_Updates BIGINT
);
DECLARE @DBID INT;
DECLARE @DBNAME SYSNAME;
DECLARE @Qry NVARCHAR(2000);
-- iterate through each DB, generate & run query
WHILE (SELECT COUNT(*) FROM #dblist) > 0
BEGIN
SELECT TOP (1) @DBID=database_id, @DBNAME=[name]
FROM #dblist ORDER BY database_id;
SET @Qry = '
INSERT INTO #t
SELECT
SYSDATETIME() AS StatsDate,
@@SERVERNAME AS ServerName,
s.database_id AS DatabaseID,
s.object_id AS ObjectID,
s.index_id AS IndexID,
''' + @DBNAME + ''' AS DatabaseName,
c.name AS SchemaName,
o.name AS TableName,
i.name AS IndexName,
s.user_seeks,
s.user_scans,
s.user_lookups,
s.user_updates,
s.system_seeks,
s.system_scans,
s.system_lookups,
s.system_updates
FROM sys.dm_db_index_usage_stats s
INNER JOIN ' + @DBNAME + '.sys.objects o ON s.object_id = o.object_id
INNER JOIN ' + @DBNAME + '.sys.schemas c ON o.schema_id = c.schema_id
INNER JOIN ' + @DBNAME + '.sys.indexes i ON s.object_id = i.object_id and s.index_id = i.index_id
WHERE s.database_id = ' + CONVERT(NVARCHAR,@DBID) + ';
';
EXEC sp_executesql @Qry;
DELETE FROM #dblist WHERE database_id = @DBID;
END -- db while loop
DROP TABLE #DBList;
BEGIN TRAN;
-- create ids for Server Name by inserting new ones into dbo.Names
INSERT INTO AdminTools.dbo.Names (Value)
SELECT DISTINCT RTRIM(LTRIM(t.ServerName)) AS ServerName
FROM #t t
LEFT JOIN AdminTools.dbo.Names n ON t.ServerName = n.Value
WHERE n.ID IS NULL AND t.ServerName IS NOT NULL
ORDER BY RTRIM(LTRIM(t.ServerName));
-- same as above for DatabaseName
INSERT INTO AdminTools.dbo.Names (Value)
SELECT DISTINCT RTRIM(LTRIM(t.DatabaseName)) AS DatabaseName
FROM #t t
LEFT JOIN AdminTools.dbo.Names n ON t.DatabaseName = n.Value
WHERE n.ID IS NULL AND t.DatabaseName IS NOT NULL
ORDER BY RTRIM(LTRIM(t.DatabaseName));
-- SchemaName
INSERT INTO AdminTools.dbo.Names (Value)
SELECT DISTINCT RTRIM(LTRIM(t.SchemaName)) AS SchemaName
FROM #t t
LEFT JOIN AdminTools.dbo.Names n ON t.SchemaName = n.Value
WHERE n.ID IS NULL AND t.SchemaName IS NOT NULL
ORDER BY RTRIM(LTRIM(t.SchemaName));
-- TableName
INSERT INTO AdminTools.dbo.Names (Value)
SELECT DISTINCT RTRIM(LTRIM(t.TableName)) AS TableName
FROM #t t
LEFT JOIN AdminTools.dbo.Names n ON t.TableName = n.Value
WHERE n.ID IS NULL AND t.TableName IS NOT NULL
ORDER BY RTRIM(LTRIM(t.TableName));
-- IndexName
INSERT INTO AdminTools.dbo.Names (Value)
SELECT DISTINCT RTRIM(LTRIM(t.IndexName)) AS IndexName
FROM #t t
LEFT JOIN AdminTools.dbo.Names n ON t.IndexName = n.Value
WHERE n.ID IS NULL AND t.IndexName IS NOT NULL
ORDER BY RTRIM(LTRIM(t.IndexName));
-- Calculate Deltas
INSERT INTO AdminTools.dbo.IndexUsageStats (StatsDate, ServerNameID, DatabaseID, ObjectID,
IndexID, DatabaseNameID, SchemaNameID, TableNameID, IndexNameID, User_Seeks, User_Scans,
User_Lookups, User_Updates, System_Seeks, System_Scans, System_Lookups, System_Updates)
SELECT
t.StatsDate,
s.ID AS ServerNameID,
t.DatabaseID,
t.ObjectID,
t.IndexID,
d.ID AS DatabaseNameID,
c.ID AS SchemaNameID,
b.ID AS TableNameID,
i.ID AS IndexNameID,
CASE
-- if the previous cumulative value is greater than the current one, the server has been reset
-- just use the current value
WHEN t.User_Seeks - ISNULL(lc.User_Seeks,0) < 0 THEN t.User_Seeks
-- if the prev value is less than the current one, then subtract to get the delta
ELSE t.User_Seeks - ISNULL(lc.User_Seeks,0)
END AS User_Seeks,
CASE
WHEN t.User_Scans - ISNULL(lc.User_Scans,0) < 0 THEN t.User_Scans
ELSE t.User_Scans - ISNULL(lc.User_Scans,0)
END AS User_Scans,
CASE
WHEN t.User_Lookups - ISNULL(lc.User_Lookups,0) < 0 THEN t.User_Lookups
ELSE t.User_Lookups - ISNULL(lc.User_Lookups,0)
END AS User_Lookups,
CASE
WHEN t.User_Updates - ISNULL(lc.User_Updates,0) < 0 THEN t.User_Updates
ELSE t.User_Updates - ISNULL(lc.User_Updates,0)
END AS User_Updates,
CASE
WHEN t.System_Seeks - ISNULL(lc.System_Seeks,0) < 0 THEN t.System_Seeks
ELSE t.System_Seeks - ISNULL(lc.System_Seeks,0)
END AS System_Seeks,
CASE
WHEN t.System_Scans - ISNULL(lc.System_Scans,0) < 0 THEN t.System_Scans
ELSE t.System_Scans - ISNULL(lc.System_Scans,0)
END AS System_Scans,
CASE
WHEN t.System_Lookups - ISNULL(lc.System_Lookups,0) < 0 THEN t.System_Lookups
ELSE t.System_Lookups - ISNULL(lc.System_Lookups,0)
END AS System_Lookups,
CASE
WHEN t.System_Updates - ISNULL(lc.System_Updates,0) < 0 THEN t.System_Updates
ELSE t.System_Updates - ISNULL(lc.System_Updates,0)
END AS System_Updates
FROM #t t
INNER JOIN AdminTools.dbo.Names s ON t.ServerName = s.Value
INNER JOIN AdminTools.dbo.Names d ON t.DatabaseName = d.Value
INNER JOIN AdminTools.dbo.Names c ON t.SchemaName = c.Value
INNER JOIN AdminTools.dbo.Names b ON t.TableName = b.Value
LEFT JOIN AdminTools.dbo.Names i ON t.IndexName = i.Value
LEFT JOIN AdminTools.dbo.IndexUsageStats_LastCumulative lc
ON s.ID = lc.ServerNameID
AND t.DatabaseID = lc.DatabaseID
AND t.ObjectID = lc.ObjectID
AND t.IndexID = lc.IndexID
ORDER BY StatsDate, ServerName, DatabaseID, ObjectID, IndexID;
-- Update last cumulative values with the current ones
MERGE INTO AdminTools.dbo.IndexUsageStats_LastCumulative lc
USING #t t
INNER JOIN AdminTools.dbo.Names s ON t.ServerName = s.Value
ON s.ID = lc.ServerNameID
AND t.DatabaseID = lc.DatabaseID
AND t.ObjectID = lc.ObjectID
AND t.IndexID = lc.IndexID
WHEN MATCHED THEN
UPDATE SET
lc.LoadTime = t.StatsDate,
lc.User_Seeks = t.User_Seeks,
lc.User_Scans = t.User_Scans,
lc.User_Lookups = t.User_Lookups,
lc.User_Updates = t.User_Updates,
lc.System_Seeks = t.System_Seeks,
lc.System_Scans = t.System_Scans,
lc.System_Lookups = t.System_Lookups,
lc.System_Updates = t.System_Updates
WHEN NOT MATCHED BY TARGET THEN
INSERT (ServerNameID, DatabaseID, ObjectID, IndexID, LoadTime, User_Seeks, User_Scans,
User_Lookups, User_Updates, System_Seeks, System_Scans,
System_Lookups, System_Updates)
VALUES (s.ID, t.DatabaseID, t.ObjectID, t.IndexID, t.StatsDate, t.User_Seeks, t.User_Scans,
t.User_Lookups, t.User_Updates, t.System_Seeks, t.System_Scans,
t.System_Lookups, t.System_Updates)
WHEN NOT MATCHED BY SOURCE
THEN DELETE;
COMMIT TRAN;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRAN;
DECLARE @ErrorNumber INT;
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;
DECLARE @ErrorProcedure NVARCHAR(126);
DECLARE @ErrorLine INT;
DECLARE @ErrorMessage NVARCHAR(2048);
SELECT @ErrorNumber = ERROR_NUMBER(),
@ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE(),
@ErrorProcedure = ERROR_PROCEDURE(),
@ErrorLine = ERROR_LINE(),
@ErrorMessage = ERROR_MESSAGE();
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState);
END CATCH
END
GO
-- displays usage statistics
CREATE VIEW [dbo].[vw_IndexUsageStats]
AS
SELECT
s.StatsDate,
vn.Value AS ServerName,
dbn.Value AS DatabaseName,
sn.Value AS SchemaName,
tn.Value AS TableName,
dn.Value AS IndexName,
s.IndexID,
s.User_Seeks,
s.User_Scans,
s.User_Lookups,
s.User_Updates,
s.System_Seeks,
s.System_Scans,
s.System_Lookups,
s.System_Updates
FROM dbo.IndexUsageStats s
INNER JOIN dbo.Names vn ON s.ServerNameID = vn.ID
INNER JOIN dbo.Names dbn ON s.DatabaseNameID = dbn.ID
INNER JOIN dbo.Names sn ON s.SchemaNameID = sn.ID
INNER JOIN dbo.Names tn ON s.TableNameID = tn.ID
LEFT JOIN dbo.Names dn ON s.IndexNameID = dn.ID;
GO