forked from yochananrachamim/AzureSQL
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAzureSQLMaintenance.txt
299 lines (251 loc) · 11.9 KB
/
AzureSQLMaintenance.txt
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
/* Azure SQL Maintenance - Maintenance script for Azure SQL Database */
/* This script provided AS IS, Please review the code before executing this on production environment */
/* For any issue or suggestion please email to: [email protected] */
/*
***********************************************
Current Version Date: 2019-06-24
***********************************************
Change Log:
2017-12-12: fix total indexes reported to the output window. functionality was not impacted by this issue.
2018-08-09: add filter to skip update statistics if it updated by using index rebuild.
2019-02-06: disable online operations where its not supported by server edition
add report for failure when error occurs during executions
2019-06-24: avoid running online operation when text/ntext or image file type used in the table
special thanks for Haden Kingsland (@theflyingdba) for sharing the code change for these types.
+ avoid rebuild twice when lob data exists due to duplicate row in index phisical stats dmv.
*/
if object_id('AzureSQLMaintenance') is null
exec('create procedure AzureSQLMaintenance as /*dummy procedure body*/ select 1;')
GO
ALTER Procedure [dbo].[AzureSQLMaintenance]
(
@operation nvarchar(10) = null,
@mode nvarchar(10) = 'smart',
@LogToTable bit = 0
)
as
begin
set nocount on
declare @msg nvarchar(max);
declare @minPageCountForIndex int = 40;
declare @OperationTime datetime2 = sysdatetime();
declare @KeepXOperationInLog int =3;
declare @ScriptHasAnError int = 0;
/* make sure parameters selected correctly */
set @operation = lower(@operation)
set @mode = lower(@mode)
if @mode not in ('smart','dummy')
set @mode = 'smart'
if @operation not in ('index','statistics','all') or @operation is null
begin
raiserror('@operation (varchar(10)) [mandatory]',0,0)
raiserror(' Select operation to perform:',0,0)
raiserror(' "index" to perform index maintenance',0,0)
raiserror(' "statistics" to perform statistics maintenance',0,0)
raiserror(' "all" to perform indexes and statistics maintenance',0,0)
raiserror(' ',0,0)
raiserror('@mode(varchar(10)) [optional]',0,0)
raiserror(' optionaly you can supply second parameter for operation mode: ',0,0)
raiserror(' "smart" (Default) using smart decition about what index or stats should be touched.',0,0)
raiserror(' "dummy" going through all indexes and statistics regardless thier modifications or fragmentation.',0,0)
raiserror(' ',0,0)
raiserror('@LogToTable(bit) [optional]',0,0)
raiserror(' Logging option: @LogToTable(bit)',0,0)
raiserror(' 0 - (Default) do not log operation to table',0,0)
raiserror(' 1 - log operation to table',0,0)
raiserror(' for logging option only 3 last execution will be kept by default. this can be changed by easily in the procedure body.',0,0)
raiserror(' Log table will be created automatically if not exists.',0,0)
end
else
begin
/*Write operation parameters*/
raiserror('-----------------------',0,0)
set @msg = 'set operation = ' + @operation;
raiserror(@msg,0,0)
set @msg = 'set mode = ' + @mode;
raiserror(@msg,0,0)
set @msg = 'set LogToTable = ' + cast(@LogToTable as varchar(1));
raiserror(@msg,0,0)
raiserror('-----------------------',0,0)
end
/* Prepare Log Table */
if object_id('AzureSQLMaintenanceLog') is null
begin
create table AzureSQLMaintenanceLog (id bigint primary key identity(1,1), OperationTime datetime2, command varchar(4000),ExtraInfo varchar(4000), StartTime datetime2, EndTime datetime2, StatusMessage varchar(1000));
end
if @LogToTable=1 insert into AzureSQLMaintenanceLog values(@OperationTime,null,null,sysdatetime(),sysdatetime(),'Starting operation: Operation=' +@operation + ' Mode=' + @mode + ' Keep log for last ' + cast(@KeepXOperationInLog as varchar(10)) + ' operations' )
create table #cmdQueue (txtCMD nvarchar(max),ExtraInfo varchar(max))
if @operation in('index','all')
begin
raiserror('Get index information...(wait)',0,0) with nowait;
/* Get Index Information */
select
i.[object_id]
,ObjectSchema = OBJECT_SCHEMA_NAME(i.object_id)
,ObjectName = object_name(i.object_id)
,IndexName = idxs.name
,i.avg_fragmentation_in_percent
,i.page_count
,i.index_id
,i.partition_number
,i.index_type_desc
,i.avg_page_space_used_in_percent
,i.record_count
,i.ghost_record_count
,i.forwarded_record_count
,null as OnlineOpIsNotSupported
into #idxBefore
from sys.dm_db_index_physical_stats(DB_ID(),NULL, NULL, NULL ,'limited') i
left join sys.indexes idxs on i.object_id = idxs.object_id and i.index_id = idxs.index_id
where idxs.type in (1/*Clustered index*/,2/*NonClustered index*/) /*Avoid HEAPS*/
and alloc_unit_type_desc = 'IN_ROW_DATA' /*avoid LOB_DATA or ROW_OVERFLOW_DATA*/
order by i.avg_fragmentation_in_percent desc, page_count desc
-- mark indexes XML,spatial and columnstore not to run online update
update #idxBefore set OnlineOpIsNotSupported=1 where [object_id] in (select [object_id] from #idxBefore where index_id >=1000)
-- mark clustered indexes for tables with 'text','ntext','image' to rebuild offline
update #idxBefore set OnlineOpIsNotSupported=1
where index_id=1 /*clustered*/ and [object_id] in (
select object_id
from sys.columns c join sys.types t on c.user_type_id = t.user_type_id
where t.name in ('text','ntext','image')
)
-- do all as offline for box edition that does not support online
update #idxBefore set OnlineOpIsNotSupported=1
where /* Editions that does not support online operation in case this has been used with on-prem server */
convert(varchar(100),serverproperty('Edition')) like '%Express%'
or convert(varchar(100),serverproperty('Edition')) like '%Standard%'
or convert(varchar(100),serverproperty('Edition')) like '%Web%'
raiserror('---------------------------------------',0,0) with nowait
raiserror('Index Information:',0,0) with nowait
raiserror('---------------------------------------',0,0) with nowait
select @msg = count(*) from #idxBefore
set @msg = 'Total Indexes: ' + @msg
raiserror(@msg,0,0) with nowait
select @msg = avg(avg_fragmentation_in_percent) from #idxBefore where page_count>@minPageCountForIndex
set @msg = 'Average Fragmentation: ' + @msg
raiserror(@msg,0,0) with nowait
select @msg = sum(iif(avg_fragmentation_in_percent>=5 and page_count>@minPageCountForIndex,1,0)) from #idxBefore
set @msg = 'Fragmented Indexes: ' + @msg
raiserror(@msg,0,0) with nowait
raiserror('---------------------------------------',0,0) with nowait
/* create queue for update indexes */
insert into #cmdQueue
select
txtCMD =
case when avg_fragmentation_in_percent>5 and avg_fragmentation_in_percent<30 and @mode = 'smart' then
'ALTER INDEX [' + IndexName + '] ON [' + ObjectSchema + '].[' + ObjectName + '] REORGANIZE;'
when OnlineOpIsNotSupported=1 then
'ALTER INDEX [' + IndexName + '] ON [' + ObjectSchema + '].[' + ObjectName + '] REBUILD WITH(ONLINE=OFF,MAXDOP=1);'
else
'ALTER INDEX [' + IndexName + '] ON [' + ObjectSchema + '].[' + ObjectName + '] REBUILD WITH(ONLINE=ON,MAXDOP=1);'
end
, ExtraInfo = 'Current fragmentation: ' + format(avg_fragmentation_in_percent/100,'p')
from #idxBefore
where
(
page_count> @minPageCountForIndex and /* not small tables */
avg_fragmentation_in_percent>=5
)
or
(
@mode ='dummy'
)
end
if @operation in('statistics','all')
begin
/*Gets Stats for database*/
raiserror('Get statistics information...',0,0) with nowait;
select
ObjectSchema = OBJECT_SCHEMA_NAME(s.object_id)
,ObjectName = object_name(s.object_id)
,StatsName = s.name
,sp.last_updated
,sp.rows
,sp.rows_sampled
,sp.modification_counter
into #statsBefore
from sys.stats s cross apply sys.dm_db_stats_properties(s.object_id,s.stats_id) sp
where OBJECT_SCHEMA_NAME(s.object_id) != 'sys' and /*Modified stats or Dummy mode*/(sp.modification_counter>0 or @mode='dummy')
order by sp.last_updated asc
/*Remove statistics if it is handled by index rebuild / reorginize
I am removing statistics based on existance on the index in the list because for indexes with <5% changes we do not apply
any action - therefore we might decide to update statistics */
if @operation = 'all'
delete _stats
from #statsBefore _stats
join #idxBefore _idx
on _idx.ObjectSchema = _stats.ObjectSchema
and _idx.ObjectName = _stats.ObjectName
and _idx.IndexName = _stats.StatsName
raiserror('---------------------------------------',0,0) with nowait
raiserror('Statistics Information:',0,0) with nowait
raiserror('---------------------------------------',0,0) with nowait
select @msg = sum(modification_counter) from #statsBefore
set @msg = 'Total Modifications: ' + @msg
raiserror(@msg,0,0) with nowait
select @msg = sum(iif(modification_counter>0,1,0)) from #statsBefore
set @msg = 'Modified Statistics: ' + @msg
raiserror(@msg,0,0) with nowait
raiserror('---------------------------------------',0,0) with nowait
/* create queue for update stats */
insert into #cmdQueue
select
txtCMD = 'UPDATE STATISTICS [' + ObjectSchema + '].[' + ObjectName + '] (['+ StatsName +']) WITH FULLSCAN;'
, ExtraInfo = '#rows:' + cast([rows] as varchar(100)) + ' #modifications:' + cast(modification_counter as varchar(100)) + ' modification percent: ' + format((1.0 * modification_counter/ rows ),'p')
from #statsBefore
end
if @operation in('statistics','index','all')
begin
/* iterate through all stats */
raiserror('Start executing commands...',0,0) with nowait
declare @SQLCMD nvarchar(max);
declare @ExtraInfo nvarchar(max);
declare @T table(txtCMD nvarchar(max),ExtraInfo nvarchar(max));
while exists(select * from #cmdQueue)
begin
delete top (1) from #cmdQueue output deleted.* into @T;
select top (1) @SQLCMD = txtCMD, @ExtraInfo=ExtraInfo from @T
raiserror(@SQLCMD,0,0) with nowait
if @LogToTable=1 insert into AzureSQLMaintenanceLog values(@OperationTime,@SQLCMD,@ExtraInfo,sysdatetime(),null,'Started')
begin try
exec(@SQLCMD)
if @LogToTable=1 update AzureSQLMaintenanceLog set EndTime = sysdatetime(), StatusMessage = 'Succeeded' where id=SCOPE_IDENTITY()
end try
begin catch
set @ScriptHasAnError=1;
set @msg = 'FAILED : ' + CAST(ERROR_NUMBER() AS VARCHAR(50)) + ERROR_MESSAGE();
raiserror(@msg,0,0) with nowait
if @LogToTable=1 update AzureSQLMaintenanceLog set EndTime = sysdatetime(), StatusMessage = @msg where id=SCOPE_IDENTITY()
end catch
delete from @T
end
end
/* Clean old records from log table */
if @LogToTable=1
begin
delete from AzureSQLMaintenanceLog
from
AzureSQLMaintenanceLog L join
(select distinct OperationTime from AzureSQLMaintenanceLog order by OperationTime desc offset @KeepXOperationInLog rows) F
ON L.OperationTime = F.OperationTime
insert into AzureSQLMaintenanceLog values(@OperationTime,null,cast(@@rowcount as varchar(100))+ ' rows purged from log table because number of operations to keep is set to: ' + cast( @KeepXOperationInLog as varchar(100)),sysdatetime(),sysdatetime(),'Cleanup Log Table')
end
if @ScriptHasAnError=0 raiserror('Done',0,0)
if @LogToTable=1 insert into AzureSQLMaintenanceLog values(@OperationTime,null,null,sysdatetime(),sysdatetime(),'End of operation')
if @ScriptHasAnError=1 raiserror('Script has errors - please review the log.',16,1)
end
GO
print 'Execute AzureSQLMaintenance to get help'
/*
Examples
1. run through all indexes and statistic and take smart decision about steps taken for each object
exec AzureSQLMaintenance 'all'
1.1 add log to table
exec AzureSQLMaintenance 'all', @LogToTable=1
2. run through all indexes and statistic with no limitation (event non modified object will be rebuild or updated)
exec AzureSQLMaintenance 'all','dummy'
3. run smart maintenance only for statistics
exec AzureSQLMaintenance 'statistics'
4. run smart maintenance only for indexes
exec AzureSQLMaintenance 'index'
*/