By: Tim Smith | Updated: 2014-10-07 | Comments (1) | Related: More > Database Administration
Problem
An old idiom states that an ounce of prevention is worth a pound of cure and I haven't met a DBA who disagrees. We tend to focus our efforts on some prevention in many cases. One particular case I've seen become more common with some of my clients, as well as other environments where DBAs have uttered those dreaded words, "Our environment is a mess." I remember speaking with an awesome Oracle DBA who was managing both Oracle and SQL Server environments who told me, "Our environment is a mess - and it's hard to know who's behind the mess; we don't know who created this or that object and why, and we don't even know if half the code in these environments is even necessary any more." How can we begin to think, plan and - most importantly - act on building our ounce of prevention?
Solution
Most clients agree that ideally they want to know who created an object and what the object does. For instance, if an environment has a stored procedure called MyProcedure17, that indicates nothing about what it does. It becomes worse when we look at the procedure code and we don't find a name or description attached to it. Is that object used? If we needed to find out, who would we call? If it failed, is it important?
Obviously, this could become fairly deep (as far as a solution) because, depending on our environment, we may need to know even more than that basic information. For the sake of focus and providing ideas, I will focus this tip on stored procedures because of what they can do - SELECT, UPDATE, DELETE, INSERT, and run a myriad of commands, provided that the user running them has permission to do so. This same logic can be expanded though - to views, functions, triggers, etc., if needed; and for some environments (OLAP), I would highly suggest expanding it to views. Also, we'll look at two specific cases: a procedure carries the appropriate environment convention with a proper name and author, or - if the author leaves - it has a description (and now with modern technology, a link to a page describing what it does is even better).
Naming Conventions for SQL Server Objects
For some environments, the name of an object means a lot - it states what it is, what database it belongs to, and summarizes what it does. For instance, stp_BulkInsert alerts in the beginning that it is a stored procedure and that it INSERTS. The lack of it having a database name attached to it means that it could be universally used by any database - if an object only operates within one database, a good naming convention might let users know that. For instance, stp_HFT_InsertValues, tells us that our object is a stored procedure belonging to the database HFT and inserts values.
Getting Alerts for Stored Procedure that don't conform
If we want our environment to alert us about procedures like MyProcedure17, we can automate this process in PowerShell and resolve the problem quickly so that our environment doesn't slowly become a mess. Simply put: if we prevent developers from using any name they want, we can maintain an organized environment, and this is easily possible even for start ups, as we'll see. Finally, by implementing a process like this, we can more easily identify unused processes, such as stored procedures, like this tip covers.
First, we will want to check the name. For this example, we want our procedures to indicate that they are procedures, as this will make things easy in source control, or when we look at other articles like identifying where procedures aren't used. If a procedure can have the same name as a table, automating what is used and what isn't used becomes an added challenge. Since our example uses the naming convention of stp_ to indicate a procedure (though it could be anything like proc_ or ending in _proc, we will find all procedures that don't have that leading their name and which are user stored procedures:
foreach ($p in $db.StoredProcedures | Where-Object {$_.IsSystemObject -eq $false}) { $pName = $p.Name if ($pName.Substring(0,4) -ne "stp_") { ## Do something } }
What we're doing here is first detecting if the user stored procedure matches our naming convention - if it doesn't, it falls inside the if clause. We will then expand that to include detecting whether it links to a documentation page. Note that this is my preference because on a documentation page, we can specify a lot more than using comments. Because a documentation page will lead with "https://" it makes it easier to find and identify if one exists (if we use a company domain, we can expand it). Finally, I prefer a documentation page because people move on and if they leave, a documentation page helps future developers - we can't always contact the developer behind a piece of code and ask, so it's best to have an explanation for why something was constructed in a certain way. As a living example of how this can become a problem, two DBAs and I recently met about code management in their environment, and even with source control implemented, when processes broke, it took them (1) hours to isolate the problem, and (2) any change they implemented often had effects on other processes. Even with comments left in the code, the overall architecture hadn't been documented on even a minor level causing newer developers to spend more time trying to figure out existing and, possibly broken, processes instead of being able to isolate the problem quickly.
In our example, the details of the procedure would go in the header, not in the body of text - if you wanted the details in the body, you would change $p.TextHeader.Replace("'","") with $p.TextBody.Replace("'","").
foreach ($p in $db.StoredProcedures | Where-Object {$_.IsSystemObject -eq $false}) { $pName = $p.Name $pTxt = $p.TextHeader.Replace("'","") if (($pName.Substring(0,4) -ne "stp_") -or ($pTxt -notlike "*Details: https://*")) { ## Do something } }
In the cases we don't point to a documentation page and want a description:
foreach ($p in $db.StoredProcedures | Where-Object {$_.IsSystemObject -eq $false}) { $pName = $p.Name $pTxt = $p.TextHeader.Replace("'","") if (($pName.Substring(0,4) -ne "stp_") -or ($pTxt -notlike "*Description:*")) { ## Do something } }
In both cases, we have set a standard for our environment involving stored procedures. Part of our preventative measures is identifying any procedure that doesn't fall into this realm. In addition, if we run this every night (a suggestion), we can easily spot an exception quickly and have it resolved. "George, I saw that you wrote a new procedure and it doesn't match our convention. We have a How-To page telling you how to make sure your procedure fits our environment, here [How-To page]. Thanks!" or if we have multiple developers per database/server, we can email the entire team, "Team, we've identified a stored procedure without documentation called MyProcedure17. If you're the developer behind this, please correct per our documentation at [documentation link]. If this isn't corrected within a week, we will script and remove this procedure. Thanks!"
Getting Alerts for Other Objects that don't conform
We can also expand this logic if we want to include views, functions, triggers, etc.; for instance, the code below this looks at views as an example, which some environments will find benefit to adding:
foreach ($v in $db.Views | Where-Object {$_.IsSystemObject -eq $false}) { $vName = $v.Name $vTxt = $v.TextHeader.Replace("'","") if (($vName.Substring(0,5) -ne "view_") -or ($vTxt -notlike "*Details: https://*") { ## Do something } }
As we'll see later, we can expand the final function to include objects we tend to use frequently
Getting More Details in the Alert
Now that we've identified the procedures (or other objects) that we know don't match our environment standards, we want to get the time they were created and compare that with the current time. In this example, I use a timeframe of seven days, though your environment may use a longer or shorter timeframe. Never assume that you can use someone else's exact timeframe unless you're the Senior DBA and know exactly when you can apply these solutions.
## These variables will be added to the first part of our function $now = Get-Date $evme_dev_email_list = "John Doe, Jane Doe " $evme_subject = "WARNING: Object Will Be Deleted" ## We'll be looking at the date the object was created and how that compares to now $proc_max = ($p.CreateDate).AddDays(7) if ($proc_max -le $now) { $drop_array += $p.Name } else { $evme_body = "Object, $pName, has been named incorrectly and will be removed seven days from the date that it was created. If you created this object, correct immediately, following our convention at https://www.OurEnvironment.com/OurNamingConvention.html." ## The below is PowerShell's built in Send-MailMessage function Send-MailMessage -From "[email protected]" -To $evme_dev_email_list -SmtpServer "smtp.OurEnvironment.com" -Subject $evme_subject -Body $evme_body -Priority High } ## Outside of our loop, we will loop over our array, script and remove the procedures foreach ($drop in $drop_array) { $db.StoredProcedures["$drop"].Script() | Out-File "C:\Scripts\$drop.sql" $db.StoredProcedures["$drop"].Drop() }
In .NET, we cannot modify a collection while looping over it, so the approach I like to use is to add the items we find to an array and then go back and modify the objects in the array. If it's been longer than seven days since the procedure was created, it's scripted and removed, otherwise, the developer list receives an email letting them know that it will be removed from the date it was created. This part of the process will vary, depending on your environment, and you can change as necessary. So let's look at the function pointed at procedures all together:
Function MaintainEnvironment_Procedures ($server, $smolibrary) { Add-Type -Path $smolibrary $now = Get-Date $evme_dev_email_list = "John Doe, Jane Doe " $evme_subject = "WARNING: Object Will Be Deleted" $serv = New-Object Microsoft.SqlServer.Management.SMO.Server($server) foreach ($db in $serv.Databases | Where-Object {$_.IsSystemObject -eq $false}) { $d = $db.Name $drop_array = @() foreach ($p in $db.StoredProcedures | Where-Object {$_.IsSystemObject -eq $false}) { $proc_max = ($p.CreateDate).AddDays(7) $pName = $p.Name $pTxt = $p.TextHeader.Replace("'","") if (($pName.Substring(0,4) -ne "stp_") -or ($pTxt -notlike "*Details: https://*")) { if ($proc_max -le $now) { $drop_array += $p.Name } else { $evme_body = "Object, $pName, has been named incorrectly and will be removed seven days from the date that it was created. If you created this object, correct immediately, following our convention at https://www.OurEnvironment.com/OurNamingConvention.html." Send-MailMessage -From "[email protected]" -To $evme_dev_email_list -SmtpServer "smtp.ourenvironment.com" -Subject $evme_subject -Body $evme_body -Priority High } } } foreach ($drop in $drop_array) { $db.StoredProcedures["$drop"].Script() | Out-File "C:\Scripts\$drop.sql" $db.StoredProcedures["$drop"].Drop() } } } MaintainEnvironment -server "OurEnvironmentServer\OurEnvironmentInstance" -smolibrary "C:\Program Files (x86)\Microsoft SQL Server\100\SDK\Assemblies\Microsoft.SqlServer.Smo.dll"
What if we want to look at multiple objects - for instance, functions, procedures and views? Because the naming convention will be different for each (for instance, a procedure might start with stp_ whereas a view will start with view_), we will look at the headers only to ensure they have documentation, and note the line below ## Add or subtract the objects we use regularly here as items in the below array: as we can expand functionality:
Function MaintainEnvironment_HeadersOnly ($server, $smolibrary) { Add-Type -Path $smolibrary $now = Get-Date $evme_dev_email_list = "John Doe, Jane Doe " $evme_subject = "WARNING: Object Will Be Deleted" $serv = New-Object Microsoft.SqlServer.Management.SMO.Server($server) ## Add or subtract the objects we use regularly here as items in the below array: $loop_objects = @("UserDefinedFunctions","StoredProcedures","Views") foreach ($db in $serv.Databases | Where-Object {$_.IsSystemObject -eq $false}) { $d = $db.Name foreach ($loop in $loop_objects) { $drop_array = @() foreach ($lo in $db.$loop | Where-Object {$_.IsSystemObject -eq $false}) { $lo_max = ($lo.CreateDate).AddDays(7) $lTxt = $lo.TextHeader.Replace("'","") $lName = $lo.Name if (($lo_max -le $now) -and ($lTxt -notlike "*Details: https://*")) { $drop_array += $lName } else { $evme_body = "Object, $lName, has been named incorrectly and will be removed seven days from the date that it was created. If you created this object, correct immediately." Send-MailMessage -From "[email protected]" -To $evme_dev_email_list -SmtpServer "smtp.OurEnvironment.com" -Subject $evme_subject -Body $evme_body -Priority High } } foreach ($drop in $drop_array) { $db.$loop["$drop"].Script() | Out-File "C:\Scripts\$drop.sql" $db.$loop["$drop"].Drop() } } } } MaintainEnvironment_HeadersOnly -server "OurEnvironmentServer\OurEnvironmentInstance" -smolibrary "C:\Program Files (x86)\Microsoft SQL Server\100\SDK\Assemblies\Microsoft.SqlServer.Smo.dll"
Remember that objects, from procedures to tables to functions, aren't always structured the same way, so we should use caution when looping through multiple objects; from a maintenance standpoint, it can make as much sense to handle each object as its own; for instance, with views we may care little about documentation and with procedures we may want to check if the name has INSERT, UPDATE, etc. in it. Likewise, we don't have to drop the objects - an email alert may suffice in our environment, so we may be able to skip the scripting and dropping. The key is that environment maintenance is simple with PowerShell and we can use a library, like SMO, to identify issues as they are created and handle them accordingly, and we can see an example of this application in the below code, where we handle our procedures, views and tables separately, remove the time comparison, and simply look for the procedures, tables and views that don't match our environment definition, emailing an alert when we find them:
Function MaintainEnvironment_EmailAlert_ProceduresViewsTables ($server, $smolibrary) { Add-Type -Path $smolibrary $evme_dev_email_list = "John Doe, Jane Doe " $evme_subject = "WARNING: Object Will Be Deleted" $serv = New-Object Microsoft.SqlServer.Management.SMO.Server($server) foreach ($db in $serv.Databases | Where-Object {$_.IsSystemObject -eq $false}) { $d = $db.Name foreach ($p in $db.StoredProcedures | Where-Object {$_.IsSystemObject -eq $false}) { $pName = $p.Name $pTxt = $p.TextHeader.Replace("'","") if (($pName.Substring(0,4) -ne "stp_") -or ($pTxt -notlike "*ETL Details: https://*")) { $evme_body = "Where is the ETL documentation for $pName? Please correct immediately." Send-MailMessage -From "[email protected]" -To $evme_dev_email_list -SmtpServer "smtp.OurEnvironment.com" -Subject $evme_subject -Body $evme_body -Priority High } } foreach ($v in $db.Views | Where-Object {$_.IsSystemObject -eq $false}) { $vName = $v.Name $vTxt = $v.TextHeader.Replace("'","") if (($vName.Substring(0,4) -ne "view_") -or ($vTxt -notlike "*Fact table for Cube *")) { $evme_body = "What cube does $vName relate to? Correct this immediately." Send-MailMessage -From "[email protected]" -To $evme_dev_email_list -SmtpServer "smtp.OurEnvironment.com" -Subject $evme_subject -Body $evme_body -Priority High } } foreach ($t in $db.Tables | Where-Object {$_.IsSystemObject -eq $false}) { $tName = $t.Name if (if (($tName -notlike "*dtb*") -and ($tName -notlike "*ttb*")) { $evme_body = "Tables in our environment must have either dtb or ttb in them. $tName does not, please correct immediately." Send-MailMessage -From "[email protected]" -To $evme_dev_email_list -SmtpServer "smtp.OurEnvironment.com" -Subject $evme_subject -Body $evme_body -Priority High } } } } MaintainEnvironment_EmailAlert_ProceduresViewsTables -server "OurEnvironmentServer\OurEnvironmentInstance" -smolibrary "C:\Program Files (x86)\Microsoft SQL Server\100\SDK\Assemblies\Microsoft.SqlServer.Smo.dll"
This shows the flexibility of how we can maintain our environment - we have our rules, now we just map them in PowerShell and point it to the applicable servers.
Conclusion
This approach saves time as developers move on - we have an environment where our objects follow our rules, and as DBAs we know that we can make certain assumptions because of these objects following our rules. Take, for instance, my article on source control - if we demarcate objects like stored procedures and views, we save a ton of energy when automating querying code. We lose tons of time and energy if we don't have a system like this, for instance a table called Currencies, a procedure called Currencies (or a derivative of currencies), and a view called Currencies (or, again a derivative of such). How could we automate such when we'd have a name conflict?
Next Steps
- First, what objects does your environment use?
- Second, how do you want the objects in your environment to be maintained as far as object names, documentation/details on objects, etc.?
- Third, use some of the above code to test in an environment and change in accordance with what object rules you want.
About the author
This author pledges the content of this article is based on professional experience and not AI generated.
View all my tips
Article Last Updated: 2014-10-07