SQL University Troubleshooting Week: Having a Plan for Every Situation

SQL University LogoToday’s SQL University post will highlight the need to have a methodology to address issues that we as IT professionals may encounter in the course of our day. We will start off by looking at why we need to have a plan for every situation, and then we will dig into a methodology that I have developed by stealing bits and pieces of other people’s approaches over my career.

Most IT professionals lean on either knowledge, instinct or some mix of the two to solve the problems that they encounter. Knowledge and instinct are powerful tools that develop with experience. In the case of knowledge, we can rely on the experience of those that are gracious enough to share their experience via books, blogs etc. to make us all stronger. Instinct is a much harder developed tool. There is no way to do a Bing search to see if you have experienced a similar situation before and how you reacted to it.

What happens if we encounter a completely new problem that nobody has ever experienced before? Instinct and knowledge both require experience to move forward but there is none to draw from. At this point there is a real chance of getting caught up in what I like to call the Reaction Cycle. The Reaction Cycle is the technological quicksand that waits for us outside of our knowledge or when we are misled by our instincts. We apply an opposite force without thought, iteratively making things worse.

Think of the Reaction Cycle in terms of a brand new junior DBA. One day while the rest of the team is off at lunch they start getting alerts from the production sales database. From the alerts it appears that the server briefly lost its connection to the SAN and now there is some database corruption. The junior DBA, seeing all of the disk errors, decides the box needs a reboot while most of the company is still at lunch and proceeds to kick it over. When the server comes back up there are multiple inaccessible databases. By this time help has arrived, the rest of the team has returned from lunch. They quickly decide that at this point the best option they have is to restore the databases. They will lose some data but since this happened over lunch it is not the end of the world. They all agree that this is much faster than trying to work through the database corruption; after all they need to react quickly before too much money is lost. As they are restoring the sales database they discover they cannot apply transaction logs after 4 AM. A little further digging reveals that the junior DBA reacted to an alert about a log filling last night by truncating the database log. At this point the production database is gone and they have to go with what they have. 8 hours of sales are now gone due to the Reaction Cycle.

Shrinking databases might be said to kill kittens but reacting can definitely kill a career. When something goes wrong, your first step is to collect information. What is wrong? What do the logs say? If you have an error message then what does Bing say about it?

The next step is to process the information that was gathered. Based on the facts and data collected you can begin to formulate a response to what is going on. Sometimes based on this you need to go back and collect more data, like if you decide you need to restore you may want to try out the restore to a development server to make sure you have all the steps down.

Finally, after all the collecting and processing it is time to respond. What we are doing here is making a carefully planned move based on based upon the collection and processing we have done in the previous steps. We almost always know at least our next move if not the next couple of moves and we have thoughts on what may go wrong and how we would respond to that.

After responding we start over with the collection phase and move into the processing phase. Did we get the results we wanted? Are things better or worse? We continue to cycle through this process until we find no response is necessary.

With that, it is time to unveil the Collect, Process, Respond methodology for troubleshooting. In simple terms, we want to gather all necessary data to develop a plan then execute it. Feel free to print the image below and hang it on your cube wall to remind you to use the methodology.
Collect, Process, Respond
Now that we have covered why it is important to have a methodical approach to problem solving and taken a high level look at the Collect, Process, Respond methodology it is time to start digging into the individual phases.

The first phase we are going to dig into is the Collect phase. The most important thing about the Collect phase is that only 1 person does the data collection per system. With multiple people collecting data from the same system the risk of reacting to monitoring induced symptoms goes up exponentially. A good example of this is if someone fires up a Profiler trace on a server to collect data about an issue while someone else is looking at active user sessions. The person monitoring user sessions may see the sessions start to pile up but not get back to the source of the waits before the trace is stopped. This could lead to user sessions piling up being misattributed as a symptom of the problem rather than a necessary byproduct of troubleshooting. If it is truly necessary to have more than 1 person collect data from a system then 1 person has to call the shots and everyone has to communicate well.

The Collect phase is where we start scoping the issue or figuring out how wide to cast the net. I like to refer to it as ruling things in. We want to err on the side of ruling things in because it is easy for knowledgeable people to rule them out later. Remember our goal here is to figure out what is wrong.

So how do we go about actually collecting data? I like to ask these questions:

  • What are the Symptoms?
  • What Locations are involved?
  • What Systems are involved?
  • What Changed?
  • What is in the Logs?
  • What are the Performance Indicators showing?
  • The starting point for the Collect phase is to look at what the symptoms are. This is a key question because it helps us figure out what to collect. The symptoms may even tell us exactly what is wrong. If users are reporting a SQL Server error message about an account being locked it then it becomes easy to know what is going on and we can jump to the Process phase.

    Once we know the symptoms we want to look at what locations are involved. This is very important because it helps define the scope of further collection activities. If all users in a particular location are having issues then we would want to focus on what is unique to that location, but if all users in the company are having issues then we would want to look at what is common to all.

    Based on the answers to the previous questions it is now time to start looking at what systems might be involved. The goal of this step is to find all of the moving parts that make up whatever activity it is that is failing. This step often includes web servers, network load balancers and SANs so it is important to start bringing in other teams at this point.

    The last of the generalist tasks to get through before really digging in with the tools that we are comfortable with is to review the change control history. Many organizations have a calendar on a wiki or on SharePoint, sometimes there is even a log at the NOC in organizations large enough to have one. Worst case, talk to the primary on-calls or managers of systems that you may be involved to find out what changed lately. Spend some time here; almost everything that goes wrong is because of a change that someone implemented.

    Finally, we get down to the part that DBAs love. We get to bust out our magical tools that we are so used to using. I always save this for last because it is easy to get lost in the data, especially if it feels like it is leading us somewhere. This is where you would go trolling on any involved servers looking for any data that supports what you have seen earlier or anything that does not match with established baselines or in absence of formal baselines then anything that does not look the way you are used to seeing it.

    There are a number of third party tools to automate information gathering. Microsoft has the Management Data Warehouse and the Performance Dashboards. The great thing about these tools is they tell you at a glance what the important metrics are and usually have some sort of indicator when things are bad.

    Remember to be careful not to react while collecting data, the idea here is to gather as much useful information as possible.

    At this point we have collected all of the information we think we need and it is time to move into the Process phase. This is most people’s favorite phase because it feels the most like solving the problem but it is important to remember that we are not pushing any buttons or pulling any levers at this point. This phase is all about the making a plan to address the issue. You want to come away from this phase with an action plan, an expected result and a plan to rollback whatever action you take in case it makes things worse.

    As we move from the Collection phase to the Process phase we need to ask these questions:

  • Are there any obvious signs of trouble?
  • Can the problem be linked to a change?
  • There is a reason that I have listed these questions first. These are the things that will let us short circuit out of the Collect phase. It pays to keep them in mind when working through the Collect phase to avoid prolonging outages with unnecessary analysis. A good example of a short-circuit out of the Collect phase would be if one of the symptoms was an error message stating that a SQL login was locked out. The problem is clear and the solution is simple and low risk. The follow-up monitoring is simply to make sure that the account does not lock out again. The trick here is to make sure that your intentions are in the right place. Be especially aware of decisions made to make you look good or avoid looking bad.

    Next we have to look at the data we collected to see if any patterns can be identified. As patterns emerge theories will develop. It is important to create at least one test for each theory. I say at least one because we may have multiple moving parts and each moving part should get a test. Say that a client application hosts a Reporting Services report is slow, we would want to first run the report by calling it directly in a web browser. If it is slow in the browser then we would want to start looking at the parts that make it up, eventually pulling out individual queries and running them in a query window. We will eventually get to what the problem is.

    A clearly defined problem almost always indicates what corrective action is necessary. To keep with the above example we may say the report is slow because the query to get customer orders for the last 10 years is missing an index. There are times when no matter how well defined the problem is the answer is not clear. In those cases having a clearly defined problem is invaluable in enlisting external help whether they are from another team or from a vendor. It is the only way to make sure you are asking for the right kind of help.

    Once a list of possible actions is developed it is important to stack-rank the possible solutions by likelihood of success. The goal is to try the action that is most likely to resolve the issue while exposing you to the smallest amount of risk. I always recommend trying things out in another environment first. It helps get the steps and timing down and it exposes weaknesses in the plan so that the plan can be properly ranked. Think about this in terms of a situation where you have unrecoverable database corruption and short circuit all the way to this step, would you immediately start a point in time restore to production or would you try it out on another server first to make sure that your backup is good and that you have a bullet-proof script?

    The last thing to do before moving on to the Respond phase is to define how to measure whether the change helped or made things worse. I like to define a measure for each benefit and each risk that I identified while ranking the possible actions. I might say that adding this index will reduce reads from 10,000 to 6 or that adding this index may cause inserts into the table to take longer. I may also say that if the index does make inserts slower and page splits are noticeably higher then I may alter the fill factor of the index. It really pays to define success and failure here to make it clear when to stay, when to rollback and when to tweak the implementation.

    More than anything, you really need to make sure you have thought things out and are doing what is right for the situation you are facing and not doing something like rebooting because that is what you always do first.

    The first step in the Respond phase is to communicate your intentions. Depending on the type of change you might just tell the rest of your team or you may have to through change control to get approval to do something. The more involved or risky the action you are going to take the more documentation you should have and the more people you should involve to make sure you are not missing anything. Think of it like pool where the shot doesn’t count unless you call it.

    Next we make the change. To make the change we follow a written plan that we have hopefully rehearsed. Granted unlocking a user’s account is something you have done 100 times so you can say that is well rehearsed but how many times have you rebuilt the passive node of a production database cluster? Use your best judgment here, erring on the side of being conservative.

    A single person should make the change so that the plan can be followed step by step. If something is missing from the plan then it should be added to the plan in case these steps need to be followed again or reversed to roll back the change.

    After all that, it is time to go back and start collecting data again. The issue is closed when there are no more symptoms to be addressed and no more fixes to be deployed.

    So there you have it, a flexible, scalable methodology for solving just about any problem that any of us might face in the IT world. Use it well.

    Posted in SQL University, Troubleshooting | Tagged , , | 5 Comments

    SQL University Troubleshooting Week: Keeping an Open Mind

    SQL University LogoOffice politics during a major event can be dangerous. It pays to be seen as contributing to solving the problem rather than being seen as a part of it. We may have the best intentions and know our systems inside and out but if we refuse to look into something because we are sure it is not our issue then we are going to be seen as difficult and argumentative. If it turns out we are wrong and it is our issue we could even be seen as hiding something.

    I like to use the example of the rip current to explain office politics during a system outage. Unfamiliar swimmers caught in rip currents typically make the mistake of swimming against the current trying to get directly back to shore. Many get exhausted before they get back to shore and drown. Swimmers familiar with rip currents will go with the flow, swimming parallel to the shore until they are out of the current then swim back to shore.

    Keeping with the rip currents example, what happens when you fight people and say it is not your issue? Do they give up and go away; leaving you to what you were working on or do they fight you harder to prove it is your issue? At times it may even seem like their goal is not prove it is your issue but prove that you are being arrogant and that it could be. Many times it becomes less about the issue at hand and more about winning an argument.

    The simple truth is that it is better to go with the flow because it is faster.

    An important part of going with the flow is to construct tests that prove that something is our issue. Notice how I say to prove it is our issue rather than prove it is not. The human brain is incredibly open to having tricks played on it, by constructing an affirmative test we trick our brain into trying to find a way to make the test work. We are not happy with just a single failure to recreate the issue, we need to change the test scenario and test more until we can recreate the issue or run out of test scenarios. Most importantly, we are engaged and working to understand what is really going on.

    Having a good attitude is key to success in the information technology field. Keeping an open mind is central to that. By going with the flow and looking at issues from the right perspective we can solve problems faster while becoming known for our skill and professionalism.

    Posted in SQL University, Troubleshooting | Tagged , , | Leave a comment

    SQL University Troubleshooting Week: Communication

    SQL University LogoIt should come as no surprise that the first topic I am covering this week is communication because the first thing I think anyone should do is communicate that they are troubleshooting an issue. This post will cover why we should communicate then dig into how to put together an initial alert. The rest of the post will be spent talking about how to communicate updates and the resolution.

    First and foremost communication prevents your management from being caught by surprise when the VP of Sales calls to ask when they will be able to place orders again.

    Communication also prevents duplicated efforts. Many times when a system is down trouble reports come in from everywhere and go to everyone, resulting in a situation where there is no clear problem definition or problem owner. Communicating the problem owner allows the information to flow to a central place, allowing the problem to be properly defined.

    Finally, communication allows people to speak up about a recent change. If another team made a change recently they may be able to identify aspects of the issue you are working on that may be related to what they did. This is not saying someone was doing something sneaky although that sometimes happens. Usually this means that something was done and communicated to all the right people but not fully understood by the people it was communicated to or lost in turnover between support rotations. Assume the best here because you need people to speak up sooner rather than later. Treating them badly when they do speak up will only cause trouble in the long run.

    So what is the best way to communicate that there is a system issue? It helps to have an email group that includes all IT on-call pagers, management and other key people. If you do not have one I suggest setting one up solely for communicating large issues. It is important not to spam this list, treat it like pulling a fire alarm. It should only be used to communicate system issues from discovery through resolution with updates at regular intervals or large milestones throughout the process.

    It is also very important that the distribution list for these emails is IT only. People outside of IT may not know the intricacies of your particular implementation leading to the possibility of spreading misinformation. They are not doing this on purpose, they think they understand and like being involved in something exciting; that they are helping get the word out; doing their part. Let your management craft the organizational communications, they will have to answer for what is said in them.

    When sending alert emails keep the subject line general. If you give too much information in the subject line then people can assume it is not their issue and move on. We want to take advantage of that little pit in their stomach that everyone gets when something breaks to get them up to speed on the issue.

    Finally, the body of the email should provide a broad overview of the issue including what systems are impacted, any major symptoms including error messages, the number of people impacted and any location specific information. It is very important to keep to the important points here. The body of the email must be short enough to be fully read while long enough to include all important information.

    The body of your email should also contain a listing of any resources you need. If you need people then say I will be contacting the primary on-call from network engineering etc. to get their attention. Never use a mass communication to say “I cannot find Mike from the server team. If anyone sees him please tell him I need his help on this issue.” It will make both of you look bad and make the person on the receiving end less likely to help.

    Finally, only state the facts when communicating an issue and never assign blame. This is such an important part of the communication. It is important to only state what you know. What you think is not important and who is to blame is even less important. In the end the person that fixes the problem will be asked to explain what went wrong. Chances are they either made the change that led up to the issue or know who did. If there was a hardware failure they will be able to explain it in-depth as well. If you have any doubt about what you are communicating then check with someone that knows more about that particular area.

    Once the first alert is sent you generally have 30 minutes to either fix the issue or convene a war room. 30 minutes is a loose rule that I use because if the fix is easy then you will almost always identify the issue, develop a plan and fix it within that time. If 30 minutes goes by and you are still trying to figure out what is wrong then it is time to ask for help. Either way a follow-up alert should go out at the 30 minute mark to update everyone on the issue. The update should follow the same rules as the initial alert although the subject line should be prefixed with “UPDATE: “. Updates should continue at regular intervals until the issue is resolved.

    At some point all issues get resolved and that also needs to be communicated. The resolution should include the subject line of the original alert prefixed with “RESOLVED:“. Due to the potential for wide distribution, the alert should never mention anyone by name. The alert should contain a factual description of what the issue was and what was done to solve it, because facts are just facts and cannot convey opinions. Conclusions on the other hand, can convey opinions. Put the facts out there and let people draw their own conclusions. All that really matters is that the people in a position to prevent such a thing in the future recognize what happened and take actions to either prevent or mitigate the impact in the future.

    I hope you can see why I think properly communicating issues is important. I have outlined a system that has worked well for me. I strongly believe that anyone handling communications in this manner will be recognized for their professionalism and leadership.

    What works for you? Please feel free to leave it in the comments below.

    Posted in SQL University, Troubleshooting | Tagged , , | 2 Comments

    SQL University Troubleshooting Week: Syllabus

    SQL University LogoWelcome to SQL University Troubleshooting Week. For anyone unfamiliar with SQL University, it is a project created by Jorge Segarra (Blog|Twitter) to give people a free way to learn SQL Server from the ground up. The professors at SQL University are bloggers with one or more of them getting a week to cover their topic.

    I am honored to close out the final week of the Spring 2011 semester with a topic that I really enjoy: Troubleshooting. I really enjoy solving problems and that has caused me to get pulled into many situations where I could use and develop my troubleshooting skills. I hope to share the things I have learned here this week to speed everyone along in the process, hopefully avoiding some of the pitfalls that I had along the way.

    I tried to keep the posts short and easily digestible but I will warn you now that there are one or two that are a bit long. Here is what I have planned for this week:

  • Communication – Why it is good to communicate during system issues and how to get the word out effectively.
  • Keeping an Open Mind – Having the right attitude means being courageous enough to put ego aside, looking at things from a different perspective.
  • Having a Plan for Every Situation – You may not know what is going to happen next but you can still have a plan to deal with it.
  •  
    I hope to cover something useful for every level this week. Even as I wrote these posts I was reminded of things I could have done better in recent situations. Look for the “Communication” post tomorrow with “Keeping an Open Mind” on Thursday and “Having a Plan for Every Situation” on Friday. Friday’s post is quite long so it will make for good weekend reading.

    Posted in SQL University, Troubleshooting | Tagged , , | Leave a comment

    SQL Saturday 67 Slides Are Now Available

    I recently debuted a new presentation, “What To Do When It All Goes So Wrong”. The presentation is designed to give Database Administrators a basic overview of the skills they need to handle virtually any crisis that may arise. While the target audience is DBAs, I feel that most IT Professionals can benefit from the concepts.

    The first delivery of the presentation went well, although I definitely have some ideas for how I can improve on it. Look for this deck to evolve a bit over time. The biggest area that I still feel needs work is the narrative around the emergency scenario that I created. Right now it does not tie as well as I would like with the concepts later on in the presentation. Look for the narrative to develop more as I get more opportunities to deliver this presentation.

    You can get to the deck from my Presentations page. Yep, that’s right, I have a presentations page now. It feels good to finally have enough content to warrant a dedicated page.

    Please have a look and feel free to leave any feedback you might have in the comments section on that page.

    Posted in General | Tagged , | Leave a comment

    Looking Up Email Addresses with PowerShell

    A week or so ago Aaron Nelson (Blog|Twitter) put out a call for help on Twitter looking for anyone that could help with adding autocomplete for looking up an email address to a PowerShell forms application. The original request was to look up the user in the Global Address List (GAL) via Outlook but after some thought we decided to switch to looking up the recipients via Active Directory (AD), allowing the search to work on machines that do not have Outlook installed…like servers. I will walk through both functions then provide a quick and dirty test application so you can try out the code for yourself.

    The first function I want to show is the one to look up a recipient via Outlook. Despite its limited application this is the more interesting code block to me. I love that I only had to pass a partial name to get back the closest match from the GAL. I chose to include the instantiation of the $outlook object in this function because it made for a more clear example. If I were writing production code I would likely declare $outlook as a global variable then just instantiate it in this function if it had not been already. A great source for more information on calling into Outlook from PowerShell is this article.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [void] [System.Reflection.Assembly]::LoadWithPartialname("Microsoft.Office.Interop.Outlook")

    Function get-emailOutlook{
        param([System.String] $searchString)
        [Microsoft.Office.Interop.Outlook.Application] $outlook = New-Object -ComObject Outlook.Application
        $item = $outlook.Session.GetGlobalAddressList().AddressEntries.Item($searchString)
        $name.Text = $item.Name
        $email.Text = $item.GetExchangeUser().PrimarySmtpAddress
    }

    The other way to get after this information is via Active Directory. My feeling is that this is going to be the preferred method for most people to look up email addresses. The exception would be those people that want to be able to look up contacts from outside their organization in their local address book. Going the AD route took a few more lines of code, but was still quite easy. The big difference here is that we had to define a search filter by applying the wildcard symbol, “*”, to the portion of the recipient name that had been typed so far. The other big thing to notice is that checks to make sure the results are not null had to be added because unlike the Outlook this search may not return any results. I found this article incredibly helpful in putting this function together.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Function get-emailAD{
        param([System.String] $searchString)
        [System.DirectoryServices.DirectorySearcher] $searcher = New-Object System.DirectoryServices.DirectorySearcher
        $searcher.Filter = "(&(objectCategory=User)(Name=$searchString*))"
        $path = $searcher.FindOne()
        if($path){
            $user = $path.GetDirectoryEntry()
        }
        if($name){
                $name.Text = $user.name
                $email.Text = $user.mail
        }
    }

    That does it for the functions. Now for the test harness. This code is not terribly pretty and could probably be made more robust but it seems to work well enough for me to demonstrate the calls. The one thing that I had thought I would need to add is multi-threading to work like AJAX, but since the code runs so much faster than the HTTP calls you would see with AJAX it seems unnecessary. If the lookups run slowly in your environment then it would make sense to add some threading logic. With that, here is the code for the test harness:

    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
    [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
    [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
    [void] [System.Reflection.Assembly]::LoadWithPartialname("Microsoft.Office.Interop.Outlook")

    Function get-emailAD{
        param([System.String] $searchString)
        [System.DirectoryServices.DirectorySearcher] $searcher = New-Object System.DirectoryServices.DirectorySearcher
        $searcher.Filter = "(&(objectCategory=User)(Name=$searchString*))"
        $path = $searcher.FindOne()
        if($path){
            $user = $path.GetDirectoryEntry()
        }
        if($name){
            $name.Text = $user.name
            $email.Text = $user.mail
        }
    }

    Function get-emailOutlook{
        param([System.String] $searchString)
        [Microsoft.Office.Interop.Outlook.Application] $outlook = New-Object -ComObject Outlook.Application
        $item = $outlook.Session.GetGlobalAddressList().AddressEntries.Item($searchString)
        $name.Text = $item.Name
        $email.Text = $item.GetExchangeUser().PrimarySmtpAddress
    }

    Function get-email{
        [System.Int32] $location = $name.SelectionStart
        if($location -eq 0){
            $name.Text = ""
            $email.Text = ""
            return;
        }
        if ($useOutlook.Checked -eq $true){
            get-emailOutlook -searchString $name.Text.Substring(0,$location)
        }
        else{
            get-emailAD -searchString $name.Text.Substring(0,$location)
        }
        $name.SelectionStart = $location
    }

    [System.Windows.Forms.Form] $form = New-Object System.Windows.Forms.Form
    $form.Text = 'Autocomplete Tester'
    $form.Size = New-Object System.Drawing.Size(400, 200)
    $form.StartPosition = 'CenterScreen'
    $form.KeyPreview = $true
    $form.Add_KeyDown({if ($_.KeyCode -eq 'Enter'){$name.SelectionStart = $name.TextLength;$email.Visible = $true;}})
    $form.Add_KeyDown({if ($_.KeyCode -eq 'Escape'){$form.Close()}})

    [System.Windows.Forms.Label] $nameLabel = New-Object System.Windows.Forms.Label
    $nameLabel.Text = "Name to Search for:"
    $nameLabel.Width = 110
    $nameLabel.Location = New-Object System.Drawing.Size(20, 50)
    $form.Controls.Add($nameLabel)

    [System.Windows.Forms.TextBox] $name = New-Object System.Windows.Forms.TextBox
    $name.Text = ''
    $name.Width = 200
    $name.Location = New-Object System.Drawing.Size(140, 50)
    $name.AutoSize = $true
    $name.Add_KeyUp({get-email})
    $form.Controls.Add($name)

    [System.Windows.Forms.Label] $emailLabel = New-Object System.Windows.Forms.Label
    $emailLabel.Text = "Email Address:"
    $emailLabel.Width = 110
    $emailLabel.Location = New-Object System.Drawing.Size(20, 80)
    $form.Controls.Add($emailLabel)

    [System.Windows.Forms.TextBox] $email = New-Object System.Windows.Forms.TextBox
    $email.Text = ''
    $email.Location = New-Object System.Drawing.Size(140, 80)
    $email.Width = 200
    $email.AutoSize = $true
    $email.Visible = $false
    $form.Controls.Add($email)

    [System.Windows.Forms.CheckBox] $useOutlook = New-Object System.Windows.Forms.CheckBox
    $useOutlook.Checked = $false
    $useOutlook.Text = "Use Outlook"
    $useOutlook.Location = New-Object System.Drawing.Size(20, 20)
    $form.Controls.Add($useOutlook)

    $form.Topmost = $True
    $form.Add_Shown({$form.Activate()})
    [void] $form.ShowDialog()

    I hope you find these functions helpful. Please let me know if you run into any issues with them or know of a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in PowerShell | Tagged , , , , , , | 3 Comments

    Would You Like To Play A Game?

    Do you have a favorite line from a holiday movie? Can you change it around slightly to make it SQL related? If so then this is the game for you. Starting this morning and going until we get bored or run out of ideas we will be playing #tsqlHolidayMovieLines on Twitter.

    The rules are simple, just tweet a quote from your favorite holiday movie including the #tsqlHolidayMovieLines hash tag. Holiday movies include any movie that is about the holidays, set in the holidays or even includes holiday decorations (yes, Die Hard is a holiday movie for our purposes).

    If you want to play but are not yet on Twitter then go read Brent Ozar’s Simple Twitter Book then go to Twitter.com and sign up for an account.

    Lets hear your #tsqlHolidayMovieLines!

    Posted in General | Tagged , , , | Leave a comment

    Certificate Based Application Roles

    What if I told you that you could sign an assembly that your Windows application uses with a certificate, load that certificate into SQL Server and then define rights on that user-assembly combination? The perfect world where a user would have different rights based on the application they are running but still act under their Windows identity. The perfect world where things work just the way they should.

    Sounds pretty awesome, huh?

    The problem is that, as far as I know, this functionality does not exist yet. There might even be very good reasons why it does not exist yet, but this is a feature that I believe is overdue. Because of this, I have opened a Connect item to try to get Certificate Based Application Roles built into SQL Server. Here is the text of my Connect Item:

    I am in search of a more perfect application role. What I would like is to be able to sign a Windows executeable or even an individual assembly with a certificate. I would then take that certificate and load it into SQL Server. I would then associate the certificate to Windows users and groups to form an application role.

    The benefit of this approach is that depending on the executeable a user is running they could have different rights. A user that has reader on all tables in a database to query with Access might have execute on certain procedures when running the accounting application but have update on other tables when running the payroll software.

    If the user changes departments then they would change rights via group membership. If they leave the company then all rights would be removed like any other windows login. All activity would take place under the context of the users login, simplifying auditing.

    If you are like me and think this functionality should exist then please go vote up my Connect Item.

    Posted in User Management | Tagged , | 1 Comment

    Using DMVs to Find the Ports that SQL Server is Listening On

    The other day I was asked to provide the port number that a SQL Server instance was listening on. As luck would have it, the log file from the last time SQL Server started had rolled off so I was left with either using remote desktop to log onto the server and all the risk that includes or finally figuring out how to query it from the DMVs. I opted for the second option.

    I did some digging, opening any of the system views that looked promising. Unfortunately, I was not having much luck. I was about to give up when I remembered that sys.dm_exec_connections will tell what IP address and port. If it had the endpoint_id to join to then I could create my own version of sys.endpoints with the information I need. A quick look at the view showed it had the fields I needed.

    Here is the query I came up with:

    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
    SELECT      e.name,
                e.endpoint_id,
                e.principal_id,
                e.protocol,
                e.protocol_desc,
                ec.local_net_address,
                ec.local_tcp_port,
                e.[type],
                e.type_desc,
                e.[state],
                e.state_desc,
                e.is_admin_endpoint
    FROM        sys.endpoints e
                LEFT OUTER JOIN sys.dm_exec_connections ec
                    ON ec.endpoint_id = e.endpoint_id
    GROUP BY    e.name,
                e.endpoint_id,
                e.principal_id,
                e.protocol,
                e.protocol_desc,
                ec.local_net_address,
                ec.local_tcp_port,
                e.[type],
                e.type_desc,
                e.[state],
                e.state_desc,
                e.is_admin_endpoint

    I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , , | 6 Comments

    How Do I Change SSRS Report Credentials with Powershell?

    Today’s script took a long time to write. The concepts are fairly simple and the resulting script is quite trivial but the lack of easy to find documentation on Reporting Services administration via scripts meant I got to spend a lot of time with my good friend intellisense. In fact, it actually led me to go out and download PowerGUI and start using it again just to get intellisense.

    Now that I am done whining about how difficult it was, let’s talk about the script. Ever had to change the login information for all reports in a particular folder? In my case I was going from SQL authentication to Windows authentication for 4 folders with 15-20 reports each. I had done this before, manually, but I broke a few reports and it took hours to complete. Having to do the whole thing a second time meant a script was in order.

    The script below is pretty simple and could easily be turned into a function. It takes a target server, target folder, user name to change to and password to assign. I have the script hard-coded to use Windows Credentials because that is all I have tested with. With that information the script walks through the folder and adjusts the first data source on each report. If you have multiple data sources then you will want to add code to handle that. That is it. (I told you the result is deceptively simple.)

    Here is the code:

    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
    CLS

    [string] $message = ""
    [string] $reportingServicesServer = "<target_server>"
    [string] $startingFolder = "<target_folder>"
    [string] $userName = "<user_id>"
    [string] $password = "<password>"
    [string] $uri = "http://{0}/ReportServer/ReportService2005.asmx?WSDL" -f $reportingServicesServer

    $reporting = New-WebServiceProxy -uri $uri -UseDefaultCredential -namespace "ReportingWebService"
    $reports = $reporting.ListChildren($startingFolder, $false) | Where-Object {$_.type -eq "Report"}

    foreach($report in $reports)
    {
        $message = "{0}: Updating UserId and Password for the following report: '{1}'." -f $(get-date -displayhint DateTime), $report.Path
        Write-Host $message
        $dataSource = $reporting.GetItemDataSources($report.Path)[0]
        $dataSource.Item.WindowsCredentials = $true
        $dataSource.Item.UserName = $userName
        $dataSource.Item.Password = $password
        $reporting.SetItemDataSources($report.Path, $dataSource)
        $message = "{0}: Update completed." -f $(get-date -displayhint DateTime)
        Write-Host $message
        Write-Host ""
    }

    So that’s it. I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in PowerShell | Tagged , , , | 2 Comments

    Careful with the New MCM Requirements

    Ever since I first heard about the Microsoft Certified Master program I have been interested. The prestige of the certification and the promise of future riches are what draw many people in but for me the big selling point is the challenge. One of the big obstacles I had to pursuing the MCM was that I had not acquired any of the required certifications. A few weeks ago I decided to get out of my own way by removing the certification excuse.

    My first step in getting certified was to figure out what certifications I needed. Since I knew I may someday want to take a shot at the MCM I looked up the required certifications on the MCM page. This is where I got into a bit of trouble. The page I went to listed the required certification as “MCITP: Database Administrator SQL Server” and “MCITP: Database Developer SQL Server”. As this page shows, the 2008 versions of the certifications include 2008 in their names. After some careful reading I decided I should sign up for the SQL 2005 tests.

    Last week, while looking over the MCM Readiness Videos, I realized my mistake. On the front page there is a link to the New Path to Microsoft Certified Master: Microsoft SQL Server 2008 and on that page the required certifications are clearly specified as the SQL 2008 versions. From the beginning I had planned to take the upgrade tests to cover the SQL 2008 certifications as well as the SQL 2005 certifications so I will end up where I need to be anyway and get some extra bullet points for my resume.

    So there is my cautionary tale. Be careful with the new MCM requirements. You only need the SQL 2008 certifications now. If you have already started down the 2005 path then at least you know you are not alone. I should add that I have been in contact with the right people (not naming here since they may or may not be part of the first line support) to get the incorrect page updated. It may take a couple of weeks to get the incorrect page updated with the coming holiday but I am confident they will get it taken care of.

    If you are thinking about the MCM, I say go for it. I can only see them making it more difficult to get once more people start to achieve it.

    Posted in General | Tagged , , | 4 Comments

    Want to Make Your SQL Server Run Faster for Free?

    I know I do!

    The trick is to properly manage your VLFs.

    What are VLFs? The short anser is that VLF stands for virtual log file. SQL Server database log files are made up of many smaller virtual log files that make it easier for SQL Server to manage the log files. For a much more in-depth answer see the Transaction Log section Paul Randal’s (Blog|Twitter) TechNet article.

    The problem with virtual log files is that having too few or too many can slow your server down. I spoke to someone over the weekend that had to wait 30 hours for their database to complete recovery. When they checked they had 1.6 million virtual log files in their database. That is a lot by any standard. A more constant issue with having too many VLFs is that they can also slow down any log operation including logged statements like insert, update and delete. Check out this article by Linchi Shea (Blog) on the performance impact too many VLFs can have. Kimberly L. Tripp (Blog|Twitter) has a great article on how many VLFs you should have.

    So if the trick to making your server run faster for free is to manage your VLFs, then how do you go about doing that? Great question. There are lots of great posts out there on how to do this. Here is an article that I wrote detailing the process that I use and some of the reasoning behind it.

    If you find any really high counts in your environment, we’ll say greater than 2 million VLFs, please leave a comment with how many. I am curious to see what the highest value is.

    Posted in File Management | Tagged , , | 4 Comments

    Get Drive Space Including Mount Points

    Today’s post is a quick one that came out of a conversation on Twitter. To make a long story short, somebody was having trouble with mount points filling up because they were not being caught by their current monitoring script. I offered to look up how my monitoring was doing this and post it here.

    After some digging through various VBScript files I was able to confirm that I use a WMI query of Win32_Volume. The query I have is in a highly custom script that does not lend itself to being understandable if taken out of context so I went ahead and converted the logic to PowerShell, then messed with it a bit to come up with something human readable. Here is that script:

    Update: Nicholas Cain (Blog|Twitter) suggested using 1GB to get results in gigabytes instead of using /1024/1024/1024. I was curious about this trick and after a little toying around, was able to adjust the script to allow the units of measure to be specified at the top. So go ahead and give it a try.

    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
    cls
    [string] $serverName = 'localhost'
    [string] $unitOfMeasure = 'GB'  #Use an empty string for bytes or KB, MB, GB, TB, PB etc.

    $vols = Get-WmiObject -computername $serverName -query "select Name, DriveType, FileSystem, FreeSpace, Capacity, Label from Win32_Volume where DriveType = 2 or DriveType = 3"

    foreach($vol in $vols)
    {
        [string] $driveType = switch ($vol.DriveType)
        {
            0 {'Unknown'}
            1 {'No Root Directory'}
            2 {'Removable Disk'}
            3 {'Local Disk'}
            4 {'Network Drive'}
            5 {'Compact Disk'}
            6 {'RAM Disk'}
            default {'unknown'}
        }
        [string] $drive = "Drive: {0}, {1}, {2}, {3}" -f $vol.name, $driveType, $vol.FileSystem, $vol.Label
        [string] $capacity = "Capacity: {0}{1}" -f [System.Math]::Round(($vol.capacity / $('1' + $unitOfMeasure)), 0), $unitOfMeasure
        [string] $freeSpace = "Free Space: {0}{1}" -f [System.Math]::Round(($vol.FreeSpace / $('1' + $unitOfMeasure)), 0), $unitOfMeasure
        Write-Output $drive
        Write-Output $capacity
        Write-Output $freeSpace
        Write-Output ""
    }

    So there is the code as promised. The next logical step would be to add a call inside the loop to insert this information into a table for trending and alerting.

    Please do not run this on an important machine before reviewing it thuroughly. I offer no warranty beyond a sympathetic ear if this script breaks something.

    Posted in PowerShell | Tagged , , | 4 Comments

    A Brute Force Way to Compress a Database

    There are a handful of scripts out there to compress all of of the objects in your SQL 2008 database using Row, Page or a smart combination of both compression types. This is not one of those scripts.

    This script is written to compress all tables in a database without wasting any time on analysis. I have implemented the script as a stored procedure that takes 2 paramaters. The first is whether to use row or page compression and the second is how long to run for in minutes. The intent would be to call the stored procedure from a job on a regular basis to run for x minutes compressing everything it can then shut down and wait for the next run time and do the same. Eventually all objects in the database will be compressed.

    I would recommend against running this without any analysis against any database. That said, it can be a handy tool for compressing an ODS or even an EDW. It would also be a great way to pick up “the rest of the tables” in a database where proper analysis has been done and a particular compression type has been applied to a small set of tables.

    Here is the script:

    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
    CREATE PROCEDURE dbo.sp_dba_compress_user_objects @compression_type nvarchar(20) = 'PAGE', @minutes_to_run int = 1440

    AS

    DECLARE @schema_name        sysname,
            @object_name        sysname,
            @sql_string         nvarchar(max),
            @row_count          int,
            @start_time         datetime,
            @index_type_desc    nvarchar(60)
           
    SELECT  @row_count          = 1,
            @start_time         = GETDATE(),
            @compression_type   = UPPER(@compression_type)

    IF @compression_type NOT IN ('ROW', 'PAGE')
        RAISERROR('@compression_type must be ''ROW'' or ''PAGE''', 16, 1)

    WHILE @row_count > 0 AND DATEADD(mi, @minutes_to_run, @start_time) > GETDATE()
     BEGIN
        SELECT      TOP 1
                    @schema_name = OBJECT_SCHEMA_NAME(p.object_id, DB_ID()),
                    @object_name = OBJECT_NAME(p.object_id, DB_ID()),
                    @index_type_desc = ips.index_type_desc
        FROM        sys.partitions p
                    INNER JOIN sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ips
                        ON p.object_id = ips.object_id
                            AND p.index_id = ips.index_id
                    LEFT OUTER JOIN sys.columns c
                        ON p.object_id = c.object_id
                            AND c.is_sparse = 1
        WHERE       p.data_compression_desc = 'NONE'
                        AND OBJECTPROPERTY(p.object_id, 'IsUserTable') = 1
                            AND OBJECTPROPERTY(c.object_id, 'IsMSShipped') = 0
                                AND c.object_id IS NULL
        ORDER BY    ips.page_count

        SELECT  @row_count = @@ROWCOUNT
       
        IF @row_count > 0
         BEGIN
            IF @index_type_desc = 'HEAP'
             BEGIN
                SELECT  @sql_string = 'ALTER TABLE ' + QUOTENAME(@schema_name) + '.' + QUOTENAME(@object_name) + ' REBUILD PARTITION=ALL WITH (DATA_COMPRESSION=' + @compression_type + ', ONLINE=ON)'
                RAISERROR(@sql_string, 0, 0) WITH NOWAIT
                EXEC    sp_executesql @sql_string
             END
            SELECT  @sql_string = 'ALTER INDEX ALL ON ' + QUOTENAME(@schema_name) + '.' + QUOTENAME(@object_name) + ' REBUILD PARTITION=ALL WITH (DATA_COMPRESSION=' + @compression_type + ', ONLINE=ON)'
            RAISERROR(@sql_string, 0, 0) WITH NOWAIT
            EXEC    sp_executesql @sql_string
         END
        ELSE
         BEGIN
            PRINT 'No work to do.'
         END
     END
     
     IF GETDATE() > DATEADD(mi, @minutes_to_run, @start_time)
      BEGIN
        PRINT 'Shut down to due long run time.'
      END
    GO

    I hope you find this stored procedure useful, but I realize it is not for everyone. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , , , | 1 Comment

    When is that Restore Going to Finish?

    I spent a good portion of last weekend restoring databases from backup due to a large release. Nothing went wrong, luckily, but I was practicing in case something did. It goes without saying that I spent a ton of that time sitting in front of my PC wondering when the restores would finally finish. In my boredom I started looking at ways to use the percent_complete column in sys.dm_exec_requests. As I was working on my query I noticed a column named estimated_completion_time. I had never seen this column before, but since the server I was on was running SQL 2005 I can only guess it had been there for some time.

    After doing a quick search on the internet for how to use the column I ended up in Books Online. The definition for the column published there is “Internal only. Is not nullable.” Now that I knew someone did not want me to use the field I really wanted to use it. The field contained a really big number so I started looking at how to translate it into a time. Given that it was a really big number and seemed to be counting down, I tried adding it as milliseconds to the current date. I was a little shocked when the query returned a time a few minutes into the future that ended up pretty close to right on the first try.

    Here is the query that I came up with:

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT  command,
            session_id,
            percent_complete,
            DATEADD(ms, estimated_completion_time, GETDATE()) AS estimated_completion_time,
            GETDATE(),
            start_time
    FROM    master.sys.dm_exec_requests
    WHERE   percent_complete > 0

    As I ran the query throughout the weekend I noticed that it was not always 100% accurate. A big thing I noticed is when restoring a SQL 2005 database to a SQL 2008 server the percent complete will be at or near 100% and the estimated completion time will be the current time while the conversion from SQL 2005 to SQL 2008 happens. That said, I still find this query very useful for giving a ballpark end time to a multi-hour restore and also found it very helpful for gauging the impact of changes like shutting off other jobs on the server or asking people to postpone work that adds x hours to the process.

    I hope you find this query useful. I expect that it will work for any process that reports progress so the applications are definitely not limited just to restores. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    UPDATE: Aaron Bertrand has a version of this query that shows more information available here: http://sqlblog.com/blogs/aaron_bertrand/archive/2009/07/30/when-will-my-backup-restore-index-reorganize-finish.aspx

    Posted in General | Tagged , , , , , , , | 5 Comments

    Stop Forgetting the DAC

    From time to time I hear of DBAs that had to restart the SQL Server service on a 2005 or 2008 server because it had stopped accepting new connections. The thing is that most times those servers did not have to be restarted. If the DBA had logged in via the Dedicated Administrator Connection (DAC) they would have been able to identify and either kill or track the progress of the offending query.

    So what is the DAC? It is a special reserved connection that allows 1 connection from the local machine by appending “ADMIN:” to the beginning of the server or server\instance name. (Exact instructions are available here) The DAC is by default only available to clients connection from the machine that the instance is running on. There is a setting that allows people to connect over the network but in an emergency it is good to know who is using that connection. Forcing people to log onto the box to use the connection means you can see who might have the connection by looking at who is connected to the server. Remember you can’t connect to SQL Server to see who is connected if things are bad enough that the DAC is your only option.

    The great part about the Dedicated Administrator Connection (DAC) is that it is always available for someone to connect. Even when normal connections to the server do not work the DAC is ready to accept a connection. Once connected via SSMS (Query Window not Object Explorer) or SQLCMD a DBA can quickly diagnose and resolve an issue.

    We no longer have to blindly restart SQL, hoping that we are not setting up a situation where our most important database will be in recovery for minutes or hours.

    We now have the tools to accurately diagnose a problem the first time it happens.

    We just have to remember to use them.

    Posted in Troubleshooting | Tagged , , | 1 Comment

    How Do I Spot Identity Columns That Are About to Max Out?

    Every so often, usually in the middle of the night or on a holiday weekend, an identity column will hit the maximum size for it’s data type and stop allowing new values to be inserted into the table. It goes without saying that an identity column with enough activity to hit the maximum value of even a regular integer indicates a busy table. Oddly enough, none of the monitoring tools that I have looks for an identity column that is about to fill up.

    Since I had identified a real problem I decided it was time to turn to my problem solver, SQL Server Management Studio. I was able to quickly throw together a stored procedure that I can install on each of my machines then add custom alerts to my monitoring tools to call it. Since some tools run at the server level and others at the database level I wrote the script to work at the database level. For simplicity I have included a script at the bottom to run this stored procedure on all databases on a server.

    The stored procedure logic is to get all identity columns in a database, using the column type from sys.types to calculate percent full based on current identity value vs. maximum identity value for that type. The inner query then returns those values back to the outer query to be filtered and have severity assigned. I could have written the whole thing as a single select rather than nesting it but writing it this way made it so much more readable.

    Here is the code for the stored procedure:

    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
    CREATE PROCEDURE dbo.sp_dba_check_identities_for_space @warning_threshold_in_pct int = 80, @error_threshold_in_pct int = 90
    AS

        SELECT  @warning_threshold_in_pct = @error_threshold_in_pct
        WHERE   @warning_threshold_in_pct > @error_threshold_in_pct

        SELECT  database_name,
                table_name,
                column_type,
                percent_used,
                CASE WHEN percent_used >= @error_threshold_in_pct THEN 'ERROR' ELSE 'WARNING' END AS severity
        FROM    (
                    SELECT  DB_NAME() as database_name,
                            OBJECT_NAME(c.object_id) as table_name,
                            IDENT_CURRENT(OBJECT_NAME(c.object_id)) as index_seed,
                            t.name as column_type,
                            (IDENT_CURRENT(OBJECT_NAME(c.object_id)) /
                            CASE
                                WHEN    t.name = 'bigint'
                                    THEN    9223372036854775807.00
                                WHEN    t.name = 'int'
                                    THEN    2147483647.00
                                WHEN    t.name = 'smallint'
                                    THEN    32767.00
                                WHEN    t.name = 'tinyint'
                                    THEN    255.00
                                WHEN    t.name = 'numeric'
                                    THEN    POWER(10, c.precision) - 1
                                WHEN    t.name = 'decimal'
                                    THEN    POWER(10, c.precision) - 1
                                WHEN    t.name = 'money'
                                    THEN    922337203685477.5807
                                WHEN    t.name = 'smallmoney'
                                    THEN    214748.3647
                                ELSE    1.00
                            END) * 100 AS percent_used
                    FROM    sys.columns c
                            INNER JOIN sys.types t
                                ON c.system_type_id = t.system_type_id
                    WHERE   c.is_identity = 1
                                AND OBJECTPROPERTY(c.object_id, 'IsUserTable') = 1
                ) dt
        WHERE   percent_used > @warning_threshold_in_pct
    GO

    EXEC sys.sp_MS_marksystemobject 'sp_dba_check_identities_for_space'

    Here is a script to run the stored procedure for all databases on a server:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    CREATE TABLE #sp_dba_check_identities_for_space_results
    (
        database_name   sysname,
        table_name      sysname,
        column_type     sysname,
        percent_used    int,
        severity        varchar(20)
    )

    EXEC sp_MSforeachdb 'USE ?
    INSERT #sp_dba_check_identities_for_space_results
        EXEC dbo.sp_dba_check_identities_for_space'

       
    SELECT  database_name,
            table_name,
            column_type,
            percent_used,
            severity
    FROM    #sp_dba_check_identities_for_space_results

    DROP TABLE #sp_dba_check_identities_for_space_results

    Please let me know if you run into any issues, have any ideas that would make this stored procedure better or just want to share how you are using it. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , , , | 4 Comments

    Can Deadlocks be Resolved by Adding an Index?

    Can Deadlocks be Resolved by Adding an Index? Yes. Really, I’m not kidding. Here, I’ll show you how.

    First, let me set the scene: One of the applications I support has had issues with deadlocks for some time now. The deadlocks are really a result of design issues that it would be too expensive to fix. One of the challenges of the current design is that some of the data is partitioned the old fashioned way, in multiple tables, based on request type. The challenge of this design is it means there are two tables that have a sequential integer that is shared between them. The idea is to be able to query both tables, union the result sets together in sequence as though they had been in the same table. To accomplish this task a third table was added to track the sequence number as an identity column. A stored procedure inserts a record into the table, grabs the identity value and then deletes the inserted row. The deadlocks have been happening in the stored procedure that grabs that identity value.

    Digging into the issue, my first step was to reproduce the deadlock to make sure I had a grasp of what was going on. I will include the code here in case anyone wants to follow along at home.

    Step 1 is to create a table:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    IF EXISTS(  SELECT  *
                FROM    sys.tables
                WHERE   name = 'FriendlyNumberSeed' )
        DROP TABLE dbo.FriendlyNumberSeed
    GO

    CREATE TABLE dbo.FriendlyNumberSeed
    (
        LastCaseNumber int IDENTITY(1,1) NOT NULL,
        DateRetrieved datetime NOT NULL
    )
    GO

    Step 2 is to insert some data and create some locks. Run this code in the same window:

    1
    2
    3
    4
    BEGIN TRANSACTION

    INSERT dbo.FriendlyNumberSeed
        SELECT  GETDATE() AS DateRetrieved

    Step 3 is to create more data and more blocking. Run this code in a separate window:

    1
    2
    3
    4
    BEGIN TRANSACTION

    INSERT dbo.FriendlyNumberSeed
        SELECT  GETDATE() AS DateRetrieved

    Step 4 is to create some blocking. Run this in the first window:

    1
    2
    DELETE  dbo.FriendlyNumberSeed
    WHERE   LastCaseNumber = SCOPE_IDENTITY()

    Step 5 is to create the deadlock. Run this code in the second window:

    1
    2
    DELETE  dbo.FriendlyNumberSeed
    WHERE   LastCaseNumber = SCOPE_IDENTITY()

    During my testing I had a few seconds between setting up the deadlock and the deadlock monitor reacting to it so I was able to pull the “All Transactions” report by right clicking on the database in Management Studio and selecting Reports > Standard Reports > All Transactions.

    Here are the results I saw:

    Locks before adding an index

    Locks before adding an index

    I spent a while digging into this, even breaking out DBCC PAGE to look at the page structure to try to figure out what was happening. After working through several wild theories, I looked at the locks again. That’s when it hit me: Why would the first transaction try to grab an update lock on the row inserted by the second transaction? There are no indexes on the table.

    To delete a record from a table with no indexes SQL Server has to do a table scan, meaning it has to touch every single row to see if they qualify. To avoid deadlocks SQL Server grabs an update lock before reading the record. In this case the update lock is prevented by the exclusive lock still held by the insert. To see if I was on the right track I looked at the plan for the delete.

    Here it is:

    A query plan for a delete that does a table scan

    Query plan showing table scan while performing a delete

    At this point I am pretty sure I am on the right track so it is time for more testing. The next thing I did was to put ROLLBACK WORK commands in both of my query windows to make sure that both transactions were completely out of the way. I then ran the following code:

    1
    2
    3
    4
    CREATE CLUSTERED INDEX IX_FriendlyNumberSeed_LastCaseNumber
    ON dbo.FriendlyNumberSeed (LastCaseNumber)
    WITH (FILLFACTOR=100)
    GO

    With the index in place I re-ran steps 2-5 above and everything worked perfectly. Let’s look at why that happened. The first step is to look at the new query plan for the delete after the index create:

    Query plan for delete statement with index yielding a seek

    Query plan for delete statement with index yielding a seek

    The final proof that I have fixed the deadlock by adding an index is to look at the transactions report. If the index fixed the problem the report should show that the update lock has disappeared. Here is the report:

    Locks after index was added

    Locks after index was added

    The update lock is gone and so is the deadlocking. Adding an index to the table has resolved my deadlock problem. The test that I did here used a clustered index but the same results would be available with a non-clustered index.

    I know this was a long post. Thanks for sticking with me all the way to the end.

    Posted in Troubleshooting | Tagged , , , , , , , | 4 Comments

    A Little Help with Azure Please

    Not long ago Microsoft announced a new product called Azure AKA “SQL Server in the Cloud”. I have to say that when I first heard about the product I was skeptical. The problem I had is that I already have servers, lots of them in fact. I struggled to find any reason that would make me want to spin up a new server in the cloud. A presenter at a user group meeting changed that for me. The presenter threw out the example of the one-off charity campaign web site that many of us have to spin up with little notice then tear down after about a month of use. The lights went on, I got it. I now had an option that was even lighter than a VM and could be spun up faster.

    Buying into the cloud was the easy part. Getting to the point where I can convince people in my organization that they should put their application in the cloud is another story. This is where I could use your help today. I have the easy parts of the argument down. The big thing is time. I can spin up a new Azure database in a couple of hours, while it could take a day or more to get a VM or server spun up depending on how isolated the application needs to be and how busy the respective teams are. Another interesting benefit is that the way the billing is set up it really makes it easy to track return on incubator type projects.

    I get the great parts of the cloud but there are still some things I could use help with. Here are the questions I have not had much luck finding answers to or have not dug into yet (I thought of 1 or 2 questions as I was writing this):

    How secure is my data?

    This is a big one. How can I prove that my data is secure both in the database and over the wire? I have heard anecdotally that data can be encrypted at the application before being sent over the wire and stored in the database but how would this look? What would performance be like?

    How do backups work?

    Can I back up a database to restore to an internal server or restore a database from a local backup to Azure? Would I still have the opportunity to manage recovery like I do on servers I own?

    How does maintenance work?

    Can I / do I need to set up the usual maintenance jobs to keep indexes and statistics up to date, check for corruption, etc.?

    What is support like?

    This is sort of a double question but how do I monitor the performance of my database in the cloud? Are there any tools available that make it easier to proactively monitor for issues? What kind of support can I expect if I do have issues? Is there someone I can call? Is there someone monitoring that will call me? Am I expected to be able handle things on my own?

    What about DR?

    In the Azure demo I saw I distinctly remember the presenter referring to a Chicago data center. Is that a single data center located near Chicago or a group of them that share the load? Would I need to plan for the possibility that natural disaster or a fiber seeking backhoe could knock my database offline? Would my database move to another data center somewhere else in the world and just run a little bit slower until my chosen data center is back online?

    So there you have it. Those are my concerns about moving data to the cloud. Overall, I think the cloud will be huge, although I do not know enough about it to know if I am ready to add it as a tool in my toolkit yet. If you know the answer to any of my questions please feel free to leave them in the comments below. Thanks for taking the time to hear me out and share any knowledge you may have.

    Posted in Azure | Tagged , , , | 3 Comments

    What Did that Geek Just Say?

    Working in a technical field has it’s ups and downs. One of the more common annoyances I run into is people that can either not explain what they mean in simple terms or that choose not to in order to appear knowledgeable. I still have a long way to go before I can suggest a sure-fire approach to curtailing this behavior. Quite honestly, I have been known to fall back on “fancy tech words” on more than one occasion to hide ignorance or to try to seem smart (OK that was hiding ignorance twice but it sounds better the way I said it). What I can offer is a great resource to understand what people are saying and maybe even have some fun with them/me.

    The National Institute of Standards and Technology, NIST, maintains a Dictionary of Algorithms and Data Structures. The dictionary is a great place to go to find out what people are talking about. It is also a great place to looking for different ways of doing things. I sometimes even go to the site just to look for entertaining technical concepts, like the “Cactus Stack”, “Stooge Sort” or even “Big-O Notation”.

    Have fun with the big words but please only use them for good or at least funny evil. Go ahead and post any you feel are interesting or just plain funny in the comments of this post. The more time I spend on that site the more terms I find I was overlooking.

    Posted in General | Tagged , , | 1 Comment

    How is Fill Factor Impacting My Indexes?

    TSQLTuesday LogoThe theme for this month’s T-SQL Tuesday is indexes so it seemed like the perfect excuse to blog about a script that I have written to see what choices for fill factors on indexes actually does to the structure of those indexes. I have to give special thanks to Brent Ozar (Blog|Twitter) for taking the time to review and offer his thoughts on the query. I have to admit that I was nervous to publish the script because I have not seen anything else like it and figured there must have been a reason for that.

    For those that are unfamiliar, fill factor is an optional parameter that can be specified when adding or rebuilding an index. Specifying a fill factor tells SQL Server to leave a certain percentage of each data page open for future inserts in order to lessen the likelihood of page splits. Page splits are what happens when SQL Server tries to add another row to a data page that it does not fit on. Most page splits involve taking half the rows on the page and putting them onto a newly allocated page somewhere else on your data file, allowing sufficient room for the new row to be added to either page. If you are lucky enough that the row you are adding would be the last row on the page then the existing page is left as is and the new row is added to the newly allocated page. Regardless of how the page splits, the new page is almost never anywhere near the other pages of the index it goes with. The scattering of index pages means that the disk heads have to move around a lot more leading to poor performance.

    Now that we have talked about the problems that fill factor can help us with, we should talk about the dark side. Yes, the dark side. Setting the fill factor to anything other than the default decreases the rows per page for that index, thereby increasing the number of pages that must be read. According to Books Online, the read performance penalty is twice the chosen fill factor. This means that setting the fill factor to 50% will lead to twice as many reads to get the same data. Even a more reasonable number like 90% would have a 20% performance penalty on all reads.

    By now it should be clear that choosing the right fill factor for your indexes is one of the more important steps in creating an index, right behind picking the right key columns. The problem is knowing how to pick a good number and here is where it gets tough because like everything else: It Depends and It Changes. My method of setting fill factors is to calculate the rows per page of an index then use the expected change in rows between reindex operations to figure out what percentage of rows need to be left free per page. The exception to this process is if the index is on an ever increasing value, like an identity column, then the fill factor is automatically 100.

    My process works very well for the “It Depends” part of setting a fill factor but completely ignores the “It Changes” part. Over time as tables get larger, the fill factor setting on a table needs to be adjusted down. I have also run into servers where the default fill factor has been set to a value other than 0 (same as 100%), creating a need to quickly identify indexes that could perform better. What I needed was a simple query that I could run that would very quickly give me an idea of where I can adjust fill factors to improve performance.

    Here is that query:

    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
    SELECT      OBJECT_NAME(ips.object_id) AS table_name,
                ips.index_type_desc,
                ISNULL(i.name, ips.index_type_desc) AS index_name,
                ISNULL(REPLACE(RTRIM((  SELECT      c.name + CASE WHEN c.is_identity = 1 THEN ' (IDENTITY)' ELSE '' END + CASE WHEN ic.is_descending_key = 0 THEN '  ' ELSE ' DESC  ' END
                                        FROM        sys.index_columns ic
                                                        INNER JOIN sys.columns c
                                                              ON ic.object_id = c.object_id
                                                                    AND ic.column_id = c.column_id
                                        WHERE       ic.object_id = ips.object_id
                                                              AND ic.index_id = ips.index_id
                                                                    AND ic.is_included_column = 0
                                        ORDER BY    ic.key_ordinal
                                        FOR XML PATH(''))), '  ', ', '), ips.index_type_desc)  AS index_keys,
                ips.record_count,
                (ips.page_count / 128.0) AS space_used_in_MB,
                ips.avg_page_space_used_in_percent,
                CASE WHEN i.fill_factor = 0 THEN 100 ELSE i.fill_factor END AS fill_factor,
                8096 / (ips.max_record_size_in_bytes + 2.00) AS min_rows_per_page,
                8096 / (ips.avg_record_size_in_bytes + 2.00) AS avg_rows_per_page,
                8096 / (ips.min_record_size_in_bytes + 2.00) AS max_rows_per_page,
                8096 * ((100 - (CASE WHEN i.fill_factor = 0 THEN 100.00 ELSE i.fill_factor END)) / 100.00) / (ips.avg_record_size_in_bytes + 2.0000) AS defined_free_rows_per_page,
                8096 * ((100 - ips.avg_page_space_used_in_percent) / 100.00) / (ips.avg_record_size_in_bytes + 2) AS actual_free_rows_per_page,
                reads = ISNULL(ius.user_seeks, 0) + ISNULL(ius.user_scans, 0) + ISNULL(ius.user_lookups, 0),
                writes =  ISNULL(ius.user_updates, 0),
                1.00 * (ISNULL(ius.user_seeks, 0) + ISNULL(ius.user_scans, 0) + ISNULL(ius.user_lookups, 0)) / ISNULL(CASE WHEN ius.user_updates > 0 THEN ius.user_updates END, 1) AS reads_per_write
    FROM        sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'SAMPLED') ips
                INNER JOIN sys.indexes i
                    ON ips.object_id = i.object_id
                        AND ips.index_id = i.index_id
                LEFT OUTER JOIN sys.dm_db_index_usage_stats ius
                    ON ius.database_id = DB_ID()
                        AND ips.object_id = ius.object_id
                            AND ips.index_id = ius.index_id
    WHERE       ips.alloc_unit_type_desc != 'LOB_DATA'
    ORDER BY    ips.index_type_desc,
                OBJECT_NAME(ips.object_id),
                (ips.page_count / 128.0)

    The query should be very familiar to anyone that has looked at index fragmentation in SQL 2005 or newer. The same rules apply, the only difference is the columns that are being used. For larger databases consider limiting the scan to a single table or even a single index. It is also a good idea to ignore smaller tables here. I leave it up to the individual running the script to define a small table. For some that will be 100 pages, others 500 pages, but anything over 1000 pages should probably be looked at.

    The size calculations used in the query are based on the formulas found here: http://msdn.microsoft.com/en-us/library/ms178085(SQL.90).aspx, although the math is quite simple because the DMV accounts for things like null bitmaps and row version information.

    I assume that everyone will come up with slightly different ways to use the query. I like to make 2 passes over the data, the first in the morning and the second after the end of the business day. My first pass through the results is used to look for indexes that have too small of a fill factor set. They are easy to find because their free rows per page numbers are less than 1. A value of less than 1 means that the fill factor either needs to be changed to allow some free rows per page or to be more honest about the actual number of free rows per page. My second pass is used to look at the change over the course of the day. The best way to do the comparison is to paste both result sets into Excel and use formulas to look for differences. The second pass will show the indexes that have their factor set either too high or too low. The idea is to focus just as much on the indexes that show significant changes as much as those that do not show any changes at all.

    So there it is, a query to tell how good the current fill factor settings are.

    To make sure that all users stay as happy as possible it is best to run the query the first time during an off-peak time so that impact can be safely gauged.

    Please let me know if you run into any issues, have any ideas that would make this script better or just want to share how you are using it. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , , , , , | 13 Comments

    Quote of the Day

    I do not usually post a quote of the day but since this is my own quote I decided to share it.

    Today is the first day of the rest of your life. Make it yours, own the day. Repeat tomorrow. This has been your wake up call.

    Posted in General | Tagged | 3 Comments

    A Stored Procedure to Move SSIS Packages Between Servers

    Today’s post is one that I have been debating on whether to publish for a while. The purpose of the stored procedure I am sharing is to move SSIS packages stored via SQL Server Storage from one server SQL 2005 server to another in a way that can easily be invoked by any release management system that can call stored procedures. The part I have reservations about is that it uses linked servers. I almost never allow linked servers to be created on the servers I manage, mostly because they can be a security problem. Breaking the rules in this case is what was right for the particular problems I was trying to solve. Please consider whether you can implement this logic another way before using this stored procedure in your environment.

    This stored procedure is not terribly complicated so I will run through what it does fairly quickly. The first step is to get the folder_id of the package we want to copy. If it gets more than 1 folder name back it throws an error because it does not know which package to move. If the folder_id returned is null then an error is thrown. If the stored procedure makes it through those checks, the current version a the destination is deleted and the new version is copied there.

    Here is the code:

    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
    CREATE PROCEDURE dbo.move_ssis_package @from_server_name varchar(256), @to_server_name varchar(256), @package_name sysname

    AS

    DECLARE @sql_command nvarchar(4000),
    @folder_id uniqueidentifier,
    @foldername sysname

    SELECT @sql_command = 'SELECT @folder_id = pf2.[folderid]
    FROM ['
    + @from_server_name + '].[msdb].[dbo].[sysdtspackagefolders90] pf
    INNER JOIN ['
    + @from_server_name + '].[msdb].[dbo].[sysdtspackages90] p
    ON pf.folderid = p.folderid
    LEFT OUTER JOIN ['
    + @to_server_name + '].[msdb].[dbo].[sysdtspackagefolders90] pf2
    ON pf.[foldername] = pf2.[foldername]
    WHERE p.name = @package_name'


    EXEC sp_executesql @sql_command, N'@package_name sysname, @folder_id uniqueidentifier OUTPUT', @package_name = @package_name, @folder_id=@folder_id OUTPUT

    IF @@ROWCOUNT &gt; 1
    BEGIN
    RAISERROR ('This package exists in more than one location.', 16, 1)
    END

    IF @folder_id IS NULL
    BEGIN
    RAISERROR ('SSIS Folder does not exist.', 16, 1)
    END

    SELECT @sql_command = 'DELETE [' + @to_server_name + '].[msdb].[dbo].[sysdtspackages90]
    WHERE name = @package_name'


    EXEC sp_executesql @sql_command, N'@package_name sysname', @package_name = @package_name

    SELECT @sql_command = 'INSERT [' + @to_server_name + '].[msdb].[dbo].[sysdtspackages90]
    SELECT [name]
    ,[id]
    ,[description]
    ,[createdate]
    ,@folder_id AS [folderid]
    ,[ownersid]
    ,[packagedata]
    ,[packageformat]
    ,[packagetype]
    ,[vermajor]
    ,[verminor]
    ,[verbuild]
    ,[vercomments]
    ,[verid]
    ,[isencrypted]
    ,[readrolesid]
    ,[writerolesid]
    FROM ['
    + @from_server_name + '].[msdb].[dbo].[sysdtspackages90]
    WHERE name = @package_name'


    EXEC sp_executesql @sql_command, N'@package_name sysname, @folder_id uniqueidentifier', @package_name = @package_name, @folder_id=@folder_id

    Please let me know if you run into any issues, have any ideas that would make this stored procedure better or just want to share how you are using it. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , | 1 Comment

    Great News! SSMS Tools Pack 1.9 is Coming Out!

    Mladen Prajdic (Blog|Twitter) recently announced that the newest version of SSMS Tools Pack is coming out and I am excited.

    Why am I excited?

    Well, I am glad you asked.

    I am excited because it will allow me to define my window colors in SQL Management Studio using regular expressions rather than having to define them each individually.

    Why is that such a big deal?

    I have 100s of servers and I am constantly adding new servers while decommissioning old ones. The sheer amount of changes that would have to be made manually has always kept me from being able to take advantage of window coloring. Rather than have 100s of rules I now have less than 10 regular expressions that cover all of my servers. Here, check it out:

    SSMS Tools Pack Connection Coloring Options Window

    Naming convention changed to protect the employed.

    So is that all the SSMS Tools Pack does is color windows?

    No, not at all. The SSMS Tools Pack is a large suite of plug-ins for SQL Management Studio available as a free download. There are several features that I cannot live without. My favorite feature is that it can be configured to keep a journal of all queries you have run. This can be especially useful if you work in an environment where a sys admin can push updates that cause your machine to reboot whenever they feel like it. If you are not familiar with all of the features currently in the product then please go check out the list here.

    If you have not tried out the SSMS Tools Pack then I highly suggest you give version 1.9 a whirl. I know I will.

    Posted in Utilities | Tagged , , , , , | 2 Comments

    How Can I Quickly Script Out Replication?

    I recently finished taking down a bunch of servers that I was using to scale out my environment by forcing read only connections off of my main read-write servers. To make a long story short, hardware advances and the additional diagnostic information in SQL 2005 allowed me to consolidate to a few very powerful, reasonably well-tuned read-write servers. The consolidation of servers allowed me to save a ton of power and cooling along with some rack space and a good size chunk of SAN disk.

    Taking down the servers means that I now have to update all of my environment diagrams, server configuration scripts and even a spreadsheet or two. Anyone who has ever done this before is cringing right now. One of the worst tasks is updating the replication scripts. I script my replication settings to a network share just in case I do something silly and need to revert to my last know good setup. The scripts can really save my bacon but they are incredibly tedious to create. I have to go into Management Studio, right-click on each publication, select generate script, select script to file then finally find the existing file for that database to add to or decide there is not one and start a new file. With the amount of scripts I had to create it would have easily taken 4, make that 8 hours with interruptions to get everything scripted.

    Given, that the whole process would have taken hours and probably would have gotten screwed up along the way I decided to turn to PowerShell. Unfortunately, I did not have a script ready to go….WHHAAAT?…yeah I know..I don’t have a script for everything..so I threw the question out to Twitter. Aaron Nelson (Blog|Twitter) came back right away, pointing me toward SQL PowerShell Extensions (SQLPSX) and very quickly I had a working script. If you are not familiar with SQLPSX please take some time to check it out. It really makes coding PowerShell for SQL Server fast. More importantly, if you are not part of the SQL community on Twitter then get there first.

    The actual script is not terribly complex. It takes a distribution server name and an output directory as parameters then works through all publications on each of the servers that connects to the distribution server, scripting them out.

    I have only run this script against a dedicated distribution server but it should also work where the publisher is the distributor too.

    I spent about 4 hours throwing the script together and generated all of the scripts I needed in a little over 1 minute.

    With that, here is the script:

    Update: Chad Miller (Blog|Twitter) showed how this script could take better advantage of the features of SQLPSX. His version of the script is available here: http://sev17.com/2010/08/quickly-script-out-replication-redux/

    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
    param ([string]$sqlServer, [string]$outputDirectory, [bool]$scriptPerPublication)

    if ($sqlServer -eq "")
    {
        $sqlserver = Read-Host -Prompt "Please provide a value for -sqlServer"
    }

    if ($outputDirectory -eq "")
    {
        $outputDirectory = Read-Host -Prompt "Please provide a value for -outputDirectory"
    }

    function ScriptPublications
    {
        param ([string]$sqlServer, [string] $outputDirectory, [bool] $scriptPerPublication)
       
        Import-Module Repl
       
        [string] $path =  "$outputDirectory\$((get-date).toString('yyyy-MMM-dd_HHmmss'))\"
       
        New-Item $path -ItemType Directory | Out-Null
       
        foreach($publication in Get-ReplPublication $sqlServer)
        {
            [string] $fileName = "{0}{1}.sql" -f $path,$publication.DatabaseName.Replace(" ", "")
            if($scriptPerPublication)
            {
                $fileName = "{0}{1}_{2}.sql" -f $path,$publication.DatabaseName.Replace(" ", ""),$publication.Name.Replace(" ", "")
            }
            [string] $progressText = "Scripting {0} to {1}" -f $publication.Name.Replace(" ", ""),$fileName
            Write-Output $progressText
            $publication.Script([Microsoft.SqlServer.Replication.scriptoptions]::Creation `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::IncludeArticles `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::IncludePublisherSideSubscriptions `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::IncludeCreateSnapshotAgent `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::IncludeGo `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::EnableReplicationDB `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::IncludePublicationAccesses `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::IncludeCreateLogreaderAgent `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::IncludeCreateQueuereaderAgent `
                -bor  [Microsoft.SqlServer.Replication.scriptoptions]::IncludeSubscriberSideSubscriptions) | Out-File $fileName -Append
        }
    }

    [Microsoft.SqlServer.Management.Common.ServerConnection] $serverConnection = new-object Microsoft.SqlServer.Management.Common.ServerConnection($sqlServer)
    [Microsoft.SqlServer.Replication.ReplicationServer] $distributor = New-Object Microsoft.SqlServer.Replication.ReplicationServer($serverConnection);

    foreach($distributionPublisher in $distributor.DistributionPublishers)
    {
        if($distributionPublisher.PublisherType -eq "MSSQLSERVER")
        {
            [string] $path = $outputDirectory + "\from_" + $distributionPublisher.Name.Replace("\", "_")
            ScriptPublications -sqlServer $distributionPublisher.Name -outputDirectory $path -scriptPerPublication $false
        }
    }

    As usual, I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in PowerShell, Utilities | Tagged , | 4 Comments

    What is an Easy Way to Return Results from a CLR Stored Procedure?

    Introduction

    What is an Easy Way to Return Results from a CLR Stored Procedure? The question sounds simple enough but yet when I went searching for answers I could not find it. This post describes a helper class that I came up with to handle returning values from a CLR stored procedure.

    My Solution

    When I set out to write my first CLR stored procedure I expected to be able to do something easy, like write a method that returns an array and have SQL Server work out how to display it as a recordset. In the end I found that CLR works sort of like that, except that you have to figure out all the sizes, declare the structure then handle the passing back of each and every cell in each and every row. I guess that is OK, but if you have read any of my other posts you will have noticed a common theme: I am Lazy. Being as lazy as I am, I started digging into Intellisense to see what methods the various classes exposed to make my life easier. Pretty quickly I found SqlMetaData.InferFromValue to define the columns of the result without having to figure out what SQLMetaData type each column in the result set converted to.

    Armed with a way to quickly define a column in a recordset I started adding iterative code to walk through various types of objects. I started with walking a DataReader then added DataTables and DataSets, then finally progressed to using reflection to display all of the properties of an object or even all of the properties for all of the objects in an array. I have also added an optional debug flag to output information about the result set to make it easy to define a temporary table to hold the results. Now I have a helper class that I can reference from my CLR stored procedures to quickly return results without very much time spent coding.

    Here is an example to show how easy the helper class makes it to code a CLR stored procedure:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    using System;
    using System.IO;
    using AdventuresInSql;

    public partial class StoredProcedures
    {
        [Microsoft.SqlServer.Server.SqlProcedure]
        public static void FileInfo(String filePath)
        {
            FileInfo fileInfo = new FileInfo(filePath);
            SqlClrHelper.RenderResults(fileInfo);
        }
    };

    On my systems, I deploy the helper class in it’s own assembly, add the assembly to the server I want to develop against, then open a new project, connect to that server, reference that assembly and write my code. I realize that most people are not using CLR in any distributed manner, making it easiest to just include the class in their project and run with it there.

    Warnings: I highly suggest taking the time to deploy this class in it’s own assembly. The assembly this class resides in has to be marked UNSAFE. The database this assembly is deployed to must also be marked TRUSTWORTHY so I highly suggest keeping CLR objects in their own highly secured database. Most importantly, if you do not know what these setttings do stop now and find out before moving any further. UPDATE: Per Adam Machanic’s (Blog|Twitter) comments below, the TRUSTWORTHY setting is not needed if you use certificates.

    With that, here is the code for the helper class Updated 7/26/2010 to better handle null values, increase performance and make easier to use. The biggest changes are switching to generic methods rather than using object typed parameters and getting column definitions more efficiently.:

    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
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Data.OleDb;
    using System.Data.SqlClient;
    using System.Globalization;
    using System.Reflection;
    using Microsoft.SqlServer.Server;

    public sealed class SqlClrHelper
    {

        #region Member Variables

        private const String ARGUMENT_EXCEPTION_STRING = "Extract of property: \"{0}\" failed with the following message: {1}";
        private const String COLUMN_NAME = "ColumnName";
        private const String COLUMN_SIZE = "ColumnSize";
        private const String DATA_TYPE = "DataType";
        private const String DEBUG_WARNING_MESSAGE = "***Turn off debug before trying to select into a table to avoid conversion exceptions***";
        private const String OBJECT_TYPE_DIFFERENT_EXCEPTION = "All objects in objectsToRender[] must be of the same type.";
        private const String TO_STRING = "ToString()";

        #endregion

        #region Internal Methods

        /// <summary>  
        ///<para>Class will only ever contain static methods.
        ///Added private constructor to prevent compiler from generating default constructor.</para>  
        /// </summary>  
        private SqlClrHelper()
        {
        }

        /// <summary>  
        ///<para>This method takes a column name, type and maximum length, returning the column definition as SqlMetaData.</para>  
        /// </summary>
        /// <param name="System.String">A column name to be used in the returned Microsoft.SqlServer.Server.SqlMetaData.</param>
        /// <param name="System.Type">A column data type to be used in the returned Microsoft.SqlServer.Server.SqlMetaData.</param>
        /// <param name="System.Int32">The maximum length of the column to be used in the returned Microsoft.SqlServer.Server.SqlMetaData.</param>
        private static SqlMetaData ParseSqlMetaData(String columnName, Type type, Int64 maxLength)
        {
            SqlParameter sqlParameter = new SqlParameter();
            sqlParameter.DbType = (DbType)TypeDescriptor.GetConverter(sqlParameter.DbType).ConvertFrom(type.Name);
            if (sqlParameter.SqlDbType == SqlDbType.Char || sqlParameter.SqlDbType == SqlDbType.NChar || sqlParameter.SqlDbType == SqlDbType.NVarChar || sqlParameter.SqlDbType == SqlDbType.VarChar)
            {
                if (maxLength > 8000)
                {
                    maxLength = -1;
                }
                return new SqlMetaData(columnName, sqlParameter.SqlDbType, maxLength);
            }
            else if (sqlParameter.SqlDbType == SqlDbType.Text || sqlParameter.SqlDbType == SqlDbType.NText)
            {
                return new SqlMetaData(columnName, sqlParameter.SqlDbType, -1);
            }
            else
            {
                return new SqlMetaData(columnName, sqlParameter.SqlDbType);
            }
        }

        /// <summary>  
        ///<para>This method takes a single object and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="<T>">A populated object.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        private static void RenderResults<T>(T objectToRender)
        {
            RenderResults(objectToRender, false);
        }

        /// <summary>  
        ///<para>This method takes the SqlMetaData created to render an object and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="System.Collections.Generic.List<Microsoft.SqlServer.Server>">A reference to a populated SqlMetaData List.</param>
        private static void WriteRecordStructure(List<SqlMetaData> sqlMetaDataList)
        {
            RenderResults(sqlMetaDataList.ToArray(), false);
        }

        #endregion

        #region Public Methods

        #region RenderResults Overloads

        #region Pass-Through Overloads

        /// <summary>  
        ///<para>This method takes a DataSet and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="System.Data.DataSet">A reference to a populated DataSet.</param>  
        public static void RenderResults(DataSet dataSet)
        {
            RenderResults(dataSet);
        }

        /// <summary>  
        ///<para>This method takes a DataSet and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="System.Data.DataSet">A reference to a populated DataSet.</param>
        /// <param name="System.Boolean">A boolean value indicating whether or not to return information
        /// about the record structure to the client.</param>  
        public static void RenderResults(DataSet dataSet, Boolean isDebugOn)
        {
            foreach (DataTable dataTable in dataSet.Tables)
            {
                RenderResults(dataTable, isDebugOn);
            }
        }

        /// <summary>  
        ///<para>This method takes a DataTable and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="System.Data.DataTable">A reference to a populated DataTable.</param>  
        public static void RenderResults(DataTable dataTable)
        {
            RenderResults(dataTable, false);
        }

        /// <summary>  
        ///<para>This method takes an OleDbDataReader and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="System.Data.OleDb.OleDbDataReader">A reference to a populated OleDbDataReader.</param>
        public static void RenderResults(OleDbDataReader dataReader)
        {
            RenderResults(dataReader, false);
        }

        /// <summary>  
        ///<para>This method takes a single object and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="<T>">A reference to a populated object.</param>
        /// <param name="System.Boolean">A boolean value indicating whether or not to return information
        /// about failed argument exceptions and record structure to the client.</param>
        public static void RenderResults<T>(T objectToRender, Boolean isDebugOn)
        {
            T[] objectsToRender = new T[1];
            objectsToRender[0] = objectToRender;
            RenderResults(objectsToRender, isDebugOn);
        }

        /// <summary>  
        ///<para>This method takes an array of objects and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="System.Object[]">A reference to a populated object.</param>
        public static void RenderResults<T>(T[] objectsToRender)
        {
            RenderResults(objectsToRender, false);
        }

        #endregion

        /// <summary>  
        ///<para>This method takes a DataTable and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="System.Data.DataTable">A reference to a populated DataTable.</param>  
        /// <param name="System.Boolean">A boolean value indicating whether or not to return information
        /// about the record structure to the client.</param>  
        public static void RenderResults(DataTable dataTable, Boolean isDebugOn)
        {
            List<SqlMetaData> sqlMetaDataList = new List<SqlMetaData>();
            for (int i = 0; i < dataTable.Rows[0].ItemArray.Length; i++)
            {
                sqlMetaDataList.Add(ParseSqlMetaData(dataTable.Columns[i].ColumnName, dataTable.Columns[i].DataType, dataTable.Columns[i].MaxLength));
            }
            SqlDataRecord sqlDataRecord = new SqlDataRecord(sqlMetaDataList.ToArray());
            SqlContext.Pipe.SendResultsStart(sqlDataRecord);
            if (SqlContext.Pipe.IsSendingResults)
            {
                foreach (DataRow dataRow in dataTable.Rows)
                {
                    sqlDataRecord.SetValues(dataRow.ItemArray);
                    SqlContext.Pipe.SendResultsRow(sqlDataRecord);
                }
                SqlContext.Pipe.SendResultsEnd();
            }
            if (isDebugOn)
            {
                WriteRecordStructure(sqlMetaDataList);
            }
        }

        /// <summary>  
        ///<para>This method takes an OleDbDataReader and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="System.Data.OleDb.OleDbDataReader">A reference to a populated OleDbDataReader.</param>
        /// <param name="System.Boolean">A boolean value indicating whether or not to return information
        /// about the record structure to the client.</param>  
        public static void RenderResults(OleDbDataReader oleDBDataReader, Boolean isDebugOn)
        {
            Int64 columnSize = 0;
            List<SqlMetaData> sqlMetaDataList = new List<SqlMetaData>();
            foreach (DataRow dataRow in oleDBDataReader.GetSchemaTable().Rows)
            {
                if (Int64.TryParse(((Int32)dataRow[COLUMN_SIZE]).ToString(CultureInfo.CurrentCulture), out columnSize))
                {
                    sqlMetaDataList.Add(ParseSqlMetaData((String)dataRow[COLUMN_NAME], (Type)dataRow[DATA_TYPE], columnSize));
                }
                else
                {
                    sqlMetaDataList.Add(ParseSqlMetaData((String)dataRow[COLUMN_NAME], (Type)dataRow[DATA_TYPE], -1));
                }
            }
            SqlDataRecord sqlDataRecord = new SqlDataRecord(sqlMetaDataList.ToArray());
            Object[] objects = new Object[sqlMetaDataList.Count];
            SqlContext.Pipe.SendResultsStart(sqlDataRecord);
            if (SqlContext.Pipe.IsSendingResults)
            {
                while (oleDBDataReader.Read())
                {
                    oleDBDataReader.GetValues(objects);
                    sqlDataRecord.SetValues(objects);
                    SqlContext.Pipe.SendResultsRow(sqlDataRecord);
                }
                SqlContext.Pipe.SendResultsEnd();
            }
            if (isDebugOn)
            {
                WriteRecordStructure(sqlMetaDataList);
            }
            if (oleDBDataReader.NextResult())
            {
                RenderResults(oleDBDataReader, isDebugOn);
            }
        }

        /// <summary>  
        ///<para>This method takes an array of objects and renders it back to the client.</para>  
        /// </summary>  
        /// <param name="<T>[]">A reference to an array of populated objects.</param>
        /// <param name="System.Boolean">A boolean value indicating whether or not to return information
        /// about failed argument exceptions and record structure to the client.</param>
        public static void RenderResults<T>(T[] objectsToRender, Boolean isDebugOn)
        {
            List<SqlMetaData> sqlMetaDataList = new List<SqlMetaData>();
            List<List<Object>> sqlMetaDataValues = new List<List<Object>>();
            SqlDataRecord sqlDataRecord = null;
            Type objectType = null;
            for (int i = 0; i < objectsToRender.Length; i++)
            {
                if (objectsToRender[i] == null)
                {
                    continue;
                }
                T objectToRender = objectsToRender[i];
                if (objectType == null)
                {
                    objectType = objectToRender.GetType();
                }
                if (objectToRender.GetType() != objectType)
                {
                    throw (new InvalidCastException(OBJECT_TYPE_DIFFERENT_EXCEPTION));
                }
                foreach (PropertyInfo property in objectToRender.GetType().GetProperties())
                {
                    SqlMetaData sqlMetaData = null;
                    if (property.CanRead && property.GetIndexParameters().Length == 0)
                    {
                        try
                        {
                            sqlMetaData = SqlMetaData.InferFromValue(property.GetValue(objectToRender, null), property.Name.ToString());
                            for (int j = 0; j < sqlMetaDataList.Count; j++)
                            {
                                if (sqlMetaDataList[j].Name == sqlMetaData.Name)
                                {
                                    if (sqlMetaDataList[j].MaxLength < sqlMetaData.MaxLength)
                                    {
                                        sqlMetaDataList[j] = sqlMetaData;
                                    }
                                    sqlMetaData = null;
                                    break;
                                }
                            }
                            if (sqlMetaData != null)
                            {
                                sqlMetaDataList.Add(sqlMetaData);
                            }
                            if (sqlMetaDataValues.Count == i)
                            {
                                sqlMetaDataValues.Add(new List<Object>());
                            }
                            sqlMetaDataValues[i].Add(property.GetValue(objectToRender, null));
                        }
                        catch (ArgumentException ex)
                        {
                            if (isDebugOn)
                            {
                                SqlContext.Pipe.Send(String.Format(CultureInfo.CurrentCulture, ARGUMENT_EXCEPTION_STRING, property.Name.ToString(), ex.Message.ToString()));
                            }
                        }
                    }
                }
                if (i == 0)
                {
                    sqlMetaDataList.Add(SqlMetaData.InferFromValue(objectToRender.ToString(), TO_STRING));
                }
                sqlMetaDataValues[i].Add(objectToRender.ToString());
            }
            sqlDataRecord = new SqlDataRecord(sqlMetaDataList.ToArray());
            SqlContext.Pipe.SendResultsStart(sqlDataRecord);
            if (SqlContext.Pipe.IsSendingResults)
            {
                sqlMetaDataValues.ForEach(sqlMetaDataValue =>
                {
                    sqlDataRecord.SetValues(sqlMetaDataValue.ToArray());
                    SqlContext.Pipe.SendResultsRow(sqlDataRecord);
                });
                SqlContext.Pipe.SendResultsEnd();
            }
            if (isDebugOn)
            {
                WriteRecordStructure(sqlMetaDataList);
            }

        }

        #endregion

        #endregion

    }

    Conclusion

    I fear that by making it easy to use CLR that I may be opening up a can of worms. I ask that before using CLR that you make sure that it is the best way to accomplish the task you have been given.

    As usual, I hope you find this class useful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that code from the internet is like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in CLR | Tagged | 5 Comments

    Why Would a Delete Make My Database Grow?

    Introduction

    A while back I had a developer come to me complaining that every time they ran a large delete statement on a certain database the delete would fail with a message claiming the database was full. My first instinct was that they were doing something wrong so I asked for the script so I could try it myself. To my surprise, running the delete actually did fill the database.

    Troubleshooting

    To figure out why a delete would cause a database to grow, I started with Profiler to see if anything was running as a side-effect of the deletes. The only thing that Profiler showed was the delete. Unable to explain what was happening, I threw the question to #SqlHelp on Twitter. Almost immediately, Paul Randal (Blog|Twitter) asked if I had Read Committed Snapshot Isolation (RCSI) turned on for that database. I confirmed that the database did in fact have RCSI turned on and Paul explained that what I was seeing was SQL Server adding the pointers to the version store to the data pages as they are marked deleted.

    Moving Forward

    Once I knew what the issue was my mind began to shift gears into how to prevent it from biting me in production. The obvious answer is to set the database size to be sufficiently high enough to allow extra space for large updates or deletes to add version information to the data pages. The problem with this approach is that eventually the reason for the free space will be forgotten and normal growth of the database will eventually eat up that free space.

    While looking for options I remembered how I had a similar experience rebuilding indexes to move them to a new file group on new disk. The idea of the project was to move the database to new disk with minimal downtime. To accomplish the move I created a new file group and rebuilt all of the indexes onto it, starting with clustered indexes. Once the primary data file was down to just the system tables I shrunk it, took the whole database offline, moved the file and brought the database back online. The total downtime was about 15 seconds but all of the work took about a week. The work I was doing had to be minimally disruptive so I used the ONLINE flag along with DROP_EXISTING to recreate the indexes.  I was surprised to find out at the end of that work to find out that my database had grown significantly in size. After a ton of research I discovered that the ONLINE flag was adding version information to each page, leading to the unexpected growth.

    Could the version information for online index operations be the same as what is used by Read Committed Snapshot Isolation?

    Could rebuilding all of the indexes in my database with help me to pre-size my database, avoiding later surprises in production?

    How would I go about proving my theory?

    The Proof

    To prove my theory that rebuilding my indexes with would allow me to pre-size my database; I found a quiet corner of the development environment and created a test database.

    Here is the script to create the database if you should choose to follow along at home:

    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
    USE master
    GO
    IF EXISTS(SELECT * FROM sys.databases WHERE name = 'RecordSizeTest')
        DROP DATABASE RecordSizeTest
    GO

    CREATE DATABASE RecordSizeTest
    GO

    USE RecordSizeTest
    GO
    IF EXISTS(SELECT * FROM sys.tables WHERE name = 'TestTable')
        DROP TABLE dbo.TestTable
    GO

    CREATE TABLE dbo.TestTable
    (
        id              int identity(1,1) NOT NULL,
        varchar_value   varchar(400) NOT NULL,
        bit_value       bit NOT NULL,
        create_date     smalldatetime NOT NULL DEFAULT(GETDATE())
    )
    GO

    CREATE UNIQUE CLUSTERED INDEX IX_TestTable_id ON dbo.TestTable (id)
    GO

    INSERT  dbo.TestTable (varchar_value, bit_value)
        SELECT  REPLICATE('TEST', COUNT(*)),
                COUNT(*) % 2
        FROM    dbo.TestTable
    GO 100

    ALTER DATABASE RecordSizeTest SET READ_COMMITTED_SNAPSHOT ON
    GO

    SELECT * FROM dbo.TestTable

    Now that the database is created, the first step is to see what the database pages look like by default using DBCC IND and DBCC PAGE.

    Note: The commands I am using are well-covered elsewhere so I am not going to spend any time describing them beyond just showing how I used them. It goes without saying that you should not run anything on your systems without first taking the time to understand what it does.

    The first step is to figure out where the table ended up. Time for DBCC IND:

    1
    2
    DBCC IND(RecordSizeTest, TestTable, 0)
    GO

    Below are the results of the DBCC IND command. To keep things easy I am looking for the first page of the table. To find it I look for a page that has a PrevPageId of 0 and a PageType of 1. In this case the page I am looking for is 145.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    PageFID PagePID     IAMFID IAMPID      ObjectID    IndexID     PartitionNumber PartitionID          iam_chain_type       PageType IndexLevel NextPageFID NextPagePID PrevPageFID PrevPagePID
    ------- ----------- ------ ----------- ----------- ----------- --------------- -------------------- -------------------- -------- ---------- ----------- ----------- ----------- -----------
    1       146         NULL   NULL        2105058535  1           1               72057594038845440    In-row data          10       NULL       0           0           0           0
    1       145         1      146         2105058535  1           1               72057594038845440    In-row data          1        0          1           150         0           0
    1       150         1      146         2105058535  1           1               72057594038845440    In-row data          1        0          1           153         1           145
    1       153         1      146         2105058535  1           1               72057594038845440    In-row data          1        0          0           0           1           150

    (4 row(s) affected)

    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    Next I want to get a look at that page so I run the following command:

    1
    2
    3
    4
    5
    DBCC TRACEON(3604)
    GO

    DBCC PAGE(RecordSizeTest, 1, 145, 3)
    GO

    Below are the DBCC PAGE results. The 2 things to really notice are that free space on the page, m_freeCnt is currently 212 bytes and that the values for Record Attributes are NULL_BITMAP and VARIABLE_COLUMNS. While we are looking at this page I am also making sure that the record in slot 0 has an id of 1 for the next step in the test.

    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
    PAGE: (1:145)


    BUFFER:


    BUF @0x0000000090FC6300

    bpage = 0x000000009018C000           bhash = 0x0000000000000000           bpageno = (1:145)
    bdbid = 6                            breferences = 0                      bUse1 = 31047
    bstat = 0x6c00009                    blog = 0x21432159                    bnext = 0x0000000000000000

    PAGE HEADER:


    Page @0x000000009018C000

    m_pageId = (1:145)                   m_headerVersion = 1                  m_type = 1
    m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x200
    m_objId (AllocUnitId.idObj) = 28     m_indexId (AllocUnitId.idInd) = 256  
    Metadata: AllocUnitId = 72057594039762944                                
    Metadata: PartitionId = 72057594038845440                                 Metadata: IndexId = 1
    Metadata: ObjectId = 2105058535      m_prevPage = (0:0)                   m_nextPage = (1:150)
    pminlen = 13                         m_slotCnt = 58                       m_freeCnt = 212
    m_freeData = 7864                    m_reservedCnt = 0                    m_lsn = (34:159:16)
    m_xactReserved = 0                   m_xdesId = (0:0)                     m_ghostRecCnt = 0
    m_tornBits = -436072588              

    Allocation Status

    GAM (1:2) = ALLOCATED                SGAM (1:3) = NOT ALLOCATED          
    PFS (1:1) = 0x60 MIXED_EXT ALLOCATED   0_PCT_FULL                         DIFF (1:6) = CHANGED
    ML (1:7) = NOT MIN_LOGGED            

    Slot 0 Offset 0x60 Length 16

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP     Record Size = 16

    Memory Dump @0x000000002309A060

    0000000000000000:   10000d00 01000000 00530384 9d040000 †.........S.„....

    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 1                              

    Slot 0 Column 2 Offset 0x0 Length 0 Length (physical) 0

    varchar_value =                      

    Slot 0 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 0 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 0 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (010086470766)        
    Slot 1 Offset 0x70 Length 24

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 24                    
    Memory Dump @0x000000002309A070

    0000000000000000:   30000d00 02000000 01530384 9d040000 †0........S.„....
    0000000000000010:   01001800 54455354 †††††††††††††††††††....TEST        

    Slot 1 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 2                              

    Slot 1 Column 2 Offset 0x14 Length 4 Length (physical) 4

    varchar_value = TEST                

    Slot 1 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 1 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 1 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (020068e8b274)        
    Slot 2 Offset 0x88 Length 28

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 28                    
    Memory Dump @0x000000002309A088

    0000000000000000:   30000d00 03000000 00530384 9d040000 †0........S.„....
    0000000000000010:   01001c00 54455354 54455354 ††††††††††....TESTTEST    
    <snip>
    .
    .
    .
    </snip
    Slot 57 Offset 0x1dc0 Length 248

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 248                    
    Memory Dump @0x000000002309BDC0

    0000000000000000:   30000d00 3a000000 01530384 9d040000 †0...:....S.„....
    0000000000000010:   0100f800 54455354 54455354 54455354 †..ø.TESTTESTTEST
    0000000000000020:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000030:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000040:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000050:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000060:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000070:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000080:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000090:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000A0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000B0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000C0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000D0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000E0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000F0:   54455354 54455354 †††††††††††††††††††TESTTEST        

    Slot 57 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 58                              

    Slot 57 Column 2 Offset 0x14 Length 228 Length (physical) 228

    varchar_value = TESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTT
    ESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTE
    STTESTTESTTESTTEST                  

    Slot 57 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 57 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 57 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (3a0026382d41)        


    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    Next I want to delete the first row from the table to see what effect that has. I picked the first row because I already know what page it is on so I can quickly see the impact, if any, of deleting it. I chose to do this in a transaction to also see what effect a rollback might have. Here is the next bit of code in the test:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    DBCC TRACEON(3604)
    GO

    BEGIN TRANSACTION

    DELETE  dbo.TestTable
    WHERE   id = 1
    GO

    DBCC PAGE(RecordSizeTest, 1, 145, 3)
    GO

    ROLLBACK

    DBCC PAGE(RecordSizeTest, 1, 145, 3)
    GO

    Below are the latest DBCC PAGE results from before the rollback. Right away it is clear that the row in Slot 0 has been deleted because it’s Record Type is now GHOST_DATA_RECORD. It is also noteable that even though the row has been marked deleted the m_freeCnt on the page has gone down to 198. Sticking with our theory, the reduction in free space should be caused by the addition of version information and, sure enough, the Record Attributes now include VERSIONING_INFO and a new Version Information section is visible with a Transaction Timestamp and a Version Pointer to a location in TempDB. We have now proven that we know how to make a page grow on demand by running a delete.

    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
    PAGE: (1:145)


    BUFFER:


    BUF @0x0000000090FC6300

    bpage = 0x000000009018C000           bhash = 0x0000000000000000           bpageno = (1:145)
    bdbid = 6                            breferences = 1                      bUse1 = 31887
    bstat = 0x6c0000b                    blog = 0x21432159                    bnext = 0x0000000000000000

    PAGE HEADER:


    Page @0x000000009018C000

    m_pageId = (1:145)                   m_headerVersion = 1                  m_type = 1
    m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x2000
    m_objId (AllocUnitId.idObj) = 28     m_indexId (AllocUnitId.idInd) = 256  
    Metadata: AllocUnitId = 72057594039762944                                
    Metadata: PartitionId = 72057594038845440                                 Metadata: IndexId = 1
    Metadata: ObjectId = 2105058535      m_prevPage = (0:0)                   m_nextPage = (1:150)
    pminlen = 13                         m_slotCnt = 58                       m_freeCnt = 198
    m_freeData = 7956                    m_reservedCnt = 0                    m_lsn = (34:324:10)
    m_xactReserved = 0                   m_xdesId = (0:973)                   m_ghostRecCnt = 1
    m_tornBits = -436072588              

    Allocation Status

    GAM (1:2) = ALLOCATED                SGAM (1:3) = NOT ALLOCATED          
    PFS (1:1) = 0x68 MIXED_EXT ALLOCATED   0_PCT_FULL                         DIFF (1:6) = CHANGED
    ML (1:7) = NOT MIN_LOGGED            

    Slot 0 Offset 0x1ef6 Length 30

    Record Type = GHOST_DATA_RECORD      Record Attributes =  NULL_BITMAP VERSIONING_INFO
    Record Size = 30                    
    Memory Dump @0x000000002309BEF6

    0000000000000000:   5c000d00 01000000 00530384 9d040000 †\........S.„....
    0000000000000010:   b0010000 01000000 3dbd0500 0000††††††°.......=½....  

    Version Information =
        Transaction Timestamp: 376125
        Version Pointer: (file 1 page 432 currentSlotId 0)


    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 1                              

    Slot 0 Column 2 Offset 0x0 Length 0 Length (physical) 0

    varchar_value =                      

    Slot 0 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 0 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 0 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (010086470766)        
    Slot 1 Offset 0x1ede Length 24

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 24                    
    Memory Dump @0x000000002309BEDE

    0000000000000000:   30000d00 02000000 01530384 9d040000 †0........S.„....
    0000000000000010:   01001800 54455354 †††††††††††††††††††....TEST        

    Slot 1 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 2                              

    Slot 1 Column 2 Offset 0x14 Length 4 Length (physical) 4

    varchar_value = TEST                

    Slot 1 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 1 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 1 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (020068e8b274)        
    Slot 2 Offset 0x88 Length 28

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 28                    
    Memory Dump @0x000000002309A088

    0000000000000000:   30000d00 03000000 00530384 9d040000 †0........S.„....
    0000000000000010:   01001c00 54455354 54455354 ††††††††††....TESTTEST    
    <snip>
    .
    .
    .
    </snip>
    Slot 57 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 58                              

    Slot 57 Column 2 Offset 0x14 Length 228 Length (physical) 228

    varchar_value = TESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTT
    ESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTE
    STTESTTESTTESTTEST                  

    Slot 57 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 57 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 57 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (3a0026382d41)        


    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    Next are the DBCC PAGE results from after the rollback. The m_freeCnt has gone back to 212, the row in slot 0 no longer shows as deleted and the versioning information has been removed. The fact that the versioning information goes away as part of the rollback is interesting. It means that no matter how many times I try to do a delete that fills the database I will always start from the same point. It makes perfect sense in terms of ACID but until I saw it for myself I was not sure how it would work.

    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
    PAGE: (1:145)


    BUFFER:


    BUF @0x0000000090FC6300

    bpage = 0x000000009018C000           bhash = 0x0000000000000000           bpageno = (1:145)
    bdbid = 6                            breferences = 0                      bUse1 = 32941
    bstat = 0x6c0000b                    blog = 0x21432159                    bnext = 0x0000000000000000

    PAGE HEADER:


    Page @0x000000009018C000

    m_pageId = (1:145)                   m_headerVersion = 1                  m_type = 1
    m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x6000
    m_objId (AllocUnitId.idObj) = 28     m_indexId (AllocUnitId.idInd) = 256  
    Metadata: AllocUnitId = 72057594039762944                                
    Metadata: PartitionId = 72057594038845440                                 Metadata: IndexId = 1
    Metadata: ObjectId = 2105058535      m_prevPage = (0:0)                   m_nextPage = (1:150)
    pminlen = 13                         m_slotCnt = 58                       m_freeCnt = 212
    m_freeData = 7972                    m_reservedCnt = 0                    m_lsn = (34:324:13)
    m_xactReserved = 0                   m_xdesId = (0:973)                   m_ghostRecCnt = 0
    m_tornBits = -436072588              

    Allocation Status

    GAM (1:2) = ALLOCATED                SGAM (1:3) = NOT ALLOCATED          
    PFS (1:1) = 0x60 MIXED_EXT ALLOCATED   0_PCT_FULL                         DIFF (1:6) = CHANGED
    ML (1:7) = NOT MIN_LOGGED            

    Slot 0 Offset 0x1f14 Length 16

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP     Record Size = 16

    Memory Dump @0x000000002309BF14

    0000000000000000:   10000d00 01000000 00530384 9d040000 †.........S.„....

    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 1                              

    Slot 0 Column 2 Offset 0x0 Length 0 Length (physical) 0

    varchar_value =                      

    Slot 0 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 0 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 0 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (010086470766)        
    Slot 1 Offset 0x1ede Length 24

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 24                    
    Memory Dump @0x000000002309BEDE

    0000000000000000:   30000d00 02000000 01530384 9d040000 †0........S.„....
    0000000000000010:   01001800 54455354 †††††††††††††††††††....TEST        

    Slot 1 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 2                              

    Slot 1 Column 2 Offset 0x14 Length 4 Length (physical) 4

    varchar_value = TEST                

    Slot 1 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 1 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 1 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (020068e8b274)
    <snip>
    .
    .
    .
    </snip>
    Slot 57 Offset 0x1dc0 Length 248

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
    Record Size = 248                    
    Memory Dump @0x000000002309BDC0

    0000000000000000:   30000d00 3a000000 01530384 9d040000 †0...:....S.„....
    0000000000000010:   0100f800 54455354 54455354 54455354 †..ø.TESTTESTTEST
    0000000000000020:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000030:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000040:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000050:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000060:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000070:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000080:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000090:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000A0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000B0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000C0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000D0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000E0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000F0:   54455354 54455354 †††††††††††††††††††TESTTEST        

    Slot 57 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 58                              

    Slot 57 Column 2 Offset 0x14 Length 228 Length (physical) 228

    varchar_value = TESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTT
    ESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTE
    STTESTTESTTESTTEST                  

    Slot 57 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 57 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 57 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (3a0026382d41)        


    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    Now that I have proven that I can delete a record in a database that uses Read Committed Snapshot Isolation and cause space usage to increase I now want to repeat the test after rebuilding the clustered index on the table with ONLINE=ON. Here is the next bit of code to run:

    1
    2
    CREATE UNIQUE CLUSTERED INDEX IX_TestTable_id ON dbo.TestTable (id) WITH (ONLINE=ON, DROP_EXISTING=ON)
    GO

    Now that the index has been rebuilt it is time to figure out where it ended up. Time for DBCC IND:

    1
    2
    DBCC IND(RecordSizeTest, TestTable, 0)
    GO

    Based on the results of DBCC IND we are looking for Page 156. It is noteable that DBCC IND returned 5 rows this time instead of 4. More pages generally means more data so lets dig into it.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    PageFID PagePID     IAMFID IAMPID      ObjectID    IndexID     PartitionNumber PartitionID          iam_chain_type       PageType IndexLevel NextPageFID NextPagePID PrevPageFID PrevPagePID
    ------- ----------- ------ ----------- ----------- ----------- --------------- -------------------- -------------------- -------- ---------- ----------- ----------- ----------- -----------
    1       157         NULL   NULL        2105058535  1           1               72057594038910976    In-row data          10       NULL       0           0           0           0
    1       156         1      157         2105058535  1           1               72057594038910976    In-row data          1        0          1           160         0           0
    1       160         1      157         2105058535  1           1               72057594038910976    In-row data          1        0          1           161         1           156
    1       161         1      157         2105058535  1           1               72057594038910976    In-row data          1        0          1           162         1           160
    1       162         1      157         2105058535  1           1               72057594038910976    In-row data          1        0          0           0           1           161

    (5 row(s) affected)

    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    Here is the syntax of the next DBCC PAGE command to run:

    1
    2
    3
    4
    5
    DBCC TRACEON(3604)
    GO

    DBCC PAGE(RecordSizeTest, 1, 156, 3)
    GO

    The results below seem quite different. First off, m_freeCnt is 680 instead of 212. Adding version information should not increase free space so there must be less records here, m_slotCnt proves that. This page has 53 slots or rows while the earlier page held 58 rows of data. That proves that the extra row in DBCC IND is an extra row of data that was added after the rebuild of the index to add the versioning information. Looking at the record in slot 0, it now looks like it did before the rollback of the delete. The Record Attributes include VERSIONING_INFO and there is a Version Information section just below that. The Version Information includes a Transaction Timestamp from when the index was rebuilt but no Version Pointer because the row is unchanged.

    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
    PAGE: (1:156)


    BUFFER:


    BUF @0x0000000090FC6080

    bpage = 0x0000000090182000           bhash = 0x0000000000000000           bpageno = (1:156)
    bdbid = 6                            breferences = 0                      bUse1 = 34110
    bstat = 0x6c0000b                    blog = 0x432159bb                    bnext = 0x0000000000000000

    PAGE HEADER:


    Page @0x0000000090182000

    m_pageId = (1:156)                   m_headerVersion = 1                  m_type = 1
    m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x2000
    m_objId (AllocUnitId.idObj) = 29     m_indexId (AllocUnitId.idInd) = 256  
    Metadata: AllocUnitId = 72057594039828480                                
    Metadata: PartitionId = 72057594038910976                                 Metadata: IndexId = 1
    Metadata: ObjectId = 2105058535      m_prevPage = (0:0)                   m_nextPage = (1:160)
    pminlen = 13                         m_slotCnt = 53                       m_freeCnt = 680
    m_freeData = 7406                    m_reservedCnt = 0                    m_lsn = (34:385:19)
    m_xactReserved = 0                   m_xdesId = (0:0)                     m_ghostRecCnt = 0
    m_tornBits = 0                      

    Allocation Status

    GAM (1:2) = ALLOCATED                SGAM (1:3) = ALLOCATED              
    PFS (1:1) = 0x60 MIXED_EXT ALLOCATED   0_PCT_FULL                         DIFF (1:6) = CHANGED
    ML (1:7) = NOT MIN_LOGGED            

    Slot 0 Offset 0x60 Length 30

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VERSIONING_INFO
    Record Size = 30                    
    Memory Dump @0x0000000018A4A060

    0000000000000000:   50000d00 01000000 d0530384 9d040000 †P.......ÐS.„....
    0000000000000010:   00000000 00000000 47be0500 0000††††††........G¾....  

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 1                              

    Slot 0 Column 2 Offset 0x0 Length 0 Length (physical) 0

    varchar_value =                      

    Slot 0 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 0 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 0 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (010086470766)        
    Slot 1 Offset 0x7e Length 38

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS VERSIONING_INFO
    Record Size = 38                    
    Memory Dump @0x0000000018A4A07E

    0000000000000000:   70000d00 02000000 d1530384 9d040000 †p.......ÑS.„....
    0000000000000010:   01001800 54455354 00000000 00000000 †....TEST........
    0000000000000020:   47be0500 0000††††††††††††††††††††††††G¾....          

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 1 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 2                              

    Slot 1 Column 2 Offset 0x14 Length 4 Length (physical) 4

    varchar_value = TEST                

    Slot 1 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 1 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 1 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (020068e8b274)  
    <snip>
    .
    .
    .
    </snip>
    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 52 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 53                              

    Slot 52 Column 2 Offset 0x14 Length 208 Length (physical) 208

    varchar_value = TESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTT
    ESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTEST

    Slot 52 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 52 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 52 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (350070284e19)        


    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    Now that the versioning information has been added it is time to re-run the delete test to see what effect a delete and subsequent rollback has. Here is the code for this test:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    DBCC TRACEON(3604)
    GO

    BEGIN TRANSACTION

    DELETE  dbo.TestTable
    WHERE   id = 1
    GO

    DBCC PAGE(RecordSizeTest, 1, 156, 3)
    GO

    ROLLBACK

    DBCC PAGE(RecordSizeTest, 1, 156, 3)
    GO

    The first set of DBCC PAGE results shows exactly what I expected. The record in slot 0 is marked as a GHOST_DATA_RECORD and the version pointer is now populated with a pointer to a location in TempDB. Note: You can run DBCC PAGE to look at that record in TempDB if the transaction is still open. It is beyond the scope of this post so I will not cover it here but definitely interesting to look at. What has not changed is that m_freeCnt is still 680. There was no change in record size or space usage.

    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
    PAGE: (1:156)


    BUFFER:


    BUF @0x0000000090FC6080

    bpage = 0x0000000090182000           bhash = 0x0000000000000000           bpageno = (1:156)
    bdbid = 6                            breferences = 1                      bUse1 = 35111
    bstat = 0x6c0000b                    blog = 0x432159bb                    bnext = 0x0000000000000000

    PAGE HEADER:


    Page @0x0000000090182000

    m_pageId = (1:156)                   m_headerVersion = 1                  m_type = 1
    m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x2000
    m_objId (AllocUnitId.idObj) = 29     m_indexId (AllocUnitId.idInd) = 256  
    Metadata: AllocUnitId = 72057594039828480                                
    Metadata: PartitionId = 72057594038910976                                 Metadata: IndexId = 1
    Metadata: ObjectId = 2105058535      m_prevPage = (0:0)                   m_nextPage = (1:160)
    pminlen = 13                         m_slotCnt = 53                       m_freeCnt = 680
    m_freeData = 7474                    m_reservedCnt = 0                    m_lsn = (34:435:35)
    m_xactReserved = 0                   m_xdesId = (0:992)                   m_ghostRecCnt = 1
    m_tornBits = 0                      

    Allocation Status

    GAM (1:2) = ALLOCATED                SGAM (1:3) = ALLOCATED              
    PFS (1:1) = 0x68 MIXED_EXT ALLOCATED   0_PCT_FULL                         DIFF (1:6) = CHANGED
    ML (1:7) = NOT MIN_LOGGED            

    Slot 0 Offset 0x1d14 Length 30

    Record Type = GHOST_DATA_RECORD      Record Attributes =  NULL_BITMAP VERSIONING_INFO
    Record Size = 30                    
    Memory Dump @0x0000000018A4BD14

    0000000000000000:   5c000d00 01000000 d0530384 9d040000 †\.......ÐS.„....
    0000000000000010:   c8010000 01000100 a0bf0500 0000††††††È....... ¿....  

    Version Information =
        Transaction Timestamp: 376736
        Version Pointer: (file 1 page 456 currentSlotId 1)


    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 1                              

    Slot 0 Column 2 Offset 0x0 Length 0 Length (physical) 0

    varchar_value =                      

    Slot 0 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 0 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 0 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (010086470766)        
    Slot 1 Offset 0x1cee Length 38

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS VERSIONING_INFO
    Record Size = 38                    
    Memory Dump @0x0000000018A4BCEE

    0000000000000000:   70000d00 02000000 d1530384 9d040000 †p.......ÑS.„....
    0000000000000010:   01001800 54455354 00000000 00000000 †....TEST........
    0000000000000020:   47be0500 0000††††††††††††††††††††††††G¾....          

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 1 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 2                              

    Slot 1 Column 2 Offset 0x14 Length 4 Length (physical) 4

    varchar_value = TEST                

    Slot 1 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 1 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 1 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (020068e8b274)
    <snip>
    .
    .
    .
    </snip>
    KeyHashValue = (3400154ff2a1)        
    Slot 52 Offset 0x1bfc Length 242

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS VERSIONING_INFO
    Record Size = 242                    
    Memory Dump @0x0000000018A4BBFC

    0000000000000000:   70000d00 35000000 d0530384 9d040000 †p...5...ÐS.„....
    0000000000000010:   0100e400 54455354 54455354 54455354 †..ä.TESTTESTTEST
    0000000000000020:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000030:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000040:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000050:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000060:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000070:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000080:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000090:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000A0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000B0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000C0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000D0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000E0:   54455354 00000000 00000000 47be0500 †TEST........G¾..
    00000000000000F0:   0000†††††††††††††††††††††††††††††††††..              

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 52 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 53                              

    Slot 52 Column 2 Offset 0x14 Length 208 Length (physical) 208

    varchar_value = TESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTT
    ESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTEST

    Slot 52 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 52 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 52 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (350070284e19)        


    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    The second DBCC PAGE result shows that the record in slot 0 is no longer a GHOST_DATA_RECORD and the version pointer has reverted to Null. The m_freeCnt is still 680.

    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
    PAGE: (1:156)


    BUFFER:


    BUF @0x0000000090FC6080

    bpage = 0x0000000090182000           bhash = 0x0000000000000000           bpageno = (1:156)
    bdbid = 6                            breferences = 3                      bUse1 = 35512
    bstat = 0x6c0000b                    blog = 0x432159bb                    bnext = 0x0000000000000000

    PAGE HEADER:


    Page @0x0000000090182000

    m_pageId = (1:156)                   m_headerVersion = 1                  m_type = 1
    m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x6000
    m_objId (AllocUnitId.idObj) = 29     m_indexId (AllocUnitId.idInd) = 256  
    Metadata: AllocUnitId = 72057594039828480                                
    Metadata: PartitionId = 72057594038910976                                 Metadata: IndexId = 1
    Metadata: ObjectId = 2105058535      m_prevPage = (0:0)                   m_nextPage = (1:160)
    pminlen = 13                         m_slotCnt = 53                       m_freeCnt = 680
    m_freeData = 7504                    m_reservedCnt = 0                    m_lsn = (34:435:38)
    m_xactReserved = 0                   m_xdesId = (0:992)                   m_ghostRecCnt = 0
    m_tornBits = 0                      

    Allocation Status

    GAM (1:2) = ALLOCATED                SGAM (1:3) = ALLOCATED              
    PFS (1:1) = 0x60 MIXED_EXT ALLOCATED   0_PCT_FULL                         DIFF (1:6) = CHANGED
    ML (1:7) = NOT MIN_LOGGED            

    Slot 0 Offset 0x1d32 Length 30

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VERSIONING_INFO
    Record Size = 30                    
    Memory Dump @0x0000000018A4BD32

    0000000000000000:   50000d00 01000000 d0530384 9d040000 †P.......ÐS.„....
    0000000000000010:   00000000 00000000 47be0500 0000††††††........G¾....  

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 1                              

    Slot 0 Column 2 Offset 0x0 Length 0 Length (physical) 0

    varchar_value =                      

    Slot 0 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 0 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 0 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (010086470766)        
    Slot 1 Offset 0x1cee Length 38

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS VERSIONING_INFO
    Record Size = 38                    
    Memory Dump @0x0000000018A4BCEE

    0000000000000000:   70000d00 02000000 d1530384 9d040000 †p.......ÑS.„....
    0000000000000010:   01001800 54455354 00000000 00000000 †....TEST........
    0000000000000020:   47be0500 0000††††††††††††††††††††††††G¾....          

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 1 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 2                              

    Slot 1 Column 2 Offset 0x14 Length 4 Length (physical) 4

    varchar_value = TEST                

    Slot 1 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 1 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 1 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (020068e8b274)        
    Slot 2 Offset 0xa4 Length 42

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS VERSIONING_INFO
    Record Size = 42                    
    Memory Dump @0x0000000018A4A0A4

    0000000000000000:   70000d00 03000000 d0530384 9d040000 †p.......ÐS.„....
    0000000000000010:   01001c00 54455354 54455354 00000000 †....TESTTEST....
    0000000000000020:   00000000 47be0500 0000†††††††††††††††....G¾....      

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 2 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 3                              

    Slot 2 Column 2 Offset 0x14 Length 8 Length (physical) 8

    varchar_value = TESTTEST            

    Slot 2 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 2 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 2 Offset 0x0 Length 0 Length (physical) 0
    <snip>
    .
    .
    .
    </snip>
    KeyHashValue = (3400154ff2a1)        
    Slot 52 Offset 0x1bfc Length 242

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS VERSIONING_INFO
    Record Size = 242                    
    Memory Dump @0x0000000018A4BBFC

    0000000000000000:   70000d00 35000000 d0530384 9d040000 †p...5...ÐS.„....
    0000000000000010:   0100e400 54455354 54455354 54455354 †..ä.TESTTESTTEST
    0000000000000020:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000030:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000040:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000050:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000060:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000070:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000080:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    0000000000000090:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000A0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000B0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000C0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000D0:   54455354 54455354 54455354 54455354 †TESTTESTTESTTEST
    00000000000000E0:   54455354 00000000 00000000 47be0500 †TEST........G¾..
    00000000000000F0:   0000†††††††††††††††††††††††††††††††††..              

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 52 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 53                              

    Slot 52 Column 2 Offset 0x14 Length 208 Length (physical) 208

    varchar_value = TESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTT
    ESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTEST

    Slot 52 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 52 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 52 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (350070284e19)        


    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    I am feeling pretty good about my results so far but I want to do a final test to rule out any effect from the use of transactions in my testing. Here is the final test:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    DBCC TRACEON(3604)
    GO

    DELETE  dbo.TestTable
    WHERE   id = 1
    GO

    DBCC PAGE(RecordSizeTest, 1, 156, 3)
    GO

    The DBCC PAGE results from this test confirm that transactions have not impacted the test cases above. The results below do depend on how the test is run though. I ran both the delete and DBCC PAGE as a single command and was able to see the ghost record in slot 0 and m_freeCnt was still 680. If I had run the statements individually I would most likely have seen that the ghost cleanup process had removed the record in slot 0 with the row that had been in slot 1 now showing in slot 0. The m_freeCnt would also have been updated to be 712, reflecting the removal of the record.

    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
    PAGE: (1:156)


    BUFFER:


    BUF @0x0000000090FC6080

    bpage = 0x0000000090182000           bhash = 0x0000000000000000           bpageno = (1:156)
    bdbid = 6                            breferences = 1                      bUse1 = 35886
    bstat = 0x6c0000b                    blog = 0x432159bb                    bnext = 0x0000000000000000

    PAGE HEADER:


    Page @0x0000000090182000

    m_pageId = (1:156)                   m_headerVersion = 1                  m_type = 1
    m_typeFlagBits = 0x4                 m_level = 0                          m_flagBits = 0x2000
    m_objId (AllocUnitId.idObj) = 29     m_indexId (AllocUnitId.idInd) = 256  
    Metadata: AllocUnitId = 72057594039828480                                
    Metadata: PartitionId = 72057594038910976                                 Metadata: IndexId = 1
    Metadata: ObjectId = 2105058535      m_prevPage = (0:0)                   m_nextPage = (1:160)
    pminlen = 13                         m_slotCnt = 53                       m_freeCnt = 680
    m_freeData = 7504                    m_reservedCnt = 0                    m_lsn = (34:435:43)
    m_xactReserved = 0                   m_xdesId = (0:993)                   m_ghostRecCnt = 1
    m_tornBits = 0                      

    Allocation Status

    GAM (1:2) = ALLOCATED                SGAM (1:3) = ALLOCATED              
    PFS (1:1) = 0x68 MIXED_EXT ALLOCATED   0_PCT_FULL                         DIFF (1:6) = CHANGED
    ML (1:7) = NOT MIN_LOGGED            

    Slot 0 Offset 0x1d32 Length 30

    Record Type = GHOST_DATA_RECORD      Record Attributes =  NULL_BITMAP VERSIONING_INFO
    Record Size = 30                    
    Memory Dump @0x0000000018A4BD32

    0000000000000000:   5c000d00 01000000 d0530384 9d040000 †\.......ÐS.„....
    0000000000000010:   d0010000 01000000 81c00500 0000††††††Ð........À....  

    Version Information =
        Transaction Timestamp: 376961
        Version Pointer: (file 1 page 464 currentSlotId 0)


    Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 1                              

    Slot 0 Column 2 Offset 0x0 Length 0 Length (physical) 0

    varchar_value =                      

    Slot 0 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 0 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 0 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (010086470766)        
    Slot 1 Offset 0x1cee Length 38

    Record Type = PRIMARY_RECORD         Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS VERSIONING_INFO
    Record Size = 38                    
    Memory Dump @0x0000000018A4BCEE

    0000000000000000:   70000d00 02000000 d1530384 9d040000 †p.......ÑS.„....
    0000000000000010:   01001800 54455354 00000000 00000000 †....TEST........
    0000000000000020:   47be0500 0000††††††††††††††††††††††††G¾....          

    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 1 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 2                              

    Slot 1 Column 2 Offset 0x14 Length 4 Length (physical) 4

    varchar_value = TEST                

    Slot 1 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 1                        

    Slot 1 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 1 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (020068e8b274)  
    <snip>
    .
    .
    .
    </snip>
    Version Information =
        Transaction Timestamp: 376391
        Version Pointer: Null


    Slot 52 Column 1 Offset 0x4 Length 4 Length (physical) 4

    id = 53                              

    Slot 52 Column 2 Offset 0x14 Length 208 Length (physical) 208

    varchar_value = TESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTT
    ESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTESTTEST

    Slot 52 Column 3 Offset 0x8 Length 1 (Bit position 0)

    bit_value = 0                        

    Slot 52 Column 4 Offset 0x9 Length 4 Length (physical) 4

    create_date = 2010-05-28 14:11:00.000                                    

    Slot 52 Offset 0x0 Length 0 Length (physical) 0

    KeyHashValue = (350070284e19)        


    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    Conclusion

    I started out with a theory that rebuilding the indexes in a database with ONLINE=ON will prevent unexpected space usage due to large update or delete operations in a database that has Read Committed Snapshot Isolation enabled. I feel that through my tests I have shown that adding the versioning information at the time of reindexing is a viable alternative to keeping extra free space in a database. It is definitely a great alternative to dealing with production issues caused by a full database. Like anything else this solution is not perfect, but if I know there is a big delete coming, I will definitely make sure the indexes on the impacted tables are rebuilt with ONLINE=ON or that I have included extra space via FILL_FACTOR. Most importantly, I know to expect growth from large update or delete operations and can manage accordingly in the way the database is set up and the number of records impacted by each pass over the data.

    Thank you for sticking with me to the end of this long post. I hope it was as informative to read as it was to write.

    Posted in File Management | Tagged , , | 1 Comment

    What is a Good Way to Run CheckDB on a VLDB?

    Introduction

    Today’s script is one that I wrote based on the logic outlined in this post by Paul Randal (Blog|Twitter). This script is written for SQL 2000 but, as Paul notes, the logic will work on SQL 2005.

    The Script

    This stored procedure stays pretty true to the logic outlined in Paul’s post so I will just cover the differences here. The first thing to notice is that the parameters passed into the procedure are the days of the week that different portions of the check should run, the maximum run time in minutes and whether or not to print debug messages. The stored procedure then parses the input strings and runs CHECKALLOCs and CHECKCATALOGs if requested.

    If tables should be checked today a little more work is necessary. I decided to use a utility database to hold work tables for my custom scripts called DBADB. The first part of performing a table check is to see if a work table already exists in the database. If the table does not exist then one is created and loaded with a list of all tables in the database. After the table is loaded, the process begins looping through the table, checking that the run time has not been exceeded then running checktable on each table. This continues until the table list has been processed or time runs out. If time runs out then the process picks up where it left off next time the table check starts to make sure all tables are eventually checked before starting over again.

    Here is the script:

    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
    CREATE PROCEDURE [dbo].[sp_dba_checkdb_vldb] @days_to_run_checkalloc varchar(15) = '1,4', @days_to_run_checkcatalog varchar(15) = '1', @days_to_run_checktable varchar(15) = '1,2,3,4,5,6,7', @max_minutes_to_run int = 360, @debug_flag bit = 0
    AS

     BEGIN

        DECLARE @date_part_search_string    char(3),
                @start_time                 datetime,
                @sql_text                   nvarchar(4000),
                @current_object_id          int

        SELECT  @date_part_search_string = '%' + CAST(DATEPART(dw, GETDATE()) AS VARCHAR) + '%',
                @start_time = GETDATE()

        IF PATINDEX(@date_part_search_string, @days_to_run_checkalloc) > 0
         BEGIN
            IF @debug_flag = 1
                PRINT 'DEBUG: Running DBCC CHECKALLOC'
                DBCC CHECKALLOC
         END

        IF PATINDEX(@date_part_search_string, @days_to_run_checkcatalog) > 0
         BEGIN
            IF @debug_flag = 1
                PRINT 'DEBUG: Running DBCC CHECKCATALOG'
                DBCC CHECKCATALOG
         END
         
        IF PATINDEX(@date_part_search_string, @days_to_run_checktable) > 0
         BEGIN
            DECLARE @control_table  varchar(500)
            SELECT  @control_table = DB_NAME() + '_dbcc_checktable_worklist'
            IF NOT EXISTS(SELECT * FROM [DBADB].[dbo].[sysobjects] WHERE name = @control_table)
             BEGIN
                SELECT  @sql_text = 'SELECT DISTINCT
                                            i.id,
                                            CAST(NULL AS datetime) AS run_date_time
                                    INTO    [DBADB].[dbo].'
    + QUOTENAME(@control_table) + '
                                    FROM    sysindexes i
                                            INNER JOIN sysobjects o
                                                ON i.id = o.id
                                    WHERE   o.type != '
    'TF'''

                IF @debug_flag = 1
                    PRINT 'DEBUG: Running sql command: [' + @sql_text + ']'
                EXEC sp_executesql  @sql_text

                SELECT  @sql_text = 'CREATE CLUSTERED INDEX IX_' + @control_table + '_id_run_date_time ON [DBADB].[dbo].' + QUOTENAME(@control_table) + ' (id, run_date_time)'
                IF @debug_flag = 1
                    PRINT 'DEBUG: Running sql command: [' + @sql_text + ']'
                EXEC sp_executesql  @sql_text
             END

            SELECT  @sql_text = '   SELECT  TOP 1 @current_object_id = c.id
                                    FROM    [DBADB].[dbo].['
    + @control_table + '] c
                                            INNER JOIN sysobjects o
                                                ON c.id = o.id
                                    WHERE   c.run_date_time IS NULL
                                                AND o.type != '
    'TF'''
             
            IF @debug_flag = 1
                PRINT 'DEBUG: Running sql command: [' + @sql_text + ']'
               
            EXEC sp_executesql @sql_text, N'@current_object_id int OUTPUT', @current_object_id = @current_object_id OUTPUT
           
            IF @debug_flag = 1
                PRINT 'DEBUG: @current_object_id = ' + ISNULL(CAST(@current_object_id AS varchar), 'NULL')
           
            WHILE   @current_object_id IS NOT NULL AND DATEADD(mi, @max_minutes_to_run, @start_time) > GETDATE()
             BEGIN
                SELECT @current_object_id = NULL

                SELECT  @sql_text = '   SELECT  TOP 1 @current_object_id = c.id
                                        FROM    [DBADB].[dbo].['
    + @control_table + '] c
                                                INNER JOIN sysobjects o
                                                    ON c.id = o.id
                                        WHERE   c.run_date_time IS NULL
                                                    AND o.type != '
    'TF'''

                IF @debug_flag = 1
                    PRINT 'DEBUG: Running sql command: [' + @sql_text + ']'
                   
                EXEC sp_executesql @sql_text, N'@current_object_id int OUTPUT', @current_object_id = @current_object_id OUTPUT

                IF @debug_flag = 1
                    PRINT 'DEBUG: @current_object_id = ' + ISNULL(CAST(@current_object_id AS varchar), 'NULL')
                   
                IF @debug_flag = 1
                    PRINT 'DEBUG: Running DBCC CHECKTABLE(' + CAST(@current_object_id AS varchar) + ')'

                DBCC CHECKTABLE(@current_object_id)

                SELECT  @sql_text = '   UPDATE  [DBADB].[dbo].[' + @control_table + ']
                                        SET     run_date_time = GETDATE()
                                        WHERE   id = @current_object_id'


                IF @debug_flag = 1
                    PRINT 'DEBUG: Running sql command: [' + @sql_text + ']'

                EXEC sp_executesql @sql_text, N'@current_object_id int OUTPUT', @current_object_id = @current_object_id OUTPUT
             END
           
            IF @current_object_id IS NULL
             BEGIN
                PRINT 'Ran out of work to do...cleaning up and shutting down.'
                IF EXISTS(SELECT * FROM [DBADB].[dbo].[sysobjects] WHERE name = @control_table)
                 BEGIN
                    SELECT  @sql_text = 'DROP TABLE [DBADB].[dbo].' + QUOTENAME(@control_table)
                    IF @debug_flag = 1
                        PRINT 'DEBUG: Running sql command: [' + @sql_text + ']'
                    EXEC sp_executesql  @sql_text
                 END
             END
            ELSE
                PRINT 'Ran out of time...shutting down.'
         END
     END
    GO

    Conclusion

    As usual, I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged | 2 Comments

    How Do I Move SQL Database Files Around?

    Introduction

    Today’s script is one that I had not planned on blogging so soon but since Paul Randal just talked about moving SQL Server files around for TechNet Magazine, it seemed like a good time to break this one out.

    The Script

    This script is a little different in that it is a script that creates a script, a “turducken” script if you will. The idea here is to run this script and let it output to text then take those results, paste them into a new window, review the resulting script and maybe even run it.

    The script starts out by getting the default data and log file locations from SQL Server by checking the registry, using a method learned by watching Profiler while checking the location with Management Studio. (I often comment out these lines to change to specific locations.) The script then begins building the string to output by creating the command to turn xp_cmdshell on. A lot of people, including me, have a policy against xp_cmdshell being turned on on their servers but in cases like this where it is turned on to be used and turned right back off I feel I can get away with it. The next step is to create alter database scripts to take the databases offline. Next, the alter database statements and DOS file move commands are created. The command to set the database online is then added and finally, xp_cmdshell is turned back off.

    Updated 07/14/2010 to replace move commands with copy to make sure the files are still good before they are deleted. This does add manual cleanup but the trade off is not having to find out how good your last backup is. Thanks to Paul Randal (Blog|Twitter) and Buck Woody (Blog|Twitter) for pointing this out.

    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
    DECLARE @file_path  nvarchar(520),
            @log_path   nvarchar(520)

    EXEC master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'DefaultData', @file_path OUTPUT
    EXEC master.dbo.xp_instance_regread N'HKEY_LOCAL_MACHINE', N'Software\Microsoft\MSSQLServer\MSSQLServer', N'DefaultLog', @log_path OUTPUT

    SELECT  'EXEC sp_configure ''show advanced options'', ''1''
    GO
    RECONFIGURE
    GO
    EXEC sp_configure '
    'xp_cmdshell'', ''1''
    GO
    RECONFIGURE
    GO'


    SELECT 'ALTER DATABASE [' + DB_NAME(mf.database_id) + '] SET OFFLINE WITH ROLLBACK IMMEDIATE
    GO
    ALTER DATABASE ['
    + DB_NAME(mf.database_id) + '] MODIFY FILE (NAME = [' + mf.name + '], FILENAME = ''' + @file_path + '\' + mf.name + '.mdf'')
    GO
    ALTER DATABASE ['
    + DB_NAME(mf.database_id) + '] MODIFY FILE (NAME = [' + mf2.name + '], FILENAME = ''' + @log_path + '\' + mf2.name + '.ldf'')
    GO
    EXEC xp_cmdshell '
    'copy /Y "' + mf.physical_name + '" "' + @file_path + '\' + mf.name + '.mdf"''
    GO
    EXEC xp_cmdshell '
    'copy /Y "' + mf2.physical_name + '" "' + @log_path + '\' + mf2.name + '.ldf"''
    GO
    ALTER DATABASE ['
    + DB_NAME(mf.database_id) + '] SET ONLINE
    GO

    '

    FROM    sys.master_files mf
            INNER JOIN sys.master_files mf2
                ON mf.database_id = mf2.database_id
    WHERE   DB_NAME(mf.database_id) NOT IN ('master', 'model', 'msdb', 'tempdb') and mf.type_desc = 'ROWS' and mf.file_id = 1 and mf2.type_desc = 'LOG'
    AND (mf.physical_name != @file_path + '\' + mf.name + '.mdf' OR mf2.physical_name != @log_path + '\' + mf2.name + '.ldf')
    order by mf.name

    SELECT  'EXEC sp_configure ''xp_cmdshell'', ''0''
    GO
    RECONFIGURE
    GO
    EXEC sp_configure '
    'show advanced options'', ''0''
    GO
    RECONFIGURE
    GO'

    Would this Work for TempDB?

    Yes and no. The resulting script can be used to alter the location of TempDB but SQL Server must be stopped and started to move the files. There would also have to be manual cleanup in that the old TempDB files would have to be deleted.

    Would this Work for System Databases other than TempDB?

    No. There is a lot involved in moving a system database. Detailed instructions can be found here: http://msdn.microsoft.com/en-us/library/ms345408.aspx

    Conclusion

    As usual, I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , , , , | Comments Off

    Where Do I Start with PowerShell?

    Introduction

    This morning I set out to get some information about getting started in PowerShell for a coworker. Rather than spend a bunch of time searching for different sites I threw the question out to the SQL Community on Twitter via #SqlHelp. The response was so overwhelming that I decided I at least owed it to the SQL Community to get it all written down.

    Thank you to everyone who supplied a link, you are what keeps the SQL Community great!

    So here is everything that came in broken out by category:

    Blogs

    Aaron Nelson: http://sqlvariant.com/wordpress/

    Aaron Nelson – PowerShell Links: http://sqlvariant.com/wordpress/index.php/powershell-links/

    Aaron Nelson – Getting Started with PowerShell: http://sqlvariant.com/wordpress/index.php/2010/02/sqlserversqldatabasestables-dir/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+Sqlvariations+%28SQLvariations%3A+SQL+Server%2C+a+little+PowerShell%2C+maybe+some+Hyper-V%29

    Aaron Nelson – Get More Done With SQLPSX: http://sqlvariant.com/wordpress/index.php/2010/02/get-more-done-with-sqlpsx/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+Sqlvariations+%28SQLvariations%3A+SQL+Server%2C+a+little+PowerShell%2C+maybe+some+Hyper-V%29

    Buck Woody – Carpe Datum: http://blogs.msdn.com/buckwoody/archive/tags/PowerShell/default.aspx

    Buck Woody – InformIt: http://www.informit.com/guides/content.aspx?g=sqlserver&seqNum=253&rll=1

    Buck Woody – Intro to PowerShell Series: http://blogs.technet.com/heyscriptingguy/archive/2009/05/27/how-does-windows-powershell-make-it-easier-to-work-with-sql-server-2008.aspx

    Hey, Scripting Guy! Blog: http://blogs.technet.com/heyscriptingguy/archive/tags/getting+started/default.aspx

    Laerte Junior – Great Practical Examples on SimpleTalk.com: http://www.simple-talk.com/author/laerte-junior/

    Laerte Junior – $hell Your Experience (Portuguese): http://laertejuniordba.spaces.live.com/

    Windows PowerShell Blog: http://blogs.msdn.com/powershell/

    Community

    Powershellcommunity.org: http://www.powershellcommunity.org/

    PowerShell.com: http://powershell.com/cs/

    eBooks

    Master-PowerShell with Dr.Tobias Weltner: http://powershell.com/cs/blogs/ebook/

    TechNet

    Task-Based Guide to PowerShell: http://technet.microsoft.com/en-us/library/ee332526.aspx

    PowerShell Script Center: http://technet.microsoft.com/en-us/scriptcenter/dd742419.aspx

    Script Center: http://technet.microsoft.com/en-us/scriptcenter/default.aspx

    Windows PowerShell: Survival Guide: http://social.technet.microsoft.com/wiki/contents/articles/windows-powershell-survival-guide.aspx

    Applications / GUIs

    PowerGUI: http://powergui.org/index.jspa

    Videos

    Midnight DBA PowerShell Videos: http://midnightdba.itbookworm.com/Admin.aspx

    White Papers

    Understanding and Using PowerShell Support in SQL Server 2008: http://msdn.microsoft.com/en-us/library/dd938892.aspx

    Conclusion

    So there you have it, a pretty substantial list of resources to get started in PowerShell.These all look like great resources and I can see I have a ton of reading to do.

    Posted in PowerShell | Tagged | 10 Comments

    How Can I Tell if SQL Agent is Running via SQL?

    Introduction

    Today’s post is a continuation in my on-going effort to document all of the scripts I use to manage my environment. To date it has been a great exercise both in getting things written down and in getting feedback from others on better ways I could be doing things. To that end, here is a stored procedure that I use to tell if SQL Agent is running before trying to start a job programmatically.

    The Script

    This script opens with some trickery that I stole from Management Studio using Profiler. This is my new favorite way to determine if a server is a named instance and handle it accordingly. I will be going back to retrofit all of my scripts to use this method.

    The next step is to declare a temporary table and pull in the results of xp_servicecontrol to be able to use them later. The xp_servicecontrol extended procedure is undocumented and unsupported but still seems like the best option in this case. Alternatives I have seen include using xp_cmdshell (NO!) or building a CLR stored procedure to call WMI and get this information.

    Finally, the service status is selected into the output variable for programmatic use by a caller and the flag is checked to see if the service status should also be returned as a result set for debug reasons.

    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
    CREATE PROCEDURE [dbo].[sp_dba_GetSqlAgentStatus] @display_results bit = 0, @service_state varchar(100) = null OUTPUT

    AS

     BEGIN

        DECLARE @agent_string   nvarchar(256)
             
        IF (SERVERPROPERTY('INSTANCENAME') IS NOT NULL)
            SELECT @agent_string = @agent_string + N'SQLAgent$' + CONVERT (sysname, SERVERPROPERTY('INSTANCENAME'))
        ELSE
            SELECT @agent_string = @agent_string + N'SQLServerAgent'

        CREATE TABLE #service_state
        (
            current_service_state   varchar(50)
        )
       
        INSERT INTO #service_state
            EXEC master.dbo.xp_servicecontrol 'QueryState', 'SQLServerAgent', @service_state OUTPUT

        SELECT  @service_state = current_service_state
        FROM    #service_state

        DROP TABLE #service_state

        IF @display_results = 1
            SELECT @service_state AS current_service_state

     END
    GO

    Conclusion

    As usual, I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged | 2 Comments

    What is a Good Way to Get the SQL Server Log Directory?

    Introduction

    Today’s post is going to be a quick one to get back in the saddle again. I have been on an involuntary hiatus from blogging due to production issues. Things are getting better so it seems like time to get back to blogging.

    The script for today is one that I use in all of my job creation scripts as well as a script I have to go back and clean up job output file paths. It gets the location of master on a SQL 2005 or SQL 2008 server, using that to find the Log directory.

    The Script

    This script is quite simple so I will not spend a ton of time here. It just looks at the location of master, goes up 1 level and swaps in “Log” for “Data” in the directory name. Please note that this may not work well in your environment if you have moved master after installing SQL. Thanks to Argenis Fernandez (Blog|Twitter) for pointing out the ineffcient way I was doing this before, this script now just looks at the location of the SQL Server Error Log and uses the same directory. Warning: This is using undocumented functionality, but the best things in SQL tend to be undocumented.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    CREATE PROCEDURE [dbo].[sp_dba_GetJobOutputFileDirectory] @log_directory varchar(400) OUTPUT, @showResults BIT = 1

    AS

     BEGIN

        SELECT  @log_directory = REPLACE(CAST(SERVERPROPERTY('ErrorLogFileName') AS nvarchar(512)), 'ERRORLOG', '')

        IF @showResults = 1
            SELECT @log_directory

     END
    GO

    Conclusion

    As usual, I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged | Comments Off

    What is a Good Way to Quickly Disable SQL Agent Job Schedules?

    Introduction

    I recently blogged about a Stored Procedure to Sequentially Run SQL Agent Jobs and have been meaning to blog about this script as a follow-up because this is very helpful for converting from SQL job schedules to sequential job schedules while still having a back out path. The question of how to disable SQL jobs came up on #SqlHelp yesterday so I figured it was time to cover this script.

    Background

    When I looked at switching over to running my maintenance jobs in a sequential manner rather than fussing with individual job schedules it seemed like a daunting task. I had to have a way to cover a lot of ground fast so manually via Management Studio was out and I needed to be able to quickly rollback so a flexible script was definitely a must.

    I decided to write a script to disable the job schedules on all of my maintenance jobs. I figured this was the easiest way to cover the most ground and if things went bad I could just change the script to turn the enabled flag back on and re-run it.

    The Script

    This script has 2 major parts, disable the job schedules then notify SQL Agent of the change. The script begins by declaring a table variable (NOTE: I started out with a regular #temp table here but got column definition errors that were corrected by changing to a table variable. I have not had time to research why this happened.) to store the changed records in, allowing SQL Agent to be notified of only the job schedules that have been changed.

    I could have just notified on all jobs but this invites the possibility of “fixing” the schedule on a job that had been broken by some other scripted update. Not wanting to take the chance of starting SQL Agent jobs that have not run in a while I opted to carefully limit the scope of the notifications.

    Once I have the results it is as simple as opening a cursor and calling sp_sqlagent_notify for each job schedule that has been updated.

    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
    USE msdb

    DECLARE @updated_schedules table
    (
        schedule_id int,
        job_id      uniqueidentifier,
        job_name    sysname
    )

    UPDATE  ss
    SET     ss.[enabled] = 0
    OUTPUT  INSERTED.schedule_id,
            sj.job_id,
            sj.name
    INTO    @updated_schedules
    FROM    msdb.dbo.sysjobs sj
            INNER JOIN msdb.dbo.sysjobschedules sjs
                ON sj.job_id = sjs.job_id
            INNER JOIN msdb.dbo.sysschedules ss
                ON sjs.schedule_id = ss.schedule_id
    WHERE   sj.name LIKE '<pattern>'
               
    DECLARE @schedule_id    int,
            @job_id         uniqueidentifier,
            @job_name       sysname

    DECLARE csr CURSOR FAST_FORWARD READ_ONLY
    FOR
        SELECT  schedule_id,
                job_id,
                job_name
        FROM    @updated_schedules

    OPEN csr

    FETCH NEXT FROM csr INTO @schedule_id, @job_id, @job_name

    WHILE @@FETCH_STATUS <> -1
     BEGIN
       
        EXECUTE msdb.dbo.sp_sqlagent_notify @op_type     = N'S',
                                            @job_id      = @job_id,
                                            @schedule_id = @schedule_id,
                                            @action_type = N'U'
       
        PRINT 'Called sp_sqlagent_notify for ' + @job_name
       
        FETCH NEXT FROM csr INTO @schedule_id, @job_id, @job_name
       
     END

    CLOSE csr
    DEALLOCATE csr

    Conclusion

    As usual, I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged | 2 Comments

    Whats a Good Rule for Max Degree of Parallelism?

    Introduction

    I am one of those people that believe that anything worth doing is worth having a script to do it. Setting the Max Degree of Parallelism is no exception. In this post I will go through the script I use as well as the metrics I watch to make sure the setting is correct.

    Reasoning

    Max Degree of Parallelism (MAXDOP) is a setting that many people do not get right and for good reason it is tricky. The easiest scenario is a server with NUMA architecture. For NUMA servers the stepping off point is the number of cores per NUMA node. For other servers the answer is often “It Depends” followed by “It Changes Over Time”. The good news here is that the setting is fairly easy to tune by watching wait stats.

    The rule of thumb I use for gauging whether or not my settings are correct is to make sure that CXPACKET waits are slightly higher than SOS_SCHEDULER_YIELD. My goal is to get them as close as possible to each other without SOS_SCHEDULER_YIELD waits exceeding CXPACKET waits. Depending on your workload and the amount of tuning you are doing you may need to evaluate your settings more often.

    The Script

    This script is dirt simple but do not be deceived, this is not a change to take lightly. It is a great starting off point for a new server but careful research should be done before running on any existing production systems. The basic logic is enable ‘show advanced options’ then get the hyperthread_ratio from sys.dm_os_sys_info. This will give you the number of cores per socket and that is typically equal to the number of cores on a NUMA node. Hyperthreading is not the same as multiple cores, I am not sure I would use this script on a hyperthreaded server. Finally the script sets ‘max degree of parallelism’ to the hyperthread ratio and turns off ‘show advanced options’.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    EXEC sys.sp_configure N'show advanced options', '1'
    RECONFIGURE WITH OVERRIDE

    DECLARE @hyperthread_ratio  INT

    SELECT  @hyperthread_ratio = hyperthread_ratio
    FROM    sys.dm_os_sys_info

    EXEC sys.sp_configure N'max degree of parallelism', @hyperthread_ratio
    RECONFIGURE WITH OVERRIDE

    EXEC sys.sp_configure N'show advanced options', '0'
    RECONFIGURE WITH OVERRIDE

    Conclusion

    As usual, I hope you find this script helpful. This script and the basic rules I have laid out work well for me in my environment. Test it carefully in yours. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , , , , , | 5 Comments

    How Do I Find SSIS Packages that Exist in Multiple Folders?

    Introduction

    If you are using SQL Storage for your SSIS packages and have multiple folders to make life easier then there is no doubt you have run into this before. A package gets moved to production in what appears to be the right folder but the requestor reports that the old package is still running. You move the package again and still no change. Eventually you realize that you have duplicate packages in different folders on your server and set out about the painful task of opening each folder, hunting for other copies of the package. This is not so bad if you have 2-3 folders but the more complex your environment the more painful this becomes. Having run into this on more than one occasion myself I decided to write a script to do the work for me.

    The Script

    This is another easy one so I will not spend too much time on the mechanics of it. Get all packages, grouped by name where COUNT(*) > 1 then dig for the folder names and concatenate them into a nice string. I chose to create a folder name list because it makes it easy to create a report that can be emailed from the server. This script does not help if the packages are not named the same but at that point the problem should be pretty obvious to the person trying to run it.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    SELECT      p.name AS package_name,
                REPLACE(RTRIM((
                    SELECT      pf2.foldername + ' '
                    FROM        msdb.dbo.sysdtspackagefolders90 pf2
                                INNER JOIN msdb.dbo.sysdtspackages90 p2
                                    ON pf2.folderid = p2.folderid
                    WHERE       p2.name = p.name
                    ORDER BY    pf2.foldername
                    FOR XML PATH('')
                )), ' ', ', ') AS folder_names
    FROM        msdb.dbo.sysdtspackages90 p
    GROUP BY    p.name
    HAVING      COUNT(*) > 1

    Conclusion

    As usual, I hope you find this script helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , | Leave a comment

    How Do I Kill All Sessions from a Windows Group?

    Introduction

    The stored procedure I am posting today will kill all sessions for users that are members of a given domain group. I use this stored procedure to keep ad-hoc (write down Access) users out of the way of nightly builds on my data warehouse. I have created two jobs for each group of users that I want to keep out of the way. The first job denies connect to the Windows Group then calls this stored procedure to kick all of the users off. I run this job just before the build begins. The second job grants connect to the group after the nightly build finishes.

    The Script

    The stored procedure gets all users logged in via Windows Authentication by looking for the slash in their login name, cursoring through the list of users getting all windows groups for each user. If any windows groups that user is a member of match the one passed in then the session is killed.

    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
    CREATE PROCEDURE dbo.sp_dba_kill_spids_for_domain_group(@domain_group sysname)

    AS

    SET NOCOUNT ON

    DECLARE @spid           smallint,
            @loginame       nvarchar(128),
            @command        nvarchar(max)

    CREATE TABLE #Logins
    (
        account_name        sysname NULL,
        type                char(8) NULL,
        privilege           char(9) NULL,
        mapped_login_name   sysname NULL,
        permission_path     sysname NULL
    )

    DECLARE csr CURSOR FAST_FORWARD READ_ONLY FOR
        SELECT  RTRIM(login_name),
                session_id
        FROM    sys.dm_exec_sessions
        WHERE   login_nameLIKE '%\%' --Look for the slash between domain and login

    OPEN csr

    FETCH NEXT FROM csr INTO @loginame, @spid

    WHILE @@FETCH_STATUS <> -1
     BEGIN
        INSERT #Logins
            EXEC xp_logininfo @loginame, 'all'
       
        IF EXISTS(  SELECT  *
                    FROM    #Logins
                    WHERE   permission_path = @domain_group    )
         BEGIN
            SELECT  @command = 'KILL ' + CAST(@spid AS nvarchar) + ' --' + @loginame
            PRINT @command
            EXEC sp_executesql @command
         END

        TRUNCATE TABLE #Logins

        FETCH NEXT FROM csr INTO @loginame, @spid
     END

    CLOSE csr

    DEALLOCATE csr

    DROP TABLE #Logins

    SET NOCOUNT OFF

    GO

    Conclusion

    As usual, I hope you find this stored procedure helpful. Be careful that you know who is in a group before killing the sessions. I have seen situations where people found out they were in the wrong group because their sessions kept getting killed. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Security, User Management, Utilities | Tagged | Leave a comment

    T-SQL Tuesday #005 – Self Service Performance Information

    Introduction

    Today I am taking a little detour from the scripts I have been posting to take part in “T-SQL Tuesday #005 – Reporting“.

    My T-SQL Tuesday #005 entry allows selected users of a server to see what is going on with it and react accordingly, freeing members of the DBA team from constant questions about why the server is running slow. Admittedly it does often just lead to more informed complaints but at least the time to resolution is quicker.

    The Reports

    I am a sucker for free stuff so I was extremely happy when I discovered the SQL Server 2005 Performance Dashboard Reports. The reports are a package of Reporting Services reports that put a nice user interface over all of the DMVs that were new in SQL 2005. The Performance Dashboard allows you to see CPU History, Waits, Active Sessions, Waits By CPU, IO etc. They are a powerful tool that will quickly guide users directly to whatever issue it is they are looking to solve.

    The one drawback to the the Performance Dashboard Reports is that they run as a custom report in Management Studio so if you wanted to distribute them to non-administrators then a whole bunch of people are going to get way more access than you would ever want them to have. To solve this problem I converted the reports to reporting services.

    The change to reporting services was really quite straightforward. The big problem was sorting out the connection strings. I ended up passing an extra parameter, ServerName, to each of the reports then using that server name to put together a connection string for that server. The beauty of this approach is that it allows me to create a linked report for every server I want people to see. To keep it user friendly the linked report will call the reports for the appropriate server using the correct server name. Here is how the connection string looks:

    1
    2
    3
    4
    5
        <ConnectionProperties>
            <DataProvider>SQL</DataProvider>
            <ConnectString>="data source=" &amp; Parameters!ServerName.Value</ConnectString>
            <IntegratedSecurity>true</IntegratedSecurity>
          </ConnectionProperties>

    What About Security?

    If you are anything like me you are shaking your head and thinking this is a security nightmare. Well it could be if not implemented correctly but properly thought through is reasonably safe.

    Security works in layers here. First of all I made sure the reports are running under a specific account that is unique to them. This account only has the exact rights needed to get the job done.

    The next layer of security is access to the linked reports themselves. Not everyone gets access to the linked reports. Only people in certain groups or roles should even be considered for access to these reports. By no means would I recommend leaving the reports open to your entire organization.

    Lastly, the catch-all for security is that I have added a call to “How Can I Tell if a Windows Login has Rights to My Server?“, set to raise an exception if the report requestor does not have access to that server, to every report in the package. Even if I screwed up somewhere and left a big hole that someone could exploit, short of letting anyone edit the reports, they will not be able to see any pertinent information because the first thing they see will be an exception. I also include the group that is allowing the user to see the report, allowing me to quickly see where any surprises are coming from if I get emailed a screenshot of a report from someone unexpected.

    Conclusion

    So there you have it, my entry for T-SQL Tuesday #005. I hope to go into these reports further in future posts, sharing some of the modifications I have made. Some of the changes include bug fixes and changes to make the reports keep working in SQL 2008, but others are new reports. One of the more useful new reports looks at executions per second to identify logic bugs and caching opportunities.

    Apologies for not including lots of fancy screenshots, but I am unable to show pictures of my work without showing pictures of the work of others. After including callouts to show what is mine and what is not the pictures just would not be fancy anymore.

    Posted in Reporting | Tagged , , | 1 Comment

    How Can I Tell if a Windows Login has Rights to My Server?

    Introduction

    Here is another utility stored procedure that I use. This stored procedure will tell you how a user has access to your server. This is a great stored procedure to use for things like server dashboards where you only want people to see the dashboard for servers that they have access to without granting them all the rights that would go with the dashboard.

    The Script

    This stored procedure is quite simple. For a given login name call xp_logininfo, passing in the login name, piping the results into a temporary table. Once the table is built check to see if it has any rows and whether or not we should raise an exception based on the @hide_exceptions flag. If yes then raise an exception else just move along. Lastly, return any information returned about the login to the caller.

    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
    CREATE PROCEDURE dbo.sp_dba_logininfo @loginame nvarchar(128), @hide_exceptions bit = 0

    AS

     BEGIN

        CREATE TABLE #Logins
        (
            account_name        sysname NULL,
            type                char(8) NULL,
            privilege           char(9) NULL,
            mapped_login_name   sysname NULL,
            permission_path     sysname NULL
        )

            INSERT #Logins
                EXEC xp_logininfo @loginame

        IF (SELECT COUNT(*) FROM #Logins) = 0 AND @hide_exceptions = 0
         BEGIN
            RAISERROR('Specified user does not have access to this server.', 14, 1)
         END

        SELECT  account_name,
                type,
                privilege,
                mapped_login_name,
                permission_path
        FROM    #Logins

        DROP TABLE #Logins
     
     END

    GO

    Conclusion

    As usual, I hope you find this stored procedure helpful. Please let me know if you run into any issues with it or know a better way to do the same thing. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Security, User Management, Utilities | Tagged | 5 Comments

    Script to Create A TempDB File Per Processor

    Introduction

    Trying to keep the streak alive so here is my 5th blog post in 5 days. This one is another script and by now you are probably wondering if I am either really incredibly lazy or just hate screwing up. The answer to both is an unqualifed YES.

    The Need

    The best practice from Microsoft is to have 1 TempDB file per processor on a dedicated disk. The files should be set large enough so that they do not need to grow. You can read more here. I should warn you that there is some debate on this recommendation and many people run .25 TempDB files per processor or even just 1 big file on less busy servers.

    Update: There is not a lot of good information on this subject outside of the best practices recommendations and I am not trying to fill that void here. I am not advocating a certain number of files for TempDB in this post, that would probably occupy an entire series of blog posts and require intimate knowledge of the internals of the storage engine.

    This post is a good start at dispelling some of the myths around how many files you should have for TempDB. Most noticeably, it only recommends multiple files when you are seeing latch contention in TempDB.

    I plan to update this post directly as new information is discovered to get the best information out there so please either check back or subscribe to the feed to stay informed.

    April 12, 2010 – Paul Randal has just blogged “A SQL Server DBA myth a day: (12/30) tempdb should always have one data file per processor core”. Please make sure to read that post before going down the path of 1 TempDB file per processor.

    For me, on my big servers, I like to run with 1 TempDB file per core / processor. Unfortunately being big servers, these machines have a lot of cores. The last server I built had 32 cores. Creating individual files via the GUI would have taken 30 minutes to an hour assuming I did not get something wrong and have to redo it.

    The Script

    To satisfy my need to be lazy and protect me from myself I have written a script to add a TempDB file per processor. The script is easy enough to use. Step 1 is to figure out how much space you have in total on your TempDB data drive. I specifically say data because logs should be on another drive. Step 2 is to divide the size of your drive by the number of cores / processors in your server and make that the size of your current single TempDB file. To give an example: Your new server has 32 cores and a 66 GB TempDB drive. 32 neatly goes into 64 2 times so you would set the size of your TempDB file to 2 GB, with maybe a slightly higher maximum size if you prefer to leave autogrow on. Next you would simply double check your math and settings then run the script and 31 exact copies of that file would be created. The whole thing should take less than 5 minutes.

    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
    USE [master]
    GO
    DECLARE @cpu_count      int,
            @file_count     int,
            @logical_name   sysname,
            @file_name      nvarchar(520),
            @physical_name  nvarchar(520),
            @size           int,
            @max_size       int,
            @growth         int,
            @alter_command  nvarchar(max)

    SELECT  @physical_name = physical_name,
            @size = size / 128,
            @max_size = max_size / 128,
            @growth = growth / 128
    FROM    tempdb.sys.database_files
    WHERE   name = 'tempdev'

    SELECT  @file_count = COUNT(*)
    FROM    tempdb.sys.database_files
    WHERE   type_desc = 'ROWS'

    SELECT  @cpu_count = cpu_count
    FROM    sys.dm_os_sys_info

    WHILE @file_count < @cpu_count -- Add * 0.25 here to add 1 file for every 4 cpus, * .5 for every 2 etc.
     BEGIN
        SELECT  @logical_name = 'tempdev' + CAST(@file_count AS nvarchar)
        SELECT  @file_name = REPLACE(@physical_name, 'tempdb.mdf', @logical_name + '.ndf')
        SELECT  @alter_command = 'ALTER DATABASE [tempdb] ADD FILE ( NAME =N''' + @logical_name + ''', FILENAME =N''' +  @file_name + ''', SIZE = ' + CAST(@size AS nvarchar) + 'MB, MAXSIZE = ' + CAST(@max_size AS nvarchar) + 'MB, FILEGROWTH = ' + CAST(@growth AS nvarchar) + 'MB )'
        PRINT   @alter_command
        EXEC    sp_executesql @alter_command
        SELECT  @file_count = @file_count + 1
     END

    Conclusion

    I hope you find this script helpful. Please let me know if you run into any issues with it or if it makes your life easier. Please keep in mind that scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in File Management, Utilities | Tagged | 4 Comments

    What Do I Need to Do After Moving a Database to SQL 2005/2008?

    Introduction

    I recently moved a database from SQL 2000 to SQL 2005. It was really just a matter of detaching the database, copying the files, attaching it on the new server, flip the compatibility mode, fix the user accounts, and run a few queries. Thats it? Could it have been so easy? It turns out the answer is no. There are some steps that need to be taken after moving a database to SQL 2005 or 2008 from SQL 2000.

    The Script

    This is an easy one so I am skipping straight to the meat. This is a simple script that runs DBCC UPDATEUSAGE, sp_updatestats, then runs a DBCC CHECKDB with DATA_PURITY.

    Running update usage is something that everyone was supposed to do on a regular basis on SQL 2000. In SQL 2005 and up the usage is maintained using a different algorithm and *should* never need to be updated again. This BOL entry explains the functionality in detail.

    There are a number of changes to the handling of statistics in SQL 2005 that are outlined here so the next step is to update statistics. Even if there were no changes in functionality I would probably update statistics here anyway. I cannot count the number of times updating statistics has yielded an unexpected boost in performance on high volume days. The new algorithm in 2005 and up first checks to see if statistics are out of date before updating them so it is also much less intrusive.

    Finally, the last step is to run a DBCC CheckDB with the DATA_PURITY flag. I always like to make sure a database has not picked up any corruption on it’s journey between servers so I would typically run a CheckDB here anyway, but in this case it is more important. Data purity checks were introduced in SQL 2005 to make sure the data in columns conforms to the definition of the columns, like making sure a smalldatetime column did not contain a value like ’0001-01-01 12:32:20.023′. This checking is not turned on by default when you move an older database to SQL 2005 / 2008, so to complete the migration process it is necessary to run a CheckDB with the DATA_PURITY flag turned on. Once the check is run and the database passes a flag will be set on the database and every future CheckDB will do the data purity checks. In my minde this is an essential step to fully completing the migration to SQL 2005 / 2008.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    --Set Connection Settings to Avoid CheckDB Failures
    SET ARITHABORT ON
    SET QUOTED_IDENTIFIER ON
    SET CONCAT_NULL_YIELDS_NULL ON
    SET ANSI_NULLS ON
    SET ANSI_PADDING ON
    SET ANSI_WARNINGS ON
    SET NUMERIC_ROUNDABORT OFF

    DECLARE @db_name    sysname

    SELECT  @db_name = DB_NAME()

    DBCC UPDATEUSAGE(@db_name)

    EXEC (@db_name +'..sp_updatestats ''RESAMPLE''')

    DBCC CHECKDB (@db_name) WITH ALL_ERRORMSGS, DATA_PURITY

    Conclusion

    As usual, I hope you find this script helpful. Please let me know if you run into any issues with it. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , , , | Leave a comment

    How Do I Identify Invalid or Unneeded Logins?

    Introduction

    I was looking at one of my servers and was caught off-guard by how many individual users had been granted rights. To make it worse I recognized a few user names as former coworkers. Clearly time for some house cleaning, now time to find a script.

    The Process

    I went looking in my script library for anything that might help me do this analysis faster and found a good start, a script to validate user logins. Well more like a note really, based on a conversation between Thomas LaRock (Blog|Twitter) and Tim Ford (Blog|Twitter) on Twitter about a stored procedure called sp_validatelogins that makes sure a windows login on your SQL instance matches a valid login on your domain. Thomas LaRock does a great job of documenting it here.

    Next I needed to figure out what users should have been in groups. My first answer is all of them, but being realistic I decided I would identify all users that have access to the server by both individual login and group then see if the group already has the same rights the user needs or if it could / should. It turns out this was easy, a cursor of windows users with a call to xp_logininfo to check for group membership will tell me exactly what to look at, including the groups and the command to get them because I often end up pasting that into an IM.

    As I started working through test data I began to notice users in databases that no longer exist on the server. Again, this one was pretty easy. A query against sys.server_principals with a right outer join to sys.database_principals quickly shows all users that exist in a database that do not have logins on the server. Adding a select to a temp table and wrapping it all in sp_MSforeachdb put it all together into a nice recordset that I could then work off of.

    The Script

    Here is the script I ended up with, run it in text mode rather than grid for better results. I would advise against trying to automate this any further or even taking any actions without carefully researching the impact of each change before making it. Messing with security is a quick way to get a lot of attention.

    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
    SET NOCOUNT ON

    CREATE TABLE #invalid_logins
    (
    [sid] varbinary(85),
    [name] sysname
    )

    CREATE TABLE #Logins
    (
    account_name sysname NULL,
    type char(8) NULL,
    privilege char(9) NULL,
    mapped_login_name sysname NULL,
    permission_path sysname NULL
    )

    INSERT #invalid_logins
    EXEC sp_validatelogins

    DECLARE @name sysname

    DECLARE csr CURSOR FAST_FORWARD READ_ONLY FOR
    SELECT sl.name
    FROM sys.syslogins sl
    LEFT OUTER JOIN #invalid_logins il
    ON sl.name = il.name
    WHERE sl.isntname = 1
    AND sl.isntgroup = 0
    AND il.name IS NULL

    OPEN csr

    FETCH NEXT FROM csr INTO @name

    WHILE @@FETCH_STATUS <> -1
    BEGIN
    INSERT #Logins
    EXEC xp_logininfo @acctname=@name, @option='all'
    FETCH NEXT FROM csr INTO @name
    END

    CLOSE csr
    DEALLOCATE csr

    PRINT 'The following logins are no longer valid and should be removed from this server:'
    SELECT name
    FROM #invalid_logins

    PRINT 'The following logins have access by login and by group and might not need access by login:'
    SELECT COUNT(*) AS num_of_groups,
    account_name,
    REPLACE(RTRIM(( SELECT permission_path + ' '
    FROM #Logins
    WHERE account_name = l.account_name
    FOR XML PATH('')
    )), ' ', ', ') AS group_names,
    'EXEC xp_logininfo @acctname=' + QUOTENAME(account_name) + ', @option=''all''' AS command_to_see_groups
    FROM #Logins l
    GROUP BY account_name,
    [type]
    HAVING COUNT(*) > 1
    ORDER BY num_of_groups DESC

    DROP TABLE #invalid_logins
    DROP TABLE #Logins

    PRINT 'The following result set shows users that were deleted from the server but not the individual databases'
    CREATE TABLE #databases_with_orphan_users
    (
    db_with_orphan_login sysname,
    orphan_login sysname,
    permissions_count int
    )

    EXEC sp_MSforeachdb 'USE ?
    INSERT #databases_with_orphan_users
    SELECT '
    '?'' AS db_with_orphan_login,
    dp.name,
    (SELECT COUNT(*) FROM sys.database_permissions WHERE grantee_principal_id = sp.principal_id) AS permissions_count
    FROM sys.server_principals sp
    RIGHT OUTER JOIN sys.database_principals dp
    ON sp.sid = dp.sid
    WHERE sp.sid IS NULL
    AND dp.type NOT IN ('
    'R'')
    AND dp.name NOT IN ('
    'guest'', ''sys'', ''INFORMATION_SCHEMA'')
    AND dp.type_desc NOT IN ('
    'APPLICATION_ROLE'')'

    SELECT db_with_orphan_login,
    orphan_login,
    permissions_count
    FROM #databases_with_orphan_users

    DROP TABLE #databases_with_orphan_users

    Conclusion

    I hope you find this script helpful. I have a feeling this script will be one of my favorites because it takes a lot of tedious analysis and boils it down to a quick hit list. Please let me know if you run into any issues with it. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in User Management, Utilities | Tagged , , , , | 5 Comments

    Is Anybody Using That Stored Procedure?

    Introduction

    From time to time I get asked to check if a stored procedure is still used in preparation for dropping it. Last week I tweeted my wish for a DMV that would give me the usage statistics for stored procedures to which Aaron Bertrand (Blog|Twitter) quickly replied that SQL 2008 has sys.dm_exec_procedure_stats that does exactly what I need. Unfortunately the server I needed to do the analysis on was running SQL 2005.

    Where It Went So Wrong

    Rather than digging into my script library to see if I had anything that might help do the analysis I fell into classic tunnel vision, cursing myself for not pushing harder to get this application on SQL 2008. I decided that the only way to figure out if these stored procedures were still used was to fire up Profiler and trace for the stored procedure names. I was not looking forward to doing this because it is easy to get a trace wrong, not capturing any data or bogging down the server, but I pushed on. I built my server-side trace script, trying it out on a less used server then fired it up on my production server. The production server immediately went to 100% CPU, completely killing all applications that depended on it. The only way to get the get the applications back online was to restart the SQL Service. This was absolutely the worst I have ever been burned by Profiler.

    A Better Way

    Last night I decided I was done replaying the events of the day over and over in my head and threw out what had happened to #sqlhelp on Twitter to get advice on what I could have done better. The basic feedback was try not to run traces on multiple strings and if you do have to then try multiple traces. There really was not magic tipping point that anybody could point to as to why doing something in one place would have such disastrous results while doing it somewhere else would not. This really points to what I have known for a long time: Profiler is not your friend. Profiler is more like that former coworker that you could not trust but had to work with on some of your most important projects.

    Talking to Andrew Kelly (Blog|Twitter) led to one of my more pronounced face-palm moments, ever. He pointed out that “2005 has similar dmv’s but if its a seldom run query the plan may not be in cache most of the time.” In iteration 1 of the same project I ran into less pronounced issues tracing stored procedures to make sure they were no longer used. At that time I wrote a script to check whether or not an object had a plan in the cache or not. That very script was sitting out in my common scripts directory the whole time. Ultimate Face-Palm.

    The Script

    Here is the script I ultimately used to figure out if the stored procedures were used. Pulling back the plan is optional but I find it helpful because it will tell you what parameters an object was compiled with. This can often point to where it was called from or even who the user is that called it. Careful with this query if you choose to run it on a production server you may see a performance impact.

    Note that I explicitly specify the database name rather than rely on the USE statement. When the absence of results is what you are looking for it is too easy to confuse a mistake for success.

    Updated to correct bug caused by improper usage of OBJECT_ID, pointed out by Simon Sabin (blog). I opted to keep the CROSS APPLY rather than going with an OUTER APPLY because I only want to return results for plans that are cached. The OBJECT_ID syntax is taken directly from here: http://msdn.microsoft.com/en-us/library/ms190328.aspx. I could not make it work by passing DB_ID as a parameter.

    1
    2
    3
    4
    5
    6
    7
    SELECT  q.text,
            p.query_plan, *
    FROM    sys.dm_exec_query_stats qs
            CROSS APPLY sys.dm_exec_sql_text(qs.plan_handle) q
            CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) p
    WHERE   q.dbid = DB_ID('[ database_name ]')
                AND q.objectid = OBJECT_ID('[ database_name . [ schema_name ] . | schema_name . ] object_name ]')

    Conclusion

    I learned a lot of tough lessons on this one. I suppose even tougher because they were lessons I already should have learned. Hopefully this blog and the included script will prevent someone else from falling into the same trap. Please let me know if you run into any issues with the script or if it bails you out of any jams. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Tracing and Profiler | Tagged , , , | 5 Comments

    Get Users By Connection

    Introduction

    I was recently working a production issue that required me to log in via the Dedicated Administrator Connection (DAC). When I tried to log in I was greeted with the following message:

    Could not connect because the maximum number of ’1′ dedicated administrator connections already exists. Before a new connection can be made, the existing dedicated administrator connection must be dropped, either by logging off or ending the process.

    After searching through my scripts folder and not finding anything that would tell me who was connected via the DAC I turned to Google. I found a lot of posts detailing how to use the DAC but none telling how to tell who is using it.

    Once the emergency was taken care of I made a point of going back and writing a query to tell who is using the DAC. To help the community or at least that part that relies on Google like myself I am also blogging it and adding appropriate search tags.

    The Query

    I know, I know, enough with the backstory already lets see some code. The code I have posted here will show all users by connection, although I have included a commented WHERE clause to filter to just the DAC connection. Here is the SQL:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SELECT  ep.name,
            ep.protocol_desc,
            es.login_name,
            es.nt_user_name,
            es.host_name
    FROM    master.sys.dm_exec_sessions es
            INNER JOIN master.sys.tcp_endpoints ep
                ON es.endpoint_id = ep.endpoint_id
    --WHERE ep.endpoint_id = 1 --Uncomment WHERE to show who is on DAC Connection

    Conclusion

    Hopefully this script is helpful. Unfortunately this script will not tell you who is connected via the DAC when nobody is able to connect to the SQL instance in question, netstat from a command line on the server may help there. Please let me know if you run into any issues, have any ideas that would make it better or just want to share how you are using it. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in User Management | Tagged , , , | 1 Comment

    Managing the Size of the Security Cache (TokenAndPermUserStore) in SQL 2005

    Introduction

    In an earlier post I walked through the diagnosis of a problem with TokenAndPermUserStore bloat in SQL 2005.In this post I will go through what I did to work through the issues and the results of those efforts.

    Triage

    Once I understood that the issue was TokenAndPermUserStore bloat, the first step was to stop the bleeding. The quick fix I chose was to create a job that ran every 30 minutes to issue a DBCC FREESYSTEMCACHE(‘TokenAndPermUserStore’). Once the job was in and running the random server slowdowns immediately stopped. At this point I knew I was on the right track even if all I had was a band-aid.

    Research

    There is not a lot of good documentation out there on TokenAndPermUserStore bloat. I am guessing not too many people are running into it. This post is the best resource I have found. I had already installed all of the suggested service packs and cumulative updates so that left me with trace flags and fixing the underlying code.

    While fixing the code seemed like the most obvious fix, some of the code that is causing the problem is optimal for the purpose. Of all the alternatives that have been tried, the current code is the best way to get the job done.  Going back and asking for resources to change proven code to deal with what I see as a SQL Server problem just did not seem right.

    Ruling out a code fix left me with two options, let the cache clearing job keep running every 30 minutes and hope nobody ever turns it off or add a trace flag to control the behavior of the server. I ruled out leaving the job running and set out to learn everything I could about –T4618. What I quickly learned is that there really is not that much information out there on the subject. What I was able to find through various sources is that –T4618 limits the number of entries in the TokenAndPermUserStore to 1024. I know from watching the counts in the cache that every time I free the cache it quickly shoots up to about 5000 entries then grows slowly from there so –T4618 would cause a huge amount of churn and CPU use. I then found that to get around the 1024 entries limitation I could also apply –T4610, limiting the cache to 8124 entries. 8124 sounded pretty good given the behavior I was seeing.

    I did also run across this article on using –T4621 to customize the size of the Security Cache, but decided it was not for me due to the complexities of picking a good number, converting it to binary and editing the registry on the cluster. In SQL 2008 the value can be changed with an sp_configure call (no trace flag), making it a much more viable option. For my SQL 2005 server I saw –T4621 as something to try if –T4618 and –T4610 did not work the way I expected.

    Turning on the Trace Flags

    I found a number of articles that said you can turn –T4618 and –T4610 on while the server is running. Not true. You have to add them as startup parameters using SQL Configuration Manager then restart your server, raising the stakes somewhat if I guessed wrong on the setting.

    I decided I was comfortable enough with my estimates that I pushed all of the paperwork and during a slack time on the system, added –T4618;-T4610 to the list of parameters on both cluster nodes. I then took the cluster resource offline and brought it back online. It all happened so fast that it was almost hard to tell I did anything. The only real evidence besides the log rolling over at an odd time is an entry in the SQL log  that says “–T4618”, another that says “–T4610” and finally one that says “TokenAndPermUserStore Memory Quota set by -T4618 as 8192.”

    The Results

    The primary application that uses this SQL Server is a web site. It does pretty decent volume so over the years the business has invested in some pretty cool monitoring tools. The tools were instrumental in helping identify and monitor the problem, telling me when I needed to spring into action to prevent lost sales. I have sort of a love / hate relationship with these tools because every time I said the problem was fixed they disagreed.

    The relationship I have with those tools has now changed somewhat. I now love them. The monitors show that the web site is now running 20% faster than it was even when I thought it was performing well. To say I am happy with the results is an understatement.

    I am blown away by how much fixing the TokenAndPermUserStore bloat helped overall application performance.

    Posted in Security Cache | Tagged , , , | Leave a comment

    My Adventures with the SQL 2005 Security Cache

    Introduction

    For that past couple of months I have been chasing performance issues with one of the most high profile servers I support. Yes, I said months and high profile. Typically the pressure to find a quick fix would be immense, but luckily we have a strong team working the issue so we are able to work the process rather than throw band-aids at the issue until the problem is resolved. I should add that we are lucky enough to have amazing monitoring tools that allow us to spot a problem before it noticeably impact users and take corrective action.

    The Problem

    At somewhat regular intervals my SQL 2005 SP3 server with 128GB of RAM and enough SAN bandwidth for 3 servers would begin to get sluggish. The number of active user sessions would climb without end, while none of the sessions would be waiting on any resource. This usually happened in the afternoon on certain days of the week but could happen any time during the business day as long as the server was experiencing sufficient load. The problem was quickly mitigated by freeing the procedure cache on the server or even a single DB. Even running sp_updatestats by way of a slowly moving, single database DBCC FREEPROCCACHE, would fix the problem although performance was even more sluggish while it was running. Immediately after freeing enough cache the CPU would shoot way up and any backlog would quickly clear.

    Troubleshooting

    Being a DBA and seeing the world from a DBA perspective I first assumed the issue was related to code that was no longer optimal for the task. The symptoms sure pointed to a bad plan or cardinality issues. I spent a lot of time searching for opportunities to tune code and missing indexes, cutting the CPU usage of the server in half and cut average reads per second from 200k to 50k. Oddly enough this had no impact on the frequency of the problem.

    Realizing I was dealing with something larger, I began to form a team to look into the issue. I pulled in subject matter experts from different areas of the organization to help look into this. Working through the troubleshooting process it was discovered that while new versions of monitoring tools, backup software and antivirus software had been added to the server the maximum memory setting had not been evaluated since the server was built 3 years ago. This server happens to be boot from SAN with synchronous replication for disaster recovery purposes. The primary page file is on non-replicated drives but a small page file is kept on the C drive to capture crash dumps. Watching the server run it was discovered that Windows seems to go to the page file on the C drive before going to any other. It quickly became clear that anything that forced the OS to go to the paging file would have a much higher performance cost than we had ever thought.  As a result of this realization, I dropped the memory setting down and saw a drop in CPU usage and IO waits. Oddly enough this had no impact on the frequency of the problem.

    Working through the issue with the rest of the team the focus kept coming back to the question: “What else does freeing the procedure cache do?” Working through the research process on this I flushed out a lot of configuration changes made in the heat of the moment to confront this problem in the past. One of the things I kept noticing is how this resembled some issues I had seen when the server was new that were resolved by running DBCC FREESYSTEMCACHE(‘TokenAndPermUserStore’), but I remember that had been fixed in SQL 2005 SP2. The more research I did the more I kept coming back to the possibility of security cache bloat so I decided to find out more about it. I was surprised to discover that while some issues had been fixed there are still other issues that can cause the security cache to bloat and that there are now trace flags to manage the size of the cache. Armed with this new information I waited for the problem to occur again, this time running DBCC FREESYSTEMCACHE(‘TokenAndPermUserStore’). It immediately fixed the performance problem.

    Why?

    Working with smart people is both a blessing and a curse. The blessings are obvious and well covered elsewhere, but the curse is that smart people do not get smart without having a thirst for knowledge. What that means here is that I had to be able to explain what was bloating the security cache, just having the fix was not enough.

    I truly believe that solving problems is much easier once you know what is wrong. This paradox is proven again here. Once I set out to see what was bloating the security cache, I discovered this article: http://blogs.msdn.com/psssql/archive/2008/06/16/query-performance-issues-associated-with-a-large-sized-security-cache.aspx that had everything I needed. The most important part was the first query that showed what was using the cache by type. I noticed that the majority of the entries had a class of 65535, leading me to this KB article: http://support.microsoft.com/kb/933564. The KB article explains what may cause the security cache to bloat. Reading through all of the conditions I noticed that CREATE, ALTER and DROP operations on a table object in any database (including TempDB) on the list. EUREKA!

    There is a batch job that I had seen running during these times, but since it ran a lot and there was nothing wrong with the query plans it was using I had no way to prove it was involved in the issue. The code the batch job was calling did have some opportunities for improvement that I am working through with the developers that own it.  Unfortunately, the code is very high profile so reworking it is a very slow deliberate process. One of the issues with the code is that it uses temporary tables to implement procedural logic in certain places.

    It appears that calling the code through a batch job leads to massive amounts of temporary table creation and destruction, causing the security cache to bloat, leading to higher CPU usage as the cache grows in size. Once the cache achieves critical mass, CPU usage drops and requests begin to queue. The requests will continue to run more slowly, causing further queuing until the DBCC FREESYSTEMCACHE(‘TokenAndPermUserStore’) command is issued.

    Conclusion

    I have been reminded of several things from this exercise.

    • Working through the troubleshooting process works
    • Knee-jerk reactions in the heat of the moment lead to more work later
    • What I knew then is not necessarily still true
    • Adding / Changing software on a server needs to be monitored
    • I need to make sure I am always keeping an open mind when chasing issues

     Now it is time to figure out what trace flag I want to use to address the security cache bloat. After that I get to start prioritizing all of the new items that this exercise has added to my to-do list.

    Posted in Security Cache | Tagged , , , | 3 Comments

    Community Based Training

    Introduction

    Recently I have been thinking through the best ways to get the people I support trained to the same level in SQL. The constraints I am running into are that I do not have the time or even maybe the skill to develop a syllabus and course materials to launch my own SQL Server course. I also know that getting training dollars appropriated for large scale training is difficult even in good economic times.

    The Idea

    I see the need for an option that splits the difference.

    There is a real un-served niche in the market for DBA delivered community training materials. The idea would be to work together as a community to create common course materials and delivery scripts that would be presented by individual DBAs to the teams they support. Everyone would get roughly the same experience while having a trainer that can speak directly to their specific questions or even look at examples using their specific data.

    The First Course

    The first course I see in the series would be an introduction to TSQL. I am currently reading Microsoft SQL Server 2008 T-SQL Fundamentals (PRO-Developer) by Itzik Ben-Gan to pick up pointers and it strikes me as to how teachable the text is. I feel like if I could just get people to read the book they would be trained. I think that if a few people came together and developed presentations, reading assignments and homework assignments the result would be a powerful teaching tool that any DBA could use to further their organization. The key is that this would be extremely cheap training owned by the community.

    I need to pause here and add that I have never developed courseware so I am not sure of the copyright implications or any other details that go with creating courseware from someone else’s text. I picture an author being happy that books will be bought for each participant, but I am sure there is much more to it than I am thinking of.

    I Need Feedback

    Does this sound like a good idea?

    Would you be interested in working on it?

    Do you know of something like this that already exists?

    Please let me know. I would have to consider no feedback at all to be strong feedback as well.

    Posted in General | Tagged , | Leave a comment

    Why Do My SQL 2008 Compressed Backups Shrink?

    Introduction

    Recently a coworker was doing some research with SQL 2008 Backup Compression on a very large database. During the testing they noticed that the backup file shrunk rather than grew.

    The Facts

    The database that the test was being run on was 360GB in size. The backup initially created a file that was 120GB in size. The file then shrank to 43GB by the time the backup was complete. The file only shrinks when the drive has sufficient space for a file that is 1/3 the size of the database. If there is not sufficient space the backup file will grow from 0.

    The Concern

    The concern that immediately came to mind is that the backup may be writing a file to the file system then performing some compression on it. Compression of the file after it is written would limit the ability to backup to anywhere but a local drive. It would also mean that the local drive would have to be optimized for read-write instead of just write.

    The Reality

    I recently had a chance to work with someone from Microsoft that was able to get with the right people to figure out what was going on. It turns out that this behavior is a feature meant to enhance performance.

    Here is what they had to say:

    The issue is that we attempt to pre-allocate the backup files so that they don’t keep getting extended during the backup process, which slows things down locally, and REALLY makes them slow (>1 order of magnitude) if your backup file is on an SMB share. 

    For uncompressed backups, we have a pretty good idea of how much data we’ll be backing up.

     For compressed backups, the eventual size depends on how compressible the data is, which we can’t tell beforehand.

     So, we chose a value which is an average result in customer databases, which is 1/3 of the volume of data to be backed up.

     If the backup ends up being larger than this estimate, we’ll grow the file as needed.  If it ends up being smaller, we’ll trim the file back to the size actually used.

    Conclusion

    My fears of performance issues were unfounded. I was also surprised to find out that the file could grow as well as shrink, although it makes perfect sense when I think it through. The big takeaway I see is making sure that the locations I back up to have free space equal to at least 1/3 the size of the largest database being backed up there to get best possible performance. This sounds like a good excuse to write a PowerShell script.

    More generally, the SQL Server community is amazing. The mere act of participating in the community means that at some point you will come into contact with someone that can answer just about any question you could have. I am in awe.

    Posted in Backup | Tagged , | 3 Comments

    Why Do I Need All These Servers?

    Introduction

    I am frequently getting requests for a new server to do this or that. Running through the usual questions to determine scope and scale, the price can get high pretty quickly. Inevitably,  talk turns to ideas on how to cut the cost while still delivering a robust platform. Typically this is when the questions about skipping the Test, QA or Stage environments come out. The logic is usually that a production server is a must have, a Development server is good to have and anything else is gravy.

    Applications running third party (off the shelf) tools can get by just fine without even a Development environment while heavily customized applications need many more environments.

    My goal here is to lay out the different environments as well as what I see as a the use for each.

    Local

    The Local environment refers to an individual developer’s desktop. On this desktop there may be a database server, web server and application server. This is the primary Development environment where things are often built and tried out without fear of angering the rest of the project team.

    Chances are that if your organization has a developer you have a Local environment.

    Development

    The Development environment is the place where everyone’s work starts to come together. It is the first environment that would contain any true servers. Depending on application architecture, this environment may be entirely composed of servers or in the case of client applications still include developer desktops.

    Servers in the Development environment are typically given the drive layout as production to allow for restores and Testing of scripts but may only have a fraction of the number of processors, memory and spindles that a production server would have.

    I like to compare the Development environment to a busy lake on a hot summer day. There are a lot of people doing their own thing, headed in their respective directions at high speed. The size of your boat (project) usually determines who has the right of way and everyone must adhere to the local customs while keeping their eyes open to prevent collisions.

    Testing

    The Testing environment is much more of a nice to have environment. For all intents and purposes it is a second Development environment with one important difference, change control. The Testing environment is where code is tested before turning it over to the QA department. It is also the environment where builds are rehearsed to minimize downtime during releases to the QA environment. Developers typically do not have access to change any objects in the Testing environment.

    It is very important that the drive layouts between all environments match for manageability purposes. It is even more so for the Testing environment due to how often restores are done to quickly get back to a clean state before Testing the next build.

    QA

    The QA environment is one of the more important environments. This is the environment where the QA team does their magic. The usage pattern here tends to be low volume and slow paced, so hardware for the QA environment can be the same as that used in the Development environment.

    The big thing to remember about the QA environment is that it must be as stable as possible. This is not the best environment to rehearse releases because any problems are likely to put the QA team behind schedule but, absent a test environment, it is the only place many organizations have.

    Staging

    The Staging environment is tied with production for being the most expensive environment. The Staging environment must be run on exactly the same equipment as production. The typical usage of Staging is automated load testing, user acceptance Testing and even training on important new features.

    Without a Staging environment careful scheduling would have to be done to prevent collisions in the QA environment. Otherwise QA testers may be entering defects for behaviors caused by user acceptance or load testing activities. User acceptance testers may also report back to their peers that the changes to the application make it slower when in fact it is just the environment they are testing in.

    Many organizations house their Staging and Production servers in separate locations for disaster recovery purposes. Attempting to do the same with only a QA environment would make the release process much more complicated in a disaster recovery scenario as changes would have to go directly from Development or Testing to Production.

    Production

    This is where the magic happens. I think everyone understands the need for a Production server so I am not going to spend a lot of time here. The point I would like to make is that the more money you spend on your production environment, the more environments you should have to back it up. There is no point to spending a small fortune on the best HA cluster you can find without having the capability to do real release management and disaster recovery or worst case: release management in disaster recovery.

    Conclusion

    I hope I have provided enough information to begin to ask the right questions when working with users to price out servers. Being able to properly educate users either helps them come to a realistic evaluation of their needs or a business case for 6 or 7 brand new servers that you can brag to your DBA friends about.

    Posted in General | Tagged , , , , , , , , , , | 1 Comment

    Stored Procedure to Sequentially Run SQL Agent Jobs

    Introduction

    Here is another one of my utility scripts. Lately it seems like all I am blogging about is utility scripts, but I have a ton of them and need to get them documented. This stored procedure is used to run a series of SQL Agent jobs sequentially. It is one of my favorite stored procedures because it clears up a lot of headaches for me without having to spend a bunch of money or turn my maintenance schedules over to another team.

    The Problem

    I was always finding myself rearranging maintenance job schedules to keep them as close as possible to each other without overlapping. Inevitably I would guess wrong and have jobs overlapping in one part of the schedule, hammering a server unnecessarily while having nothing running at other times, wasting my maintenance window. This problem is magnified on multi-instance clusters because jobs can overlap on different instances on the same node, leading to harder to discover performance issues.

    I know some people are thinking this sounds like a perfect use for Maintenance Plans, but I have a ton of servers. To have to install and maintain SSIS on each of them would be painful enough, but having to open up every maintenance plan and change it via the GUI on every server for even the smallest change is just too much. I need something I can script and I need to be able to use different parameters on different databases.

    The Solution

    I wrote a stored procedure, sp_dba_run_unscheduled_jobs, to run all jobs of a given prefix in order, beginning with jobs that have not run most recently and then alphabetically. The main idea of the job is to make maintenance single threaded to lessen the impact of the maintenance on any off-hours user or batch job activity. The stored procedure includes a maximum minutes of run time parameter to stop kicking off new jobs beyond a certain time.

    An added benefit to this stored procedured is that the jobs actually get done faster when they are not in contention with each other. When using this stored procedure to run maintenance jobs I use a single job with step 1 running all of the index jobs and step 2 running all of the CheckDB jobs. I have not found a situation where I have had to invoke the time limit.

    The stored procedure is written to only run the jobs that do not have a schedule or at least do not have an enabled schedule so I can still add schedules to jobs that I want to run at a certain time. This works out well when I have a reindex and CheckDB for a large database that takes up my entire maintenance window because I can add job scheduled to the long running jobs then just let the maintenance for the other databases run single threaded at the same time.

    Recently, I began using this stored procedure to fire off restore jobs. I set up a restore job for each database so I can run the job to restore a single database, including storing permissions, flushing replication etc then putting back everything except replication after the restore. The way the stored procedure works I am able to disable the jobs for the databases I do not want restored then fire off the restore jobs for the remaining jobs sequentially.

    The Code

    The code for the stored procedure is fairly straightforward. Load a cursor with a list of jobs that begin with the given prefix that are enabled but do not have a schedule or have a disabled schedule. The disabled schedule is important here because it allows the schedule to be retained in case it turns out that job has to run at the scheduled time. Once the cursor is loaded, loop through the list, checking to see if the next job is running. If the job is not running and the current time is not past the end time then start it, pausing before checking if it is running because SQL Agent can take a second to update the status. When the current job stops running grab the next one, check the time and if not past the end time start the job. Repeat until there are no jobs to be run.

    This stored procedure depends on the stored procedure: sp_dba_GetSqlJobExecutionStatus.

    Here is the code:

    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
    CREATE PROCEDURE [dbo].[sp_dba_run_unscheduled_jobs] @job_name_prefix sysname, @max_minutes_runtime int = null, @days_in_job_cycle int = 1, @mail_job_name varchar(256) = '', @mail_profile_name sysname = null, @mail_recipients varchar(max) = null
    AS

     BEGIN     

        DECLARE @job_name               sysname,
                @execution_status       int,
                @row_count              int,
                @last_run_date          int,
                @start_time             datetime,
                @stop_time              datetime,
                @min_date               datetime,
                @min_int_date           int,
                @int_date               int

        SELECT  @min_date = DATEADD(dd, 0, DATEDIFF(dd, 0, GETDATE())),
                @days_in_job_cycle = CASE WHEN @days_in_job_cycle < 1 THEN 1 ELSE @days_in_job_cycle END --Prevent Infinite Loop

        SELECT  @min_int_date = YEAR(@min_date) * 10000 + MONTH(@min_date) * 100 + DAY(@min_date) - @days_in_job_cycle,
                @int_date = YEAR(GETDATE()) * 10000 + MONTH(GETDATE()) * 100 + DAY(GETDATE())


        SELECT  @row_count  = 1,
                @job_name   = '',
                @start_time = GETDATE(),
                @stop_time  = DATEADD(mi, @max_minutes_runtime, GETDATE())

        WHILE @row_count > 0 AND (@stop_time IS NULL OR @stop_time > GETDATE())
          BEGIN

            SELECT      TOP 1
                        @job_name = sj.name,
                        @last_run_date = sjh.run_date
            FROM        msdb.dbo.sysjobs sj
                        LEFT OUTER JOIN msdb.dbo.sysjobschedules sjs
                            ON sj.job_id = sjs.job_id
                        LEFT OUTER JOIN msdb.dbo.sysschedules ss
                            ON sjs.schedule_id = ss.schedule_id
                                AND ss.[enabled] = 1
                        LEFT OUTER JOIN (
                                            SELECT      job_id,
                                                        MAX(run_date) AS run_date
                                            FROM        msdb.dbo.sysjobhistory
                                            WHERE       step_id = 0
                                                            AND run_date > @min_int_date
                                            GROUP BY    job_id 
                                        ) sjh
                            ON sj.job_id = sjh.job_id
            WHERE       (sjs.job_id IS NULL OR ss.schedule_id IS NULL)
                            AND sj.name LIKE @job_name_prefix
                                AND (sjh.run_date IS NULL OR sj.name > @job_name OR sjh.run_date < @int_date)
                                    AND sj.[enabled] = 1
            ORDER BY    ISNULL(sjh.run_date, 0),
                        sj.name
           
            SELECT  @row_count = @@ROWCOUNT

            IF @row_count > 0
             BEGIN

                EXEC dbo.sp_dba_GetSqlJobExecutionStatus @job_name = @job_name, @execution_status = @execution_status OUTPUT
                           
                IF @execution_status = 4 --Make sure job is not running
                    EXEC msdb.dbo.sp_start_job @job_name = @job_name
                   
                WAITFOR DELAY '00:00:02.000' -- Pause here to make sure the job gets started before checking the status

                EXEC dbo.sp_dba_GetSqlJobExecutionStatus @job_name = @job_name, @execution_status = @execution_status OUTPUT

                WHILE @execution_status != 4
                 BEGIN
                    WAITFOR DELAY '00:00:01.000'
                    EXEC dbo.sp_dba_GetSqlJobExecutionStatus @job_name = @job_name, @execution_status = @execution_status OUTPUT
                 END
                 
             END
             
            IF @stop_time IS NOT NULL AND @stop_time > GETDATE()
             BEGIN
                DECLARE @subject nvarchar(255),
                        @body    nvarchar(max)

                SELECT  @subject = @mail_job_name + ' on ' + @@SERVERNAME + ' - Shutting down...Time limit reached.',
                        @body = @mail_job_name + ' on ' + @@SERVERNAME + ' shut down after '
                                + CAST(ABS(DATEDIFF(mi, GETDATE(), @start_time)) AS nvarchar) + ' minutes.'

                EXEC msdb.dbo.sp_send_dbmail    @profile_name = @mail_profile_name,
                                                @recipients = @mail_recipients,
                                                @subject = @subject,
                                                @body = @body
             END
           
         END
           
     END

    GO

    EXEC sp_MS_marksystemobject 'sp_dba_run_unscheduled_jobs'
    GO

    Conclusion

    This is my new favorite utility stored procedure because I keep finding new uses for it. I hope you find this stored procedure as useful as I do. Please let me know if you run into any issues, have any ideas that would make it better or just want to share how you are using it. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged | 2 Comments

    Stored Procedure to Get SQL Job Execution Status

    Introduction

    Ever need a programmatic way to figure out if a SQL Agent job is running? I did. After spending a while searching on Google I did not turn up anything I liked so I turned to Profiler and started looking at what calls SQL Management Studio uses. After a bit of poking around I was able to come up with a stored procedure that I could call.

    The Meat

    The stored procedure is pretty straightforward. Get the job and owner from the system tables, declare a temporary table, call xp_sqlagent_enum_jobs with the results going into the temporary table, finally placing a return value in an output variable and optionally producing a recordset.
    Comments at the top of the stored procedure outline the various statuses as of the time of writing. My general rule is everything except a status of 4 means running.

    Here is the code:

    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
    USE [master]
    GO

    CREATE PROCEDURE [dbo].[sp_dba_GetSqlJobExecutionStatus](@job_name sysname, @select_data int =0, @execution_status int = NULL OUTPUT)

    AS

    SET NOCOUNT ON

    /*
    Is the execution status for the jobs.
    Value Description
    0 Returns only those jobs that are not idle or suspended.
    1 Executing.
    2 Waiting for thread.
    3 Between retries.
    4 Idle.
    5 Suspended.
    7 Performing completion actions

    */


    DECLARE @job_id uniqueidentifier,
    @is_sysadmin int,
    @job_owner sysname

    SELECT @job_id = jv.job_id,
    @job_owner = sp.name
    FROM msdb.dbo.sysjobs_view jv
    INNER JOIN sys.server_principals sp
    ON jv.owner_sid = sp.sid
    WHERE jv.name = @job_name

    SELECT @is_sysadmin = ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0)

    CREATE TABLE #xp_results
    (
    job_id uniqueidentifier NOT NULL,
    last_run_date int NOT NULL,
    last_run_time int NOT NULL,
    next_run_date int NOT NULL,
    next_run_time int NOT NULL,
    next_run_schedule_id int NOT NULL,
    requested_to_run int NOT NULL, -- BOOL
    request_source int NOT NULL,
    request_source_id sysname COLLATE database_default NULL,
    running int NOT NULL, -- BOOL
    current_step int NOT NULL,
    current_retry_attempt int NOT NULL,
    job_state int NOT NULL
    )

    INSERT INTO #xp_results
    EXECUTE master.dbo.xp_sqlagent_enum_jobs @is_sysadmin, @job_owner, @job_id

    SELECT @execution_status = job_state
    FROM #xp_results

    DROP TABLE #xp_results

    IF @select_data =1
    SELECT @job_name AS job_name,
    @execution_status AS execution_status

    SET NOCOUNT OFF
    GO

    EXEC sp_MS_marksystemobject 'sp_dba_GetSqlJobExecutionStatus'

    Conclusion

    I hope you find this stored procedure as useful as I do. Please let me know if you run into any issues or have any ideas that would make it better. As always scripts from the internet are like Halloween candy, inspect before consumption. I offer no warranty beyond a sympathetic ear if you should run into any issues.

    Posted in Utilities | Tagged , , , , , , | 5 Comments

    Using sp_ExecuteSql to Run Dynamic SQL

    Introduction

    Lately it seems like I am being bombarded by Dynamic SQL and people who insist it is the only way to accomplish something. Dynamic SQL is almost never the best way to accomplish a given task, but sometimes (and I do mean just sometimes) it is. In the post I will show why to use sp_ExecuteSql and how to convert an existing procedure to use it properly.

    The Example

    Below is an example stored procedure that I came up with that takes multiple parameters and based on those parameters formulates and executes a query. It is intentionally easy and could well be written otherwise but it works well for this purpose.

    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
    CREATE PROCEDURE dbo.GetTableInfo @object_id int = null, @name sysname = null
    AS
    DECLARE @sql_command nvarchar(max)

    SELECT @sql_command = N'
    SELECT [name],
    [object_id]
    FROM sys.tables
    '


    IF @object_id IS NOT NULL
    BEGIN
    SELECT @sql_command = @sql_command + N'WHERE [object_id] = ' + CAST(@object_id AS nvarchar)
    IF @name IS NOT NULL
    BEGIN
    SELECT @sql_command = @sql_command + N'
    AND [name] = '
    '' + @name + ''''
    END
    END
    ELSE
    BEGIN
    IF @name IS NOT NULL
    BEGIN
    SELECT @sql_command = @sql_command + N'WHERE [name] = ''' + @name + ''''
    END
    ELSE
    BEGIN
    SELECT @sql_command = N''
    END
    END

    PRINT @sql_command
    EXEC sp_executesql @sql_command

    The stored procedure takes 2 parameters, @object_id and/or @name. Based on the inputs a WHERE clause is created and appended to the rest of the SQL statement. In case anyone tries to pull a fast one to see a full table list by submitting null for both arguments a check has been added to clear the string, causing nothing to be returned. At this point, the example looks fairly robust and somewhat bulletproof.

    The Test(s)

    Testing the stored procedure should be fairly simple. To keep things easy I will test in msdb by searching for the table sysjobs by name, object_id and then both. I will then test for the possibility of SQL Injection.

    Test #1

    EXEC dbo.GetTableInfo @name = ‘sysjobs’

    Returns:

    SELECT [name],
    [object_id]
    FROM sys.tables
    WHERE [name] = ‘sysjobs’

    name object_id
    —————– —————
    sysjobs 277576027

    (1 row(s) affected)

    Test #2

    EXEC dbo.GetTableInfo @object_id = 277576027

    Returns:

    SELECT [name],
    [object_id]
    FROM sys.tables
    WHERE [object_id] = 277576027

    name object_id
    —————– —————
    sysjobs 277576027

    (1 row(s) affected)

    Test #3

    EXEC dbo.GetTableInfo @object_id = 277576027, @name = ‘sysjobs’

    Returns:

    SELECT [name],
    [object_id]
    FROM sys.tables
    WHERE [object_id] = 277576027
    AND [name] = ‘sysjobs’

    name object_id
    —————– —————
    sysjobs 277576027

    (1 row(s) affected)

    Test #4 – SQL Injection

    EXEC dbo.GetTableInfo @name = ‘sysjobs” OR ”1”=”1”; SELECT name FROM sys.server_principals WHERE name != ”’

    Returns:

    SELECT [name],
    [object_id]
    FROM sys.tables
    WHERE [name] = ‘sysjobs’ OR ’1′=’1′; SELECT name FROM sys.server_principals WHERE name != ”

    name object_id
    —————– —————
    syssubsystems 5575058
    sysproxysubsystem 21575115
    restorefilegroup 30623152
    .
    .
    .

    (97 row(s) affected)

    name
    ——————————-
    sa
    public
    sysadmin
    .
    .
    .

    (33 row(s) affected)

    The Problem

    Testing looked great until the SQL Injection test. By mangling the parameters passed into the stored procedure I was able to get it to dump a list of all logins on the server. There is no reason that a call to a stored procedure could not be added in there instead followed by “–” to comment out the rest of the string. If xp_cmdshell were unsecured on this server the box would now belong to anyone that could exploit this vulnerability. This is about as bad as it gets short of leaving the sa password blank.

    The Fix

    In the example above I made a key error to prove a point. Even though I used sp_ExecuteSql, I used it in a way that was no better than using the EXEC statement. The proper way to use it is to build a parameterized SQL statement and pass the parameters into sp_ExecuteSql seperately. I know this sounds like a lot of extra work but an important thing to note is that all of the parameters can be passed for every call and will only used if they are in the SQL string that is passed in.

    The added benefit of parameterized SQL is the elimination of procedure cache bloat. The example above creates a unique query string that will cause a plan to be added to the procedure cache every time the procedure is called unless the same exact parameters are passed. The fixed or improved version will add 3 plans to the procedure cache, slightly more if there are wide variations in the data but far less than the 1 for every call the example will add.

    Here is the fixed version:

    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
    CREATE PROCEDURE dbo.GetTableInfo @object_id int = null, @name sysname = null
    AS
    DECLARE @sql_command nvarchar(max),
    @sql_parameters nvarchar(max)

    SELECT @sql_command = N'
    SELECT [name],
    [object_id]
    FROM sys.tables
    '
    ,
    @sql_parameters = N'@object_id int, @name sysname'

    IF @object_id IS NOT NULL
    BEGIN
    SELECT @sql_command = @sql_command + N'WHERE [object_id] = @object_id'
    IF @name IS NOT NULL
    BEGIN
    SELECT @sql_command = @sql_command + N'
    AND [name] = @name'

    END
    END
    ELSE
    BEGIN
    IF @name IS NOT NULL
    BEGIN
    SELECT @sql_command = @sql_command + N'WHERE [name] = @name'
    END
    ELSE
    BEGIN
    SELECT @sql_command = N''
    END
    END

    PRINT @sql_command
    EXEC sp_executesql @sql_command, @sql_parameters, @object_id = @object_id, @name = @name

    GO

    The Re-Test(s)

    Testing the stored procedure should be fairly simple. To keep things easy I will test in msdb by searching for the table sysjobs by name, object_id and then both. I will then test for the possibility of SQL Injection.

    Test #1

    EXEC dbo.GetTableInfo @name = ‘sysjobs’

    Returns:

    SELECT [name],
    [object_id]
    FROM sys.tables
    WHERE [name] = @name

    name object_id
    —————– —————
    sysjobs 277576027

    (1 row(s) affected)

    Test #2

    EXEC dbo.GetTableInfo @object_id = 277576027

    Returns:

    SELECT [name],
    [object_id]
    FROM sys.tables
    WHERE [object_id] = @object_id

    name object_id
    —————– —————
    sysjobs 277576027

    (1 row(s) affected)

    Test #3

    EXEC dbo.GetTableInfo @object_id = 277576027, @name = ‘sysjobs’

    Returns:

    SELECT [name],
    [object_id]
    FROM sys.tables
    WHERE [object_id] = @object_id
    AND [name] = @name

    name object_id
    —————– —————
    sysjobs 277576027

    (1 row(s) affected)

    Test #4 – SQL Injection

    EXEC dbo.GetTableInfo @name = ‘sysjobs” OR ”1”=”1”; SELECT name FROM sys.server_principals WHERE name != ”’

    Returns:

    SELECT [name],
    [object_id]
    FROM sys.tables
    WHERE [name] = @name

    name object_id
    —————– —————

    (0 row(s) affected)

    Conclusion

    Dynamic SQL should be considered a highly specialized tool of last resort and used in the way I have described. As I have shown, misuse can lead to your server and the data stored on it no longer belonging to you.

    Posted in Dynamic SQL, Security | Tagged | Leave a comment

    Stored Procedures to Store and Get Database Users with All Permissions

    Introduction
    Traditionally when doing a restore, moving a database, etc. a DBA would simply go into Management Studio and use the Generate Scripts wizard to script all logins and permissions. This approach can take several minutes and as I recently learned does not script database level permissions.

    Solution
    Bothered by the amount of effort and the fact that I kept forgetting to script out the permissions until just after I had wiped them out by starting a restore, I set out to create scripts that I could just include as steps in my restore jobs. Given that the restore wipes out the database, I knew I had to have 2 steps. The first step stores the permissions before the restore, while the second puts them back after. In the spirit of keeping the restore jobs simple, I wrapped up all of the logic into 2 stored procedures that do not require any arguments.

    The first stored procedure, sp_dba_StoreDatabasePermissions, stores the logins roles and permissions to a table in msdb. I use msdb here because everybody has it and it is not master. The table is named for the database it corresponds to followed by ‘_permission_store’. The permission store table has 2 columns, the first is the actual SQL command while the second is the order to run it in. The ordering is done in groups, with all roles ranked to be run first, followed by users, adding users to roles and finally the permissions that correspond to the users and roles. The stored procedure makes use of a synonym to point to the permission store, cutting down on the use of dynamic SQL and enhancing readability. The logic to get the permissions is based on the logic of a script by Narayana Vyas Kondreddi.

    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
    USE [master]
    GO
    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER OFF
    GO
    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_dba_StoreDatabasePermissions]') AND type in (N'P', N'PC'))
        DROP PROCEDURE [dbo].[sp_dba_StoreDatabasePermissions]
    GO

    CREATE PROCEDURE [dbo].[sp_dba_StoreDatabasePermissions]

    AS

     BEGIN

        DECLARE @table_name sysname,
                @create_cmd nvarchar(4000)

        SELECT  @table_name = db_name() + '_permission_store'

        IF NOT EXISTS(SELECT * FROM msdb.sys.sysobjects WHERE name = @table_name)
         BEGIN
            SELECT  @create_cmd = 'CREATE TABLE [msdb].[dbo].' + QUOTENAME(@table_name) + ' (command nvarchar(max), run_order int)'
         END

        EXEC    sp_executesql  @create_cmd

        SELECT  @create_cmd = 'IF NOT EXISTS (SELECT * FROM sys.synonyms WHERE name = N''permission_store'') CREATE SYNONYM permission_store FOR [msdb].[dbo].' + QUOTENAME(@table_name)
        EXEC    sp_executesql  @create_cmd

        INSERT  permission_store
        SELECT  'IF NOT EXISTS(SELECT * FROM sys.database_principals WHERE name = ''' + name + ''') CREATE ROLE [' + name + ']' AS Command,
                0 AS run_order
        FROM    sys.database_principals
        WHERE   type_desc = 'DATABASE_ROLE'
                    AND is_fixed_role = 0

        INSERT  permission_store
        SELECT  'IF NOT EXISTS(SELECT * FROM sys.database_principals WHERE name = ''' + name + ''') CREATE USER [' + name + '] FOR LOGIN [' + name + ']' + ISNULL(' WITH DEFAULT_SCHEMA=[' + default_schema_name + ']', '') AS Command,
                1 AS run_order
        FROM    sys.database_principals
        WHERE   type_desc in ('WINDOWS_GROUP', 'SQL_USER','WINDOWS_USER') --Updated to include WINDOWS_USER on 10/5/2011. Thanks to Alex Hatcher for catching that.

        INSERT  permission_store
        SELECT  'EXEC sp_addrolemember @rolename=''' + dp_r.name + ''', @membername =''' + dp_m.name + '''' AS Command,
                2 AS run_order
        FROM    sys.database_role_members drm
                INNER JOIN sys.database_principals dp_r
                    ON drm.role_principal_id = dp_r.principal_id
                INNER JOIN sys.database_principals dp_m
                    ON drm.member_principal_id = dp_m.principal_id
        WHERE   dp_m.name NOT IN ('dbo')

        INSERT      permission_store
        SELECT      'IF EXISTS(SELECT * FROM sys.objects WHERE name = ''' + obj.name + ''' AND USER_NAME(schema_id) = ''' + USER_NAME(obj.schema_id) + ''' AND type = ''' + RTRIM(obj.type) + ''') ' +
                    CASE WHEN perm.state <> 'W' THEN perm.state_desc ELSE 'GRANT' END
                    + ' ' + perm.permission_name + ' ' + 'ON ' + QUOTENAME(USER_NAME(obj.schema_id)) + '.' + QUOTENAME(obj.name)
                    + CASE WHEN cl.column_id IS NULL THEN '' ELSE '(' + QUOTENAME(cl.name) + ')' END
                    + ' TO ' + QUOTENAME(usr.name) COLLATE database_default
                    + CASE WHEN perm.state <> 'W' THEN '' ELSE ' ' + 'WITH GRANT OPTION' END AS Command,
                    3 AS run_order
        FROM        sys.database_permissions AS perm
                    INNER JOIN sys.objects AS obj
                        ON perm.major_id = obj.[object_id]
                    INNER JOIN sys.database_principals AS usr
                        ON perm.grantee_principal_id = usr.principal_id
                    INNER JOIN sys.database_principals AS adm
                        ON perm.grantor_principal_id = adm.principal_id
                    LEFT JOIN sys.columns AS cl
                        ON cl.column_id = perm.minor_id AND cl.[object_id] = perm.major_id
     END
    GO
    EXEC sp_MS_marksystemobject 'sp_dba_StoreDatabasePermissions'
    GO

    The second stored procedure in the pair, sp_dba_GetDatabasePermissions, is very simple. First it checks for the synonym in the database and creates it if it is missing. After verifying the synonym, the stored procedure opens up a cursor against the permission store and begins executing commands ordered ascending by run_order. After running all commands, the stored procedure checks the value of the optional parameter @keep_permission_store to see if it should clean up the permission store table or leave it out there. The default behavior is to drop the permission store when done with it.

    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
    USE [master]
    GO
    SET ANSI_NULLS ON
    GO
    SET QUOTED_IDENTIFIER OFF
    GO
    IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[sp_dba_GetDatabasePermissions]') AND type in (N'P', N'PC'))
        DROP PROCEDURE [dbo].[sp_dba_GetDatabasePermissions]
    GO

    CREATE PROCEDURE [dbo].[sp_dba_GetDatabasePermissions] @keep_permission_store bit = 0

    AS

     BEGIN


        DECLARE @command nvarchar(max)

        SELECT  @command = 'IF NOT EXISTS (SELECT * FROM sys.synonyms WHERE name = N''permission_store'') CREATE SYNONYM permission_store FOR [msdb].[dbo].' + QUOTENAME(db_name() + '_permission_store')
        EXEC    sp_executesql  @command

        DECLARE commands CURSOR FAST_FORWARD READ_ONLY FOR
            SELECT      command
            FROM        dbo.permission_store
            ORDER BY    run_order

        OPEN commands

        FETCH NEXT from commands INTO @command

        WHILE(@@FETCH_STATUS <> -1)
         BEGIN
            PRINT @command
            EXEC sp_executesql @command
            FETCH NEXT from commands INTO @command
         END

        CLOSE commands

        DEALLOCATE commands

        IF @keep_permission_store != 1
         BEGIN
            SELECT @command = 'DROP TABLE ' + base_object_name FROM sys.synonyms WHERE name = 'permission_store'
            DROP SYNONYM dbo.permission_store
            PRINT @command
            EXEC sp_executesql @command
         END
     END
    GO
    EXEC sp_MS_marksystemobject 'sp_dba_GetDatabasePermissions'
    GO

    Other Uses
    It seems like every day I find more uses for these stored procedures. Most recently I have been running them at replication subscribers before making any changes at the publisher. The @keep_permission_store flag of sp_dba_GetDatabasePermissions comes in really handy here. Passing a value of 1 allows permissions to be put back several times, a lifesaver when things do not go right on the first try.

    I also use these stored procedures to copy permissions between databases on different servers. I started out running sp_dba_StoreDatabasePermissions then selecting from the permission store table ordered by rank in text, copying, pasting and running on the new server. After adding the print statement to sp_dba_GetDatabasePermissions it was just easier to call sp_dba_StoreDatabasePermissions then sp_dba_GetDatabasePermissions right away, copying the commands from the messages window and pasting them into a query window to run wherever I want.

    Wrap-Up
    These stored procedures have served me well and I hope they will serve you well. I would love to hear how people are using them and any suggestions for enhancements. As always, scripts on the internet are like Halloween candy, check them thoroughly before consuming.

    Posted in Security, User Management, Utilities | Tagged , , , , | 3 Comments

    Script Individual User Rights in a Database with PowerShell

    Introduction

    Tonight on Twitter the call went out on #SqlHelp looking for a way to script just database permissions for a specific user. I wrote a script once that I was pretty sure did that so I jumped in to help. It turns out that I lost that particular script and it really did not do the right thing anyway. Somewhat embarassed I decided I better stay up and get a script written to solve the problem rather than admit to wasting someone’s time.

    The Result

    After a couple of hours of hacking through the various SMO classes I was able to cobble together the script below. I had forgetten how much I enjoyed challenges like this from my developer days so this was actually quite a treat.

    Here is the code:
     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    $serverName="localhost"
    $databaseName="AdventureWorks"

    $serverConnection = new-object Microsoft.SqlServer.Management.Common.ServerConnection
    $serverConnection.ServerInstance=$serverName

    $server = new-object Microsoft.SqlServer.Management.SMO.Server($serverConnection)
    $database = $server.Databases[$databaseName]
    foreach($user in $database.Users)
    {
    foreach($databasePermission in $database.EnumDatabasePermissions($user.Name))
    {
    Write-Host $databasePermission.PermissionState $databasePermission.PermissionType "TO" $databasePermission.Grantee
    }
    foreach($objectPermission in $database.EnumObjectPermissions($user.Name))
    {
    Write-Host $objectPermission.PermissionState $objectPermission.PermissionType "ON" $objectPermission.ObjectName "TO" $objectPermission.Grantee
    }
    }
    $server.ConnectionContext.Disconnect()

    Update
    The discussion continues on this one.  The current thinking is that you should be able to call User.Script(). Jonathan Kehayias (Blog, Twitter) will be logging a Connect item and I will post voting information here.

    Posted in PowerShell | Tagged | 1 Comment

    A Busy/Accidental DBA’s Guide to Managing VLFs

    Introduction

    Properly managing VLFs can make or break the performance of your databases. There is a ton of information out there on the proper management of VLFs, but nothing I have found that tries to boil it down to the most important parts. So here it is, my attempt at A Busy/Accidental DBA’s Guide to Managing VLFs.

    What are VLFs?

    When SQL Server allocates new space in a log file it does it using Virtual Log Files (VLFs), meaning every growth of a transaction log file is made of 4 or more VLFs.  Think of VLFs as small files within the file that are easier for SQL Server to manage than one large file. (There really is a lot more to it than that but rather than lift from BOL I will refer you to this page for a more detailed explanation.)

    Why Manage VLFs?

    Having too many or in some cases not enough VLFs can cause sluggish database performance. I have also heard cases of database recovery taking far longer than expected when a log file contains too many VLFs.

    How Many VLFs Should I have?

    To quote someone much wiser: “It depends”. I use 50 VLFs as my rule of thumb because it is much easier to have a simple rule and it is a safe number in most cases. I do suggest reading this article: Transaction Log VLFs – too many or too few? before committing to a number of your own, especially if you are working with VLDBs.

    How do I Manage VLFs?

    Managing VLFs is a 2 step process. Step 1 is figuring out how many VLFs you have in each of your transaction logs. Step 2 is deciding on what number of VLFs is acceptable to you and shrinking and growing the log files to get them back under your threshold.  I have included scripts below that will help you identify and remediate high VLF counts. They probably could be wrapped up into a single script but I prefer to have control of what is running when so I can monitor for any issues the maintenance might cause.

    Many people also add a step 3 where they increase the auto-growth increment of their database. I tend to avoid raising the auto-growth unless the database is new. The log should only grow very rarely on a mature database; constantly having to address VLFs in a particular database’s log could be a sign of a larger problem like auto-shrink being turned on.

    What if I Just Shrink the Log and Let it Grow Back?

    There is a misconception that shrinking a log and increasing the auto-growth is enough to remediate high VLF counts. While shrinking a log file may lower VLF counts temporarily, they will come right back when the log file grows back. This article: Transaction Log VLFs – too many or too few? lays out how many VLFs will be added based on the auto-growth increment.  Rephrased from the article:

    •       If the file growth is less than 64MB the new portion of the log file will contain 4 VLFs
    •       If the file growth is at least 64MB and less than 1GB the new portion of the log file will contain 8 VLFs
    •       If the file growth is at least 1GB and larger = 16VLFs

    Based on that, if an 80GB log with 100 VLFs was shrunk to remove VLFs then allowed to auto-grow back to 80GB with a larger auto-growth increment, say 4GB, the log would contain 20*16 = 320 VLFs.

    How Many VLFs are in My Databases?

    This script will return the VLF count for each database on the server it is run on. I am not sure of the origins of the script but I can say it works for me.  If you know or are the original author of this script please let me know so I can give proper credit or replace the script with a link to a more current version.

    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
    DECLARE @query varchar(1000),
    @dbname varchar(1000),
    @count int

    SET NOCOUNT ON

    DECLARE csr CURSOR FAST_FORWARD READ_ONLY
    FOR
    SELECT name
    FROM master.dbo.sysdatabases

    CREATE TABLE ##loginfo
    (
    dbname varchar(100),
    num_of_rows int)

    OPEN csr

    FETCH NEXT FROM csr INTO @dbname

    WHILE (@@fetch_status <> -1)
    BEGIN

    CREATE TABLE #log_info
    (
    fileid tinyint,
    file_size bigint,
    start_offset bigint,
    FSeqNo int,
    [status] tinyint,
    parity tinyint,
    create_lsn numeric(25,0)
    )

    SET @query = 'DBCC loginfo (' + '''' + @dbname + ''') '

    INSERT INTO #log_info
    EXEC (@query)

    SET @count = @@rowcount

    DROP TABLE #log_info

    INSERT ##loginfo
    VALUES(@dbname, @count)

    FETCH NEXT FROM csr INTO @dbname

    END

    CLOSE csr
    DEALLOCATE csr

    SELECT dbname,
    num_of_rows
    FROM ##loginfo
    WHERE num_of_rows >= 50 --My rule of thumb is 50 VLFs. Your mileage may vary.
    ORDER BY dbname

    DROP TABLE ##loginfo

    How Do I Lower a Database’s VLF Count?

    Once armed with a list of databases that have high VLF counts, the next step is to shrink the logs to as small as possible then grow them back to the original size, ideally in a single growth. This is best done during off-peak times. I wrote the following script to perform those exact steps given the appropriate USE statement. You may have to run it multiple times to get to a low enough VLF count.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /*USE <db_name>*/ --Set db name before running using drop-down above or this USE statement

    DECLARE @file_name sysname,
    @file_size int,
    @file_growth int,
    @shrink_command nvarchar(max),
    @alter_command nvarchar(max)

    SELECT  @file_name = name,
            @file_size = (size / 128)
    FROM    sys.database_files
    WHERE   type_desc = 'log'

    SELECT @shrink_command = 'DBCC SHRINKFILE (N''' + @file_name + ''' , 0, TRUNCATEONLY)'
    PRINT @shrink_command
    EXEC sp_executesql @shrink_command

    SELECT @shrink_command = 'DBCC SHRINKFILE (N''' + @file_name + ''' , 0)'
    PRINT @shrink_command
    EXEC sp_executesql @shrink_command

    SELECT @alter_command = 'ALTER DATABASE [' + db_name() + '] MODIFY FILE (NAME = N''' + @file_name + ''', SIZE = ' + CAST(@file_size AS nvarchar) + 'MB)'
    PRINT @alter_command
    EXEC sp_executesql @alter_command

    In Closing

    This has by no means a comprehensive lesson in VLFs or transaction log management, but hopefully enough to get the job done. If you are looking for a more in-depth look at VLFs and transaction logs in general I suggest reading the following articles: Understanding Logging and Recovery in SQL Server, Transaction Log VLFs – too many or too few? and 8 Steps to better Transaction Log throughput.

    Posted in File Management | Tagged , , | 11 Comments

    How to Shrink TempDB in SQL 2005

    Introduction

    From time to time you find yourself needing to shrink some space out of TempDB. Shrinking database files is never my first choice but sometimes it is the best I have. Many people think that you cannot shrink TempDB in SQL 2005, but I am going to show you how.

    Why would I need to shrink TempDB?

    Yesterday afternoon my pager started going crazy because an Ad-Hoc query that needed some tuning filled TempDB on a server. Luckily, the user only impacted their own query so it was easy to quickly identify them and work with the right people to get the query rewritten.

    Once the immediate problem was resolved there had to be some cleanup. On this server, TempDB has 32 files (1 per processor) all on the same disk. The full database condition caused all kinds of alerts in our monitoring tools, from drive space alerts to too few growths remaining. There were 3 possible solutions to quiet the alerts:

    1. Reboot – There is never a good time to reboot a production server

    2. Turn off the Alerts – Not really an option. My preference would be for increasing the sensitivity

    3. Shrink TempDB – Not a great option, but the best of the 3

    Shrinking TempDB

    Once we had decided that we would go ahead and shrink the files in TempDB it seemed like the hard part was done, but after running the following command:

    USE [tempdb]

    GO

    DBCC SHRINKFILE (N’tempdev’ , 5000)

    GO

    I got back the following:

    DBCC SHRINKFILE: Page 1:878039 could not be moved because it is a work file page.

    DbId FileId CurrentSize MinimumSize UsedPages EstimatedPages

    —— ———– ———– ———– ———– ————–

    2 1 878040 640000 4672 4672

     

    (1 row(s) affected)

     

    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

     

    “Page could not be moved because it is a work file page.”…grrr. This is a new thing in SQL 2005 caused by the caching that is done in TempDB. I am not going to try to explain here how objects are cached in TempDB, but Kalen Delaney’s Inside Sql Server Series is a great place to learn about it if you are interested (http://www.insidesqlserver.com/books.html). What is important is that the cached objects are tied to a query plan and that by freeing the procedure cache you can make those objects go away, allowing you to shrink your files.

    Trying again:

    DBCC FREEPROCCACHE

    GO

    USE [tempdb]

    GO

    DBCC SHRINKFILE (N’tempdev’ , 5000)

    GO

    This time it worked:

     

    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    DbId FileId CurrentSize MinimumSize UsedPages EstimatedPages

    —— ———– ———– ———– ———– ————–

    2 1 640000 640000 264 264

     

    (1 row(s) affected)

     

    DBCC execution completed. If DBCC printed error messages, contact your system administrator.

    I think I got lucky that the shrink worked on the first try. There will certainly be times when you have to try freeing the procedure cache and shrinking multiple times to get a file to shrink, but eventually it will get the job done.

    Posted in File Management | Tagged , , , , | 7 Comments