The topic for this month's T-SQL Tuesday is:
"Be inspired by the IT horror stories from http://thedailywtf.com, and tell your own daily WTF story. The truly original way developers generated SQL in project X. Or what the grumpy "DBA" imposed on people in project Y. Or how the architect did truly weird "database design" on project Z"
And I'm torn.
I haven't missed a T-SQL Tuesday yet. Some months (okay, most months) it's the only blog post I write. I know I should write more posts, but I simply get distracted by other things. Other things like working for clients, or spending time with the family, or sometimes nothing (you know – those occasions when you find yourself doing almost nothing and time just slips away, lost to some newspaper article or mindless game that looked good in the iTunes store). So I don't want to miss one.
But I find the topic painful to write about. Not because of the memories of some of the nasty things I've seen at customer sites – that's a major part of why we get called in. But because I wouldn't ever want to be a customer who had a bad story that got told. When I see you tweeting things like "I'm dying in scalar-function hell today", I always wonder who knows which customer you're visiting today, or if you're not a consultant whether your employer knows what you're tweeting. Is your boss/customer okay with that tweet's announcement that their stuff is bad? What if you tweet "Wow – turns out our website is susceptible to SQL Injection attacks!"? Or what if you write "Oh geez, this customer hasn't had a successful backup in months…"? At what point does that become a problem for them? Is it when customers leave? Is it when they get hacked? Is it when their stock price drops? (I doubt the tweet of a visiting consultant would cause a stock price to fall, but still…)
So I'm quite reluctant to write this blog post at all. I had to think for some time before I thought of a scenario that I was happy to talk about.
This place was never a customer, and this happened a long time ago. Plus, it's not a particularly rare situation – I just hadn't seen it become this bad. So I'm happy enough to talk about this…
There was some code that was taking a long time to execute. It was populating a table with a list of IDs of interest, along with a guid that had been generated for this particular run. The main queries ran, doing whatever transforms they needed to do, inserting and updating some other tables, and then the IDs of interest were deleted from that table that was populated in the first part. It all seems relatively innocuous.
But execution was getting worse over time. It had gone from acceptable, to less than ideal, to painful. And the guy who was asking me the question was a little stumped. He knew there was a Scan on the list of IDs – he was okay with that because it was typically only a handful of rows. Once it had been a temporary table, but someone had switched it to be a regular table – I never found out why. The plans had looked the same, he told me, from when it was a temporary table even to now. But the temporary table solution hadn't seen this nasty degradation. He was hoping to fix it without making a change to the procedures though, because that would have meant source control changes. I'm hoping that the solution I recommended required a source control change too, but you never know.
What I found was that the list of IDs was being stored in a table without a clustered index. A heap. Now – I'm not opposed to heaps at all. Heaps are often very good, and shouldn't be derided. But you need to understand something about heaps – which is that they're not suited to tables that have a large amount of deletes. Every time you insert a row into a heap, it goes into the first available slot on the last page of the heap. If there aren't any slots available, it creates a new page, and the story continues. It doesn't keep track of what's happened earlier. They can be excellent for getting data in – and Lookups are very quick because every row is addressed by the actual Row ID, rather than some key values which then require a Seek operation to find them (that said, it's often cheap to avoid Lookups, by adding extra columns to the Include list of a non-clustered index). But because they don't think about what kind of state the earlier pages might be in, you can end up with heaps that are completely empty, a bunch of pointers from page to page, with header information, but no actual rows therein. If you're deleting rows from a heap, this is what you'll get.
This guy's heap had only a few rows in it. 8 in fact, when I looked – although I think a few moments later those 8 had disappeared, and were replaced by 13 others.
But the table was more than 400MB in size. For 8 small rows.
At 8kB per page, that's over 50,000 pages. So every time the table was scanned, it was having to look through 50,000 pages.
When it had been a temporary table, a new table was created every time. The rows would typically have fitted on one or two pages, and then at the end, the temporary table would've disappeared. But I think multiple processes were needing to look at the list, so making sure it wasn't bound to a single session might've been useful. I wasn't going to judge, only to offer a solution. My solution was to put a clustered index in place. I could've suggested they rebuild the heap regularly, which would've been a quick process run as often as they liked – but a clustered index was going to suit them better. Compared to single-page heap, things wouldn't've been any faster, but compared to a large empty heap, Selects and Deletes would've been much faster. Inserts are what heaps do well – but that wasn't a large part of the process here.
You see, a clustered index maintains a b-tree of data. The very structure of an index needs to be able to know what range of rows are on each page. So if all the rows on a page are removed, this is reflected within the index, and the page can be removed. This is something that is done by the Ghost Cleanup process, which takes care of actually deleting rows within indexes to reduce the effort within the transaction itself, but it does still happen. Heaps don't get cleaned up in the same way, and can keep growing until they get rebuilt.
Sadly, this is the kind of problem that people can face all the time – the system worked well at first, testing didn't show any performance problems, the scale of the system hasn't changed, but over time it just starts getting slower. Defragmenting heaps is definitely worth doing, but better is to find those heaps which fragment quickly, and turn them into clustered indexes.
…but while I hope you never come across heaps that have grown unnecessarily, my biggest hope is that you be very careful about publicly discussing situations you've seen at customers.