{"id":355080,"date":"2024-05-20T23:01:19","date_gmt":"2024-05-20T23:01:19","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=355080"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=355080","title":{"rendered":"<span>MSSQL: Table Rebuild and Reorg in highload 24\/7 Environments<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>How do you deal with index fragmentation if your SQL server is working in high load environment with 24\/7 workload without any maintenance window? What are the best practices for index rebuild and index reorganize? What is better? What is possible if you have only Standard Edition on some servers? But first, let&#8217;s debunk few myths.<\/p>\n<p><strong>Myth 1.<\/strong> We use SSD (or super duper storage), so we should not care about the fragmentation. False. Index rebuild compactifies a table, with compression it makes it sometimes several times smaller, improving the cache hits ratio and overall performance (this happens even without compression). <\/p>\n<p><strong>Myth 2<\/strong>. Index rebuild shorten SSD lifespan. False. One extra write cycle is nothing for the modern SSDs. If your tempdb is on SSD\/NVMe, it is under <strong>much<\/strong> harder stress than data disks.<\/p>\n<p><strong>Myth 3.<\/strong> On Enterprise Edition there is a good option: ONLINE=ON, so I just create a script with all tables and go ahead. False. There are tons of potential problems created by INDEX REBUILD even with ONLINE and RESUMABLE ON &#8212; so never run index rebuilds without controlling the process.<\/p>\n<p>Finally, we will tackle the REBUILD vs REORGANIZE subject and what is possible to achieve if you have only Standard Edition.<\/p>\n<h2>Enterprise Edition: Index Rebuild with ONLINE=ON and RESUMABLE=ON<\/h2>\n<h2>RESUMABLE index rebuild &#8212; theory<\/h2>\n<p>This is from where I started: up to 100Tb per server, AlwaysOn with 2-3 readable replicas, local SSDs \/ NVMe with up to 200K iops, Enterprise Edition.<\/p>\n<p>Reminder about RESUMABLE=ON. With RESUMABLE=ON you can always pause an operation using (killing the connection that is performing the rebuild, also puts rebuild in a paused state):<\/p>\n<pre><code class=\"sql\">ALTER INDEX ...  PAUSE<\/code><\/pre>\n<p>You can list indexes in resumable state using the following query:<\/p>\n<pre><code class=\"sql\">SELECT total_execution_time, percent_complete, name,state_desc,   last_pause_time,page_count   FROM sys.index_resumable_operations;<\/code><\/pre>\n<p>When rebuild is suspended, such operation even can <em>fail over<\/em> to another server! Awesome!<\/p>\n<p>Also, column <em>percent_complete shows the overall process<\/em>. (for indexes with WHERE clause, however, this value is not correct)<\/p>\n<p>Using <strong>ALTER INDEX &#8230; RESUME<\/strong> we can resume the operation (in fact, <strong>RESUME<\/strong> is just a syntax sugar &#8212; you can resume operation by simply executing the <strong>REBUILD<\/strong> command again instead of resume, just use the same options &#8212; for example, you can&#8217;t change MAXDOP on the fly). Using <strong>ALTER INDEX &#8230; ABORT<\/strong> you can kill the index rebuild.<\/p>\n<p>In the <strong>RESUMABLE<\/strong> mode transactions are short, so you should not worry that LDF growth will run out of control.<\/p>\n<h2>First try, failed miserably<\/h2>\n<p>So, let&#8217;s generate a script with <strong>ALTER INDEX<\/strong> for all indexes in our database and run it. It would be for a few days, so let&#8217;s start it on Friday so we can check the progress on Monday.<\/p>\n<p>Time to close the laptop and go home. But why are support people running between workplaces? I hear they are discussing some &#171;slowdowns&#187;&#8230; Wait, what are these alerts about AlwaysOn Queue Length? is it somehow related to my script? Oops, this is more serious, people are yelling about locks, apps are unresponsive, timeouts. Wow, my Rebuild lis blocking other connections! Alert dashboard is all red and yellow! <\/p>\n<p>My boss is angry and telling me to stop this damn rebuild and never &#8212; your hear &#8212; never! Try it again! I want to save you from this, so let&#8217;s analyze all potential problems in advance. How can rebuild affect the production system?<\/p>\n<ul>\n<li>\n<p>High CPU (and IO)<\/p>\n<\/li>\n<li>\n<p>Locks!!! (Surprise, surprise. ONLINE=ON, we believed in you, and you&#8230;) <\/p>\n<\/li>\n<li>\n<p>LDF growth out of control<\/p>\n<\/li>\n<li>\n<p>Problems with <em>AlwaysOn Queue<\/em><\/p>\n<\/li>\n<\/ul>\n<h3>Throttling<\/h3>\n<p>Let&#8217;s start with CPU and IO. We did not specify <strong>MAXDOP<\/strong>, and this is important. Index rebuild with a default <strong>MAXDOP<\/strong> can monopolize server. This is why I always specify <strong>MAXDOP<\/strong>, I recommend the following values:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/722\/a38\/914\/722a38914144988afd502e07e595ef2f.png\" width=\"856\" height=\"436\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/722\/a38\/914\/722a38914144988afd502e07e595ef2f.png\"\/><\/figure>\n<p>Almost like in doom:<\/p>\n<ul>\n<li>\n<p><strong>MAXDOP=1<\/strong> &#8212; gently and slowly<\/p>\n<\/li>\n<li>\n<p><strong>MAXDOP=2<\/strong> &#8212; normal mode<\/p>\n<\/li>\n<li>\n<p><strong>MAXDOP=4<\/strong> &#8212; aggressive mode<\/p>\n<\/li>\n<li>\n<p>(unlimited) &#8212; NIGHTMARE!<\/p>\n<\/li>\n<\/ul>\n<p>Now it is much better, for example, with <strong>MAXDOP=2<\/strong> you will use 2 cores, while on a big production server you have a lot), but still there are many other problems.<\/p>\n<p>For example, in <strong>MAXDOP=4<\/strong> mode powerful server and populate the LDF file with 1Gb\/sec and faster, which means that in 10 minutes (typical time between transaction log backups) it can fill 600Gb in LDF, which is a lot. What is worse, 1Gb\/sec in LDF is 10 G-bits for <strong>AlwaysOn<\/strong> replication. 10 Gbit is not a big number for intra &#8212; datacenter replication, but what&#8217;s about remote datacenters used for the DR?<\/p>\n<p>Hence we do need to monitor free LDF size and percentage and <strong>AlwaysOn<\/strong> queue. If $db is a database, you can monitor it using the following query:<\/p>\n<pre><code class=\"sql\">USE [$db];   SELECT      convert(int,sum(size\/128.0)) AS CurrentSizeMB,       convert(int,sum(size\/128.0 - CAST(FILEPROPERTY(name, 'SpaceUsed') AS INT)\/128.0))        AS FreeSpaceMB,     (select sum(log_send_queue_size)+sum(redo_queue_size)        from sys.dm_hadr_database_replica_states        where database_id=DB_ID('$db')) as QueueLen,     (select isnull(max(case when redo_queue_size>0 then datediff(ss,last_commit_time, getdate()) else 0 end),0)       from sys.dm_hadr_database_replica_states        where database_id=DB_ID('$db')) as ReplicationDelayInSecs     FROM sys.database_files WHERE type=1<\/code><\/pre>\n<p>Conclusion: we can&#8217;t use sqlcmd or Management Studio or SQL server job to run rebuild in unattended mode. We do need to write a script with 2 threads &#8212; one will be running the actual command, while the other will check all the parameters and throttle the main thread if something goes out of control. <\/p>\n<p>What should be controlled? What thresholds should be defined? <\/p>\n<ul>\n<li>\n<p>CPU &#8212; it should not be too high<\/p>\n<\/li>\n<li>\n<p>LDF minimum percentage free<\/p>\n<\/li>\n<li>\n<p>And\/or maximum size allocated<\/p>\n<\/li>\n<li>\n<p>Maximum AlwaysOn queue length<\/p>\n<\/li>\n<li>\n<p>Locking<\/p>\n<\/li>\n<\/ul>\n<p>In reality I had to add few other conditions:<\/p>\n<ul>\n<li>\n<p>Deadline, to prevent work from continuing into late night or a weekend &#8212; this is just in case, to play safe, we don&#8217;t wont DBAs on duty to be woken up in the middle of the night (however, it is not possible with huge indexes &#8212; some operations take several days)<\/p>\n<\/li>\n<li>\n<p>Maximum size of indexes rebuilt daily (1Tb for example) &#8212; to prevent problems with QA\/Dev databases, which are synched using log shipping &#8216;replay&#8217;<\/p>\n<\/li>\n<li>\n<p>Custom throttling. In my case, 350 powershell lines which check job execution times of 20+ most important jobs and 15+ other critical parameters &#8212; to throttle the process or to even to panic and to abort an operation completely.<\/p>\n<\/li>\n<\/ul>\n<p>I wrote flexible parametrized script on PowerShell and i wanted to share it with you, it is non profit and open source: <a href=\"https:\/\/github.com\/tzimie\/GentleRebuild\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/tzimie\/GentleRebuild<\/a> (project site <a href=\"https:\/\/www.actionatdistance.com\/gentlerebuild\" rel=\"noopener noreferrer nofollow\">https:\/\/www.actionatdistance.com\/gentlerebuild<\/a> )<\/p>\n<p>This is how it looks like:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d8c\/0b4\/ae4\/d8c0b4ae4962ad16512844f5e2e5e96b.png\" alt=\"It is actually throttling because of the custom condition\" title=\"It is actually throttling because of the custom condition\" width=\"769\" height=\"224\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/d8c\/0b4\/ae4\/d8c0b4ae4962ad16512844f5e2e5e96b.png\"\/><\/p>\n<div><figcaption>It is actually throttling because of the custom condition<\/figcaption><\/div>\n<\/figure>\n<p>Wait, why am I using <strong>kill<\/strong> command instead of <strong>PAUSE<\/strong>? There are important reasons for that.<\/p>\n<h3>Locking<\/h3>\n<p>Despite the fact that we are using REBUILD with ONLINE=ON, locking is possible on both sides:<\/p>\n<p>  <em>useful workload &#8212;> INDEX REBUILD &#8212;> useful workload<\/em><\/p>\n<p>We don&#8217;t care about the right part. Our rebuild can wait. The only problem could be alert systems yelling about locks. Scripts runs a rebuild in a connection marked with <em>program_name=Rebuild<\/em>. So you can add (<em>WHERE &#8230; AND program_name not like &#8216;Rebuild%&#8217;<\/em>) to your alert system to ignore this process. <\/p>\n<p>The left part is more important. If we lock any process, we need to yield, execute <strong>PAUSE<\/strong>, and then, after a while, continue using <strong>RESUME<\/strong>.<\/p>\n<p>However, the biggest problem is when there are both sides of the lock at the same time. So when the rebuild process is locked (right side), at the same time there is a process waiting for rebuild. To yield we run the <strong>PAUSE<\/strong> command and &#8230; nothing happens, because the rebuild process is locked, and <strong>PAUSE<\/strong> tries to finish the last chunk before going into paused mode.<\/p>\n<p>This is where <strong>kill<\/strong> command helps us: we lose the very last work chunk (few seconds), but we yield quickly. After <strong>kill<\/strong> rebuild falls into <strong>PAUSED<\/strong> status and it can be resumed later.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1d5\/6ad\/d97\/1d56add97a81934e761d7d6028c1767b.png\" width=\"1085\" height=\"609\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/1d5\/6ad\/d97\/1d56add97a81934e761d7d6028c1767b.png\"\/><\/figure>\n<p>You can see how the script yields on the screenshot above. Why does ONLINE=ON cause locks? Most of the locks come from &#8216;TASK MANAGER&#8217; internal connections, doing automatic statistics updates. The locks that affect other processes are, based on my experience, mostly schema locks.<\/p>\n<p>In the custom module, you can define the reaction of the script to other processes being locked. For example, some background processes can wait longer, while for the interactive processes scripts should yield asap. This module is called VictimClassifier, and it is very useful for Standard Edition, where locking is inevitable.<\/p>\n<p>As a reminder, you should not worry about fragmentation levels in very small tables (page_count&lt;1000), they still show high fragmentation levels even after rebuild. Often it makes sense to rebuild tables with index fragmentation > 40%, however, indexes sometimes become significantly smaller on disk even for very low fragmentations levels. <\/p>\n<p>If you rebuild with COMPRESSION=PAGE\/ROW, you should not take fragmentation levels into  account because you need to change the compression anyway. Important warning: if there is a manipulation with partitions using ALTER TABLE&#8230; SWITCH PARTITION, tables on both &#8216;sides&#8217; should have the same compression! So be careful when you automatically apply compression to big tables leaving small tables (which participate in SWITCH PARTITION magic) unaffected. <\/p>\n<p>Don&#8217;t take a paused resumable state for granted. It is still affecting your system, even it is paused. This happens because SQL server has to track all changes to apply them to the &#8216;future&#8217; image of the index as well, so it modifies execution plans for queries, which do insert\/update\/delete into\/from that index. These operations become slower. In case of merge statement, it could be significantly slower. This is why the &#8216;panic&#8217; response from custom throttling module not only pauses the rebuild, but also aborts it.<\/p>\n<p>And yes, I learned all the above the hard way. In general, my experience was like this:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/17e\/0f8\/7e3\/17e0f87e320ee5ea89a560f4f4ee6d23.png\" width=\"623\" height=\"469\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/17e\/0f8\/7e3\/17e0f87e320ee5ea89a560f4f4ee6d23.png\"\/><\/figure>\n<p>Hopefully, this script will help you avoid all these troubles.<\/p>\n<p>Finally, some indexes don&#8217;t support RESUMABLE=ON and even ONLINE=ON. In such case check what is possible for the Standard Edition.<\/p>\n<h2>What&#8217;s about Standard Edition?<\/h2>\n<p>In the Standard Edition you can&#8217;t use RESUMABLE=ON and even ONLINE=ON. What is left? INDEX REORGANIZE or doing INDEX REBUILD as quickly as possible in a controlled manner using the script. <\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/58c\/53d\/691\/58c53d691a267c7757cdfddc9cbdea73.jpg\" alt=\"a man on a bridge and a train approaching\" title=\"a man on a bridge and a train approaching\" width=\"535\" height=\"356\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/58c\/53d\/691\/58c53d691a267c7757cdfddc9cbdea73.jpg\" data-blurred=\"true\"\/><\/p>\n<div><figcaption>a man on a bridge and a train approaching<\/figcaption><\/div>\n<\/figure>\n<p>The picture above is not random.<\/p>\n<p>So, all operations in SE are not resumable. Script <strong>GentleRebuild<\/strong> still tracks everything, including locking. But as operations are not interruptable, throttling occurs only between the operations (new index rebuild is not started until throttling indicates that everything is OK. There are 2 exceptions however &#8212; locking and custom throttling conditions, which can control &#8216;interruptive power&#8217; &#8212; <em>soft<\/em> throttling (between the operations), <em>hard<\/em> (interrupting current operation), and <em>panic<\/em> &#8212; aborts everything and exits.<\/p>\n<p>The most important is locking, and kill rolls the index rebuild back, which might take quite a while (hours!) still holding the lock. The rollback could be even longer than the operation itself!<\/p>\n<p>This is how I came to the analogy with a man on bridge. You walk by a bridge and a train approaching. There is no place for both of you, where to run? Backward or forward toward the train &#8212; but it would make sense if you had almost crossed the bridge. But for the non resumable index rebuild we even don&#8217;t have progress indicator in percent!<\/p>\n<h3>Getting the right estimation for the percent of work done<\/h3>\n<p>After several experiments, I found that in most cases the dependency between page_count of the original index and logical_reads in sys.dm_exec_requests is linear:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9ea\/7f9\/abd\/9ea7f9abd3f09f27cc19eda10ca14c33.png\" width=\"736\" height=\"477\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/9ea\/7f9\/abd\/9ea7f9abd3f09f27cc19eda10ca14c33.png\"\/><\/figure>\n<p>But there are many factors as well. After attempts to minimize an error using minimal square methods, I ended up with the following formulae for the progress:<\/p>\n<ul>\n<li>\n<p>P &#8212; page_count of the original index<\/p>\n<\/li>\n<li>\n<p>L &#8212; logical_reads in sys.dm_exec_requests <\/p>\n<\/li>\n<li>\n<p>D &#8212; density from sys.dm_db_index_physical_stats (mode &#8216;DETAILED&#8217;)<\/p>\n<\/li>\n<li>\n<p>F &#8212; fragment_count from sys.dm_db_index_physical_stats<\/p>\n<\/li>\n<li>\n<p>W &#8212; physical writes<\/p>\n<\/li>\n<\/ul>\n<p>For clustered indexes (multiply by 100 for percentage):<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"formula\" source=\"pct = \\frac{50 L}{P(50 + D)}\" alt=\"pct = \\frac{50 L}{P(50 + D)}\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/c4c\/e74\/2cd\/c4ce742cd8724381252c076821396b3b.svg\" width=\"147\" height=\"48\"\/><\/p>\n<p>For non-clustered:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"formula\" source=\"pct = \\frac{150 L}{2.74 (0.1P + F)(50+D)}\" alt=\"pct = \\frac{150 L}{2.74 (0.1P + F)(50+D)}\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/715\/1df\/0fa\/7151df0fa3e11f93a482abdf2ef05323.svg\" width=\"259\" height=\"48\"\/><\/p>\n<p>For COMPRESSION=PAGE use different formulae, for clustered:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"formula\" source=\"pct = \\frac{250(L - 2W)}{1.14(P + 0.2F)(150 + D)}\" alt=\"pct = \\frac{250(L - 2W)}{1.14(P + 0.2F)(150 + D)}\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/fde\/1b0\/28c\/fde1b028c94195c8e0c515523c6d4e41.svg\" width=\"268\" height=\"50\"\/><\/p>\n<p>For non-clustered:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"formula\" source=\"pct = \\frac{50 L}{2.9 P(50 + D)}\" alt=\"pct = \\frac{50 L}{2.9 P(50 + D)}\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/df0\/a55\/ecb\/df0a55ecb39ada22ff0c3c42cad47cda.svg\" width=\"171\" height=\"48\"\/><\/p>\n<p>COMPRESSION=ROW was not estimated because results vary a lot. In any case, script does the calculation for you, so you still get the ETA and pct in a log (but it warns that it is &#8216;rough estimation&#8217;). In most cases, it is 10% accurate.<\/p>\n<h3>Where to run? Kill\/rollback or to wait until the end?<\/h3>\n<p> created a program to test multiple scenarios. Let&#8217;s start with MAXDOP=1:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/ec4\/b1b\/d85\/ec4b1bd850b1f4a7d1b9182a3f772e81.png\" width=\"809\" height=\"547\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/ec4\/b1b\/d85\/ec4b1bd850b1f4a7d1b9182a3f772e81.png\"\/><\/figure>\n<p>The orange line is percentage time until the end of operation, which steadily decreases from 100 to 0. The blue line is a time to finish the rollback if interrupted at X% before the end (as I was doing the experimentation many times restoring the same database from backup, I had luxury of knowing exactly how long does it take for any index rebuild)<\/p>\n<p>Based on a chart, until 75% it is faster to do the rollback, after 75% it is faster to wait until the rebuild finishes.<\/p>\n<p>Now let&#8217;s try to play with MAXDOP. Note that for some indexes MAXDOP is ignored and only one thread is used.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/af3\/3e7\/29e\/af33e729e46688abfc0cf1df237e019b.png\" width=\"878\" height=\"527\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/af3\/3e7\/29e\/af33e729e46688abfc0cf1df237e019b.png\"\/><\/figure>\n<p>For MAXDOP=2 &#171;point of no return&#187; is around 63%. For higher MAXDOP chart becomes crazy:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/ab0\/6a8\/915\/ab06a89157c8248fa791514da37d69de.png\" width=\"722\" height=\"486\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/ab0\/6a8\/915\/ab06a89157c8248fa791514da37d69de.png\"\/><\/figure>\n<p>Note that for Y we have values higher than 100% &#8212; so rollback takes more time than an operation itself. it makes sense as rollback works in a single thread. Higher MAXDOP &#8212; more crazyness:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/5a2\/93f\/ad0\/5a293fad05df816070b80c094a1d9db8.png\" width=\"753\" height=\"490\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/5a2\/93f\/ad0\/5a293fad05df816070b80c094a1d9db8.png\"\/><\/figure>\n<p>Point of no return is even at 42%.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/2d4\/f73\/3db\/2d4f733dba542a88feb5087a16a742f0.png\" width=\"722\" height=\"477\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/2d4\/f73\/3db\/2d4f733dba542a88feb5087a16a742f0.png\"\/><\/figure>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/3b9\/df2\/71f\/3b9df271f2717186e56ec5640b070d3f.png\" width=\"632\" height=\"494\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/3b9\/df2\/71f\/3b9df271f2717186e56ec5640b070d3f.png\"\/><\/figure>\n<p>For NONCLUSTERED charts are slightly different as has slightly different &#171;points of no return&#187;.<\/p>\n<p>So for MAXDOP>2 server behaves weirdly, but work is finished faster:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/eeb\/d97\/3f0\/eebd973f06eb1200470da3303981a404.png\" width=\"648\" height=\"693\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/eeb\/d97\/3f0\/eebd973f06eb1200470da3303981a404.png\"\/><\/figure>\n<p>Y is time (seconds). As you see, the higher MAXDOP the faster the work is done (blue line), while orange line shows the time of the worst rollback scenario.<\/p>\n<p><strong>Conclusion<\/strong><\/p>\n<p>Use MAXDOP=1 or 2 if you plan on using kill because table is heavily accessed. otherwise set maximum MAXDOP to finish as fast as possible ignoring locks (use script configuration parameter  <strong>killnonresumable=0<\/strong> &#8212; then locks are displayed in a log, but do not trigger kill\/rollback)<\/p>\n<p>Otherwise (<strong>killnonresumable=1<\/strong>), script <strong>GentleRebuild<\/strong> kills the operation only if it didn&#8217;t reach the &#171;point of no return&#187;, which is defined by heuristics ($maxdop is a number of records in sysprocesses &#8212; it is MAXDOP+1, or 1, so it <em>actual<\/em> MAXDOP, not the MAXDOP you requested &#8212; it might be different):<\/p>\n<pre><code class=\"powershell\"># returns point of no return value  # note that for MAXDOP=n there are typically n+1 threads, so $maxdop=2 should never occur function PNRheuristic([int]$maxdop, [string]$itype) {   if ($maxdop -gt 9) { return 40. }   if ($itype -eq \"CLUSTERED\") { $heur = 75., 63., 63., 55., 45., 43., 42., 40., 37. }   else { $heur = 86., 74., 74., 70., 64., 55., 50., 45., 40. }   return $heur[$maxdop-1] }<\/code><\/pre>\n<h3>Victim Classifier<\/h3>\n<p>But should we panic for any locked processes? There are processes that definitely can wait -update statistics, some background time-insensitive ETL processes. But there are interactive processes that are critical.<\/p>\n<p><strong>custom.ps1<\/strong> module has a stub of a function which you can adjust for you needs. Originally it looks like as:<\/p>\n<pre><code class=\"powershell\">function VictimClassifier([string] $conn, [int] $spid) {    # returns:   #  cmd - details of the blocked command   #  cat - category this connection falls into   #  waittime - time this connection already waited, sec   #  maxwaitsec - maximum time for this connection allowed to wait   if (Test-Path -Path \"panic.txt\" -PathType Leaf) { return 3 }    # $waitdescr, $waitcategory, $waited, $waitlimit    $q = @\"   declare @cmd nvarchar(max), @job sysname, @chain int   select @job=J.name from msdb.dbo.sysjobs J     inner join (      select        convert(uniqueidentifier, SUBSTRING(p, 07, 2) + SUBSTRING(p, 05, 2) +       SUBSTRING(p, 03, 2) + SUBSTRING(p, 01, 2) + '-' + SUBSTRING(p, 11, 2) + SUBSTRING(p, 09, 2) + '-' +       SUBSTRING(p, 15, 2) + SUBSTRING(p, 13, 2) + '-' +  SUBSTRING(p, 17, 4) + '-' + SUBSTRING(p, 21,12)) as j     from (     select substring(program_name,charindex(' 0x',program_name)+3,100) as p       from sysprocesses where program_name like 'SQLAgent - TSQL JobStep%' and spid=$spid) Q) A     on A.j=J.job_id   select @cmd=isnull(Event_Info,'') from sys.dm_exec_input_buffer($spid, NULL)   select @chain=count(*) from sysprocesses where blocked=$spid and blocked&lt;>spid   select distinct @cmd as cmd,isnull(@job,'') as job, @chain as chain, waittime\/1000 as waittime,open_tran,     rtrim(program_name) as program_name, rtrim(isnull(cmd,'')) as cmdtype     from sysprocesses P where P.spid=$spid \"@     $vinfo = MSSQLscalar $conn $q   $prg = $vinfo.program_name   $cmd = $vinfo.cmd # top level command from INPUTBUFFER   $cmdtype = $vinfo.cmdtype # from sysprocesses   $job = $vinfo.job # job name if it is a job, otherwise \"\"   $chain = $vinfo.chain # >0 if a process, locked by Rebuild, also locks other processes (chain locks)   $waittime = $vinfo.waittime # already waited   $trn = $vinfo.open_tran # has transaction open   # build category based on info above   $cat = \"INTERACTIVE\"   if ($cmd -like '*UPDATE*STATISTICS*') { $cat = \"STATS\" }   elseif ($job -like \"*ImportantJob*\") { $cat = \"CRITJOB\" }   elseif ($cmdtype -like \"*TASK MANAGER*\") { $cat = \"TASK\" } # likely change tracking   elseif ($job -gt \"\") {      $cat = \"JOB\"      $cmd = \"Job $job\"   }   elseif ($prg -like \"*Management Studio*\") { $cat = \"STUDIO\" }     elseif ($prg -like \"SQLCMD*\") { $cat = \"SQLCMD\" }     if ($chain -gt 0) { $cat = \"L-\" + $cat }    # prefix L- is added when there are chain locks, it is critical   elseif ($trn -gt 0) { $cat = \"T-\" + $cat }  # prefix T- means that locked process is in transaction   $cmd = $prg + \" \" + $cmd    # allowed wait time before it thrrottles or kills rebuild   $maxwaitsecs = @{     \"INTERACTIVE\"   = 30     \"STATS\"         = 36000     \"JOB\"           = 600     \"CRITJOB\"       = 60     \"STUDIO\"        = 3600     \"SQLCMD\"        = 600     \"T-INTERACTIVE\" = 30     \"T-STATS\"       = 600     \"T-JOB\"         = 600     \"T-CRITJOB\"     = 30     \"T-STUDIO\"      = 1800     \"T-SQLCMD\"      = 600     \"L-INTERACTIVE\" = 20     \"L-STATS\"       = 20     \"L-JOB\"         = 20     \"L-CRITJOB\"     = 20     \"L-STUDIO\"      = 20     \"L-SQLCMD\"      = 20     \"TASK\"          = 900     \"L-TASK\"        = 60     \"T-TASK\"        = 600   }    return $cmd, $cat, $waittime, $maxwaitsecs[$cat] } <\/code><\/pre>\n<p>As you can see, functions perform a triage based on data in DBCC INPUTBUFFER, job name and sysprocesses. If locked process is in a transaction, &#8216;T-&#8216; is prepended to category name, so you can assign different timeout. Finally, if locked process also locks other processes (which is much worse) &#8212; category name is prepended with &#8216;L-&#8216;. Typically the timeout for &#8216;L-&#8216; is less than for &#8216;T-&#8216; which is less than for the default category.<\/p>\n<p>Based on category function calculates the value &#8212; the number of seconds a process in that category can wait.<\/p>\n<p>The interruption logic is the following:<\/p>\n<ul>\n<li>\n<p>If we are behind the  &#171;point of no return&#187; &#8212; bad, but we will wait until the operation finishes on it&#8217;s own<\/p>\n<\/li>\n<li>\n<p>Otherwise, if blocked process has waited longer than allowed by it&#8217;s category &#8212; we do kill\/rollback<\/p>\n<\/li>\n<li>\n<p>Otherwise, add time the process already waited to the estimated time until the operation will finish based by the estimated percent of the work done (projected time). If time already waited plus projected time is more than time allowed to wait &#8212; we do kill to prevent the problem much earlier when it becomes visible.<\/p>\n<\/li>\n<li>\n<p>For the interruptable operations (RESUMABLE=ON, REORGANIZE) projected time is not calculated, but the operation yields when the wait time becomes longer than time allowed to wait. So victim classifier for such operations allows to avoid immediate yielding based on locks<\/p>\n<\/li>\n<li>\n<p>If there are multiple locked processes, script takes the most critical, with the minimal time allowed to wait.<\/p>\n<\/li>\n<\/ul>\n<p>This is how it looks on a real system:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/029\/28c\/f43\/02928cf43ca84fb61c80efd4ba22596b.png\" alt=\"\u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 PROD\" title=\"real PROD\" width=\"1081\" height=\"275\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/029\/28c\/f43\/02928cf43ca84fb61c80efd4ba22596b.png\"\/><\/p>\n<div><figcaption>real PROD<\/figcaption><\/div>\n<\/figure>\n<p>Even through all that logic helps a lot, it doesn&#8217;t guarantee you from problems. You can be in red zone because the estimated percentage is far from reality, of if rebuild is behind of &#171;point of no return&#187; and there is a lock of a critical process. <\/p>\n<p>Script parameter <strong>offlineretries=n<\/strong> allows GentleRebuild to retry the rebuild multiple times after kill\/rollback, and when n attempts are exhausted it skips the index and goes to the next. <\/p>\n<p>So in general if there is a table under constant heavy stress then in Standard Edition there is little you can do except doing INDEX REOGRANIZE. There are few benefits on offline index rebuild, however. You can use SORT_IN_TEMPDB (not compatible with RESUMABLE=ON). Use parameter <strong>sortintempdb=n &#8212; <\/strong>it enables SORT_IN_TEMPDB if an index is less than n Mb. Finally, <strong>forceoffline=1<\/strong> &#171;transforms&#187; Enterprise Edition into Standard for a script GentleRebuild.<\/p>\n<h2>Index REOGRANIZE as last resort<\/h2>\n<p>because it works even on a Standard Edition, and index reorganize doesn&#8217;t hold any long locks, doesn&#8217;t create stress on a server because it works in a single thread, so it is safe, right?<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/1e5\/c8b\/b2f\/1e5c8bb2f31952e4d124374a444b008a.jpg\" alt=\"\u0412\u0435\u0434\u044c \u043f\u0440\u0430\u0432\u0434\u0430?\" title=\"Right?\" width=\"300\" height=\"300\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/1e5\/c8b\/b2f\/1e5c8bb2f31952e4d124374a444b008a.jpg\" data-blurred=\"true\"\/><\/p>\n<div><figcaption>Right?<\/figcaption><\/div>\n<\/figure>\n<h3>Comparing the quality<\/h3>\n<p>On a copy of one of our PROD databases I conducted an experiment, rebuilding indexes using options by default, using COMPRESSION=PAGE and ROW and using INDEX REORGANIZE<\/p>\n<p>Let&#8217;s start with the non clustered indexes:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/6b1\/c24\/bf4\/6b1c24bf4655956f489342e7ffc89a41.png\" alt=\"non clustered\" title=\"non clustered\" width=\"832\" height=\"476\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/6b1\/c24\/bf4\/6b1c24bf4655956f489342e7ffc89a41.png\"\/><\/p>\n<div><figcaption>non clustered<\/figcaption><\/div>\n<\/figure>\n<p>X is the original fragmentation level in percent. Y is the difference (after\/before) of an index size after the operation. So 25% would mean that the index became 4 times smaller, 100% means no change. <\/p>\n<p>Where are the blue dots, series &#8216;rebuilt&#8217;? They are covered with the series &#8216;page&#8217; as they are identical &#8212; I believe because we had &#8216;good&#8217; indexes on INT and GUID, where almost nothing can be packed. COMPRSSION=ROW sometimes made things worse, so index became bigger after the rebuild. So you should decide case by case. I recommend using no compression or PAGE.<\/p>\n<p>yellow dots (reorg) are higher (worse) than rebuild, so the quality of index reorganize is so-so. You can also see many points around Y=100% &#8212; there was nothing reorg could improve.<\/p>\n<p>For the clustered indexes it is different:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/8db\/b6d\/120\/8dbb6d1204668013350789914b46a193.png\" alt=\"clustered\" title=\"clustered\" width=\"748\" height=\"463\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/8db\/b6d\/120\/8dbb6d1204668013350789914b46a193.png\"\/><\/p>\n<div><figcaption>clustered<\/figcaption><\/div>\n<\/figure>\n<p>Compression=page is the best, reorganize works but is not as good as index rebuild.<\/p>\n<h3>Duration<\/h3>\n<p>Even for MAXDOP=1, rebuild appears to be many times faster than reorganize.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/226\/6a3\/fce\/2266a3fce83901dd427f60798c7d1363.png\" alt=\"X - size, Mb, Y - time, ms\" title=\"X - size, Mb, Y - time, sec\" width=\"766\" height=\"517\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/226\/6a3\/fce\/2266a3fce83901dd427f60798c7d1363.png\"\/><\/p>\n<div><figcaption>X &#8212; size, Mb, Y &#8212; time, sec<\/figcaption><\/div>\n<\/figure>\n<p>Red dots &#8212; rebuild, blue &#8212; reorganize. Reorganize takes longer for non clustered indexes by 3-5-7 times, for the clustered ones the difference was up to 30 times. Sometimes IDNEX REORGANZIE never ends, i will explain later why.<\/p>\n<p>Of course, like for index rebuild, it would be nice to have estimations of the progress. Here is the formula:<\/p>\n<ul>\n<li>\n<p>W &#8212; Writes &#8212; from sys.dm_exec_requests (connection where reorganize works)<\/p>\n<\/li>\n<li>\n<p>L &#8212; logical_reads from sys.dm_exec_requests <\/p>\n<\/li>\n<li>\n<p>P &#8212; page_count &#8212; index size in pages from sys.dm_db_index_physical_stats<\/p>\n<\/li>\n<li>\n<p>F &#8212; fragment_count &#8212; number of fragments from sys.dm_db_index_physical_stats<\/p>\n<\/li>\n<li>\n<p>D &#8212; density &#8212; from sys.dm_db_index_physical_stats &#8212; &#8216;DETAILED&#8217; mode<\/p>\n<\/li>\n<\/ul>\n<p>For clustered indexes percent of the work done (multiple by 100)<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"formula\" source=\"pct = \\frac{1}{1530}(W - \\frac{L}{2000})\\frac{800 + D}{P +0.5*F}\" alt=\"pct = \\frac{1}{1530}(W - \\frac{L}{2000})\\frac{800 + D}{P +0.5*F}\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/3af\/75a\/d4c\/3af75ad4cb9e303bd25d946a39efc2cc.svg\" width=\"304\" height=\"44\"\/><\/p>\n<p>For non clustered indexes:<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"formula\" source=\"pct = 105  \\frac{W}{P + 0.5 * F} (150 + D)\" alt=\"pct = 105  \\frac{W}{P + 0.5 * F} (150 + D)\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/023\/d4f\/d36\/023d4fd36df21675d929b39ec4d34d92.svg\" width=\"266\" height=\"44\"\/><\/p>\n<p>Estimation is usually 7-10% accurate. The script will show the progress and ETA.<\/p>\n<h3>Problems with Index Reorganize<\/h3>\n<p>Yes, Index reorganize doesn&#8217;t hold long locks on data. But it holds a lock on schema. Which locks automatic statistics updates. And other processes can be locked by these updates. Hence, we still need to run REORGANIZE in a controlled manner, using a GentleRebuild.<\/p>\n<p>After REORGANZIE is interrupted and resumed, script GentleRebuild remembers the last values of L and W to correctly calculate the estimation. However, in many cases W doesn&#8217;t grow for a while &#8212; SQL server scans pages where everything is already reorganized last time, before the script was interrupted. So it has to &#8216;rewind&#8217; to the last unaffected place, and it takes time, on big indexes &#8212; even hours. So if an index is big enough and interruptions are frequent enough, it might happen that most and finally all time SQL server would be doing the &#8216;rewinding&#8217;.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/736\/c5b\/068\/736c5b0685a10dd2ea0e43e1c6e62f39.png\" alt=\"rewinding\" title=\"rewinding\" width=\"1080\" height=\"276\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/736\/c5b\/068\/736c5b0685a10dd2ea0e43e1c6e62f39.png\"\/><\/p>\n<div><figcaption>rewinding<\/figcaption><\/div>\n<\/figure>\n<p>There is yet another problem. A few times REORANIZE started to slow down important ETL processes (without causing any locks, however). At the same time estimated percent of work done, which is typically<\/p>\n<p>+\/- 15% accurate grew continuously to 150%, 200% and even more. <\/p>\n<p>I believe that in some cases an area, where INDEX REOGRANIZE is doing its page movement collides with an area where processes are writing data, and in our case (24\/7), they are doing it non-stop. So they enter almost infinite loops of inserted new data -> fragmentation -> reorganzie &#8212; next insert just arrived etc.<\/p>\n<p>For that reason script finishes INDEX REORGANZIE as soon as estimated percentage reaches 120% and recalculates the fragmentation statistics. The same can be achieved using in interactive script menu (Ctrl\/C), command R. <\/p>\n<h2>Finally<\/h2>\n<p>Remember that index fragmentation deteriorates, so you should search fragmentated indexes from time to time to redo the rebuild\/reorg:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/63a\/a06\/f9a\/63aa06f9a51d80de1aff124c23585039.png\" width=\"820\" height=\"463\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/63a\/a06\/f9a\/63aa06f9a51d80de1aff124c23585039.png\"\/><\/figure>\n<p>Actual git repository:\u00a0<a href=\"https:\/\/github.com\/tzimie\/GentleRebuild\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/tzimie\/GentleRebuild<\/a><\/p>\n<p>Project site:\u00a0<a href=\"https:\/\/www.actionatdistance.com\/gentlerebuild\" rel=\"noopener noreferrer nofollow\">https:\/\/www.actionatdistance.com\/gentlerebuild<\/a><\/p>\n<h3><\/h3>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/761518\/\"> https:\/\/habr.com\/ru\/articles\/761518\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>How do you deal with index fragmentation if your SQL server is working in high load environment with 24\/7 workload without any maintenance window? What are the best practices for index rebuild and index reorganize? What is better? What is possible if you have only Standard Edition on some servers? But first, let&#8217;s debunk few myths.<\/p>\n<p><strong>Myth 1.<\/strong> We use SSD (or super duper storage), so we should not care about the fragmentation. False. Index rebuild compactifies a table, with compression it makes it sometimes several times smaller, improving the cache hits ratio and overall performance (this happens even without compression). <\/p>\n<p><strong>Myth 2<\/strong>. Index rebuild shorten SSD lifespan. False. One extra write cycle is nothing for the modern SSDs. If your tempdb is on SSD\/NVMe, it is under <strong>much<\/strong> harder stress than data disks.<\/p>\n<p><strong>Myth 3.<\/strong> On Enterprise Edition there is a good option: ONLINE=ON, so I just create a script with all tables and go ahead. False. There are tons of potential problems created by INDEX REBUILD even with ONLINE and RESUMABLE ON &#8212; so never run index rebuilds without controlling the process.<\/p>\n<p>Finally, we will tackle the REBUILD vs REORGANIZE subject and what is possible to achieve if you have only Standard Edition.<\/p>\n<h2>Enterprise Edition: Index Rebuild with ONLINE=ON and RESUMABLE=ON<\/h2>\n<h2>RESUMABLE index rebuild &#8212; theory<\/h2>\n<p>This is from where I started: up to 100Tb per server, AlwaysOn with 2-3 readable replicas, local SSDs \/ NVMe with up to 200K iops, Enterprise Edition.<\/p>\n<p>Reminder about RESUMABLE=ON. With RESUMABLE=ON you can always pause an operation using (killing the connection that is performing the rebuild, also puts rebuild in a paused state):<\/p>\n<pre><code class=\"sql\">ALTER INDEX ...  PAUSE<\/code><\/pre>\n<p>You can list indexes in resumable state using the following query:<\/p>\n<pre><code class=\"sql\">SELECT total_execution_time, percent_complete, name,state_desc,   last_pause_time,page_count   FROM sys.index_resumable_operations;<\/code><\/pre>\n<p>When rebuild is suspended, such operation even can <em>fail over<\/em> to another server! Awesome!<\/p>\n<p>Also, column <em>percent_complete shows the overall process<\/em>. (for indexes with WHERE clause, however, this value is not correct)<\/p>\n<p>Using <strong>ALTER INDEX &#8230; RESUME<\/strong> we can resume the operation (in fact, <strong>RESUME<\/strong> is just a syntax sugar &#8212; you can resume operation by simply executing the <strong>REBUILD<\/strong> command again instead of resume, just use the same options &#8212; for example, you can&#8217;t change MAXDOP on the fly). Using <strong>ALTER INDEX &#8230; ABORT<\/strong> you can kill the index rebuild.<\/p>\n<p>In the <strong>RESUMABLE<\/strong> mode transactions are short, so you should not worry that LDF growth will run out of control.<\/p>\n<h2>First try, failed miserably<\/h2>\n<p>So, let&#8217;s generate a script with <strong>ALTER INDEX<\/strong> for all indexes in our database and run it. It would be for a few days, so let&#8217;s start it on Friday so we can check the progress on Monday.<\/p>\n<p>Time to close the laptop and go home. But why are support people running between workplaces? I hear they are discussing some &#171;slowdowns&#187;&#8230; Wait, what are these alerts about AlwaysOn Queue Length? is it somehow related to my script? Oops, this is more serious, people are yelling about locks, apps are unresponsive, timeouts. Wow, my Rebuild lis blocking other connections! Alert dashboard is all red and yellow! <\/p>\n<p>My boss is angry and telling me to stop this damn rebuild and never &#8212; your hear &#8212; never! Try it again! I want to save you from this, so let&#8217;s analyze all potential problems in advance. How can rebuild affect the production system?<\/p>\n<ul>\n<li>\n<p>High CPU (and IO)<\/p>\n<\/li>\n<li>\n<p>Locks!!! (Surprise, surprise. ONLINE=ON, we believed in you, and you&#8230;) <\/p>\n<\/li>\n<li>\n<p>LDF growth out of control<\/p>\n<\/li>\n<li>\n<p>Problems with <em>AlwaysOn Queue<\/em><\/p>\n<\/li>\n<\/ul>\n<h3>Throttling<\/h3>\n<p>Let&#8217;s start with CPU and IO. We did not specify <strong>MAXDOP<\/strong>, and this is important. Index rebuild with a default <strong>MAXDOP<\/strong> can monopolize server. This is why I always specify <strong>MAXDOP<\/strong>, I recommend the following values:<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>Almost like in doom:<\/p>\n<ul>\n<li>\n<p><strong>MAXDOP=1<\/strong> &#8212; gently and slowly<\/p>\n<\/li>\n<li>\n<p><strong>MAXDOP=2<\/strong> &#8212; normal mode<\/p>\n<\/li>\n<li>\n<p><strong>MAXDOP=4<\/strong> &#8212; aggressive mode<\/p>\n<\/li>\n<li>\n<p>(unlimited) &#8212; NIGHTMARE!<\/p>\n<\/li>\n<\/ul>\n<p>Now it is much better, for example, with <strong>MAXDOP=2<\/strong> you will use 2 cores, while on a big production server you have a lot), but still there are many other problems.<\/p>\n<p>For example, in <strong>MAXDOP=4<\/strong> mode powerful server and populate the LDF file with 1Gb\/sec and faster, which means that in 10 minutes (typical time between transaction log backups) it can fill 600Gb in LDF, which is a lot. What is worse, 1Gb\/sec in LDF is 10 G-bits for <strong>AlwaysOn<\/strong> replication. 10 Gbit is not a big number for intra &#8212; datacenter replication, but what&#8217;s about remote datacenters used for the DR?<\/p>\n<p>Hence we do need to monitor free LDF size and percentage and <strong>AlwaysOn<\/strong> queue. If $db is a database, you can monitor it using the following query:<\/p>\n<pre><code class=\"sql\">USE [$db];   SELECT      convert(int,sum(size\/128.0)) AS CurrentSizeMB,       convert(int,sum(size\/128.0 - CAST(FILEPROPERTY(name, 'SpaceUsed') AS INT)\/128.0))        AS FreeSpaceMB,     (select sum(log_send_queue_size)+sum(redo_queue_size)        from sys.dm_hadr_database_replica_states        where database_id=DB_ID('$db')) as QueueLen,     (select isnull(max(case when redo_queue_size>0 then datediff(ss,last_commit_time, getdate()) else 0 end),0)       from sys.dm_hadr_database_replica_states        where database_id=DB_ID('$db')) as ReplicationDelayInSecs     FROM sys.database_files WHERE type=1<\/code><\/pre>\n<p>Conclusion: we can&#8217;t use sqlcmd or Management Studio or SQL server job to run rebuild in unattended mode. We do need to write a script with 2 threads &#8212; one will be running the actual command, while the other will check all the parameters and throttle the main thread if something goes out of control. <\/p>\n<p>What should be controlled? What thresholds should be defined? <\/p>\n<ul>\n<li>\n<p>CPU &#8212; it should not be too high<\/p>\n<\/li>\n<li>\n<p>LDF minimum percentage free<\/p>\n<\/li>\n<li>\n<p>And\/or maximum size allocated<\/p>\n<\/li>\n<li>\n<p>Maximum AlwaysOn queue length<\/p>\n<\/li>\n<li>\n<p>Locking<\/p>\n<\/li>\n<\/ul>\n<p>In reality I had to add few other conditions:<\/p>\n<ul>\n<li>\n<p>Deadline, to prevent work from continuing into late night or a weekend &#8212; this is just in case, to play safe, we don&#8217;t wont DBAs on duty to be woken up in the middle of the night (however, it is not possible with huge indexes &#8212; some operations take several days)<\/p>\n<\/li>\n<li>\n<p>Maximum size of indexes rebuilt daily (1Tb for example) &#8212; to prevent problems with QA\/Dev databases, which are synched using log shipping &#8216;replay&#8217;<\/p>\n<\/li>\n<li>\n<p>Custom throttling. In my case, 350 powershell lines which check job execution times of 20+ most important jobs and 15+ other critical parameters &#8212; to throttle the process or to even to panic and to abort an operation completely.<\/p>\n<\/li>\n<\/ul>\n<p>I wrote flexible parametrized script on PowerShell and i wanted to share it with you, it is non profit and open source: <a href=\"https:\/\/github.com\/tzimie\/GentleRebuild\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/tzimie\/GentleRebuild<\/a> (project site <a href=\"https:\/\/www.actionatdistance.com\/gentlerebuild\" rel=\"noopener noreferrer nofollow\">https:\/\/www.actionatdistance.com\/gentlerebuild<\/a> )<\/p>\n<p>This is how it looks like:<\/p>\n<figure class=\"full-width\">\n<div><figcaption>It is actually throttling because of the custom condition<\/figcaption><\/div>\n<\/figure>\n<p>Wait, why am I using <strong>kill<\/strong> command instead of <strong>PAUSE<\/strong>? There are important reasons for that.<\/p>\n<h3>Locking<\/h3>\n<p>Despite the fact that we are using REBUILD with ONLINE=ON, locking is possible on both sides:<\/p>\n<p>  <em>useful workload &#8212;> INDEX REBUILD &#8212;> useful workload<\/em><\/p>\n<p>We don&#8217;t care about the right part. Our rebuild can wait. The only problem could be alert systems yelling about locks. Scripts runs a rebuild in a connection marked with <em>program_name=Rebuild<\/em>. So you can add (<em>WHERE &#8230; AND program_name not like &#8216;Rebuild%&#8217;<\/em>) to your alert system to ignore this process. <\/p>\n<p>The left part is more important. If we lock any process, we need to yield, execute <strong>PAUSE<\/strong>, and then, after a while, continue using <strong>RESUME<\/strong>.<\/p>\n<p>However, the biggest problem is when there are both sides of the lock at the same time. So when the rebuild process is locked (right side), at the same time there is a process waiting for rebuild. To yield we run the <strong>PAUSE<\/strong> command and &#8230; nothing happens, because the rebuild process is locked, and <strong>PAUSE<\/strong> tries to finish the last chunk before going into paused mode.<\/p>\n<p>This is where <strong>kill<\/strong> command helps us: we lose the very last work chunk (few seconds), but we yield quickly. After <strong>kill<\/strong> rebuild falls into <strong>PAUSED<\/strong> status and it can be resumed later.<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>You can see how the script yields on the screenshot above. Why does ONLINE=ON cause locks? Most of the locks come from &#8216;TASK MANAGER&#8217; internal connections, doing automatic statistics updates. The locks that affect other processes are, based on my experience, mostly schema locks.<\/p>\n<p>In the custom module, you can define the reaction of the script to other processes being locked. For example, some background processes can wait longer, while for the interactive processes scripts should yield asap. This module is called VictimClassifier, and it is very useful for Standard Edition, where locking is inevitable.<\/p>\n<p>As a reminder, you should not worry about fragmentation levels in very small tables (page_count&lt;1000), they still show high fragmentation levels even after rebuild. Often it makes sense to rebuild tables with index fragmentation > 40%, however, indexes sometimes become significantly smaller on disk even for very low fragmentations levels. <\/p>\n<p>If you rebuild with COMPRESSION=PAGE\/ROW, you should not take fragmentation levels into  account because you need to change the compression anyway. Important warning: if there is a manipulation with partitions using ALTER TABLE&#8230; SWITCH PARTITION, tables on both &#8216;sides&#8217; should have the same compression! So be careful when you automatically apply compression to big tables leaving small tables (which participate in SWITCH PARTITION magic) unaffected. <\/p>\n<p>Don&#8217;t take a paused resumable state for granted. It is still affecting your system, even it is paused. This happens because SQL server has to track all changes to apply them to the &#8216;future&#8217; image of the index as well, so it modifies execution plans for queries, which do insert\/update\/delete into\/from that index. These operations become slower. In case of merge statement, it could be significantly slower. This is why the &#8216;panic&#8217; response from custom throttling module not only pauses the rebuild, but also aborts it.<\/p>\n<p>And yes, I learned all the above the hard way. In general, my experience was like this:<\/p>\n<figure class=\"full-width\"><\/figure>\n<p>Hopefully, this script will help you avoid all these troubles.<\/p>\n<p>Finally, some indexes don&#8217;t support RESUMABLE=ON and<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-355080","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/355080","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=355080"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/355080\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=355080"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=355080"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=355080"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}