0

Error Log Management

Thought I’d write a quick post about managing error logs within SQL Server. Good error log management is essential for investigating issues that occur in SQL Server. The last thing you want is a very large error log file that takes ages to open when you are trying to determine the cause of a problem.

I always change the maximum number of error logs to 30 (at a minimum) and then recycle the error log each night. This way I can keep a month’s worth of logs in easily manageable portions (this does depend on the system but one day’s worth of logs in each file is generally OK).

So to change the maximum number of error logs, go to Management > SQL Server Logs and right-click and select Configure. Change the maximum number of error logs (I changed it to 30) and then hit the script button which will give you the following:-

USE [master];
GO

EXEC xp_instance_regwrite 
	N'HKEY_LOCAL_MACHINE', 
	N'Software\Microsoft\MSSQLServer\MSSQLServer', 
	N'NumErrorLogs', 
	REG_DWORD, 
	30;
GO

Microsoft has provided the stored procedure sys.sp_cycle_errorlog so this can be setup to run nightly as a SQL Server Agent job. However I have found that the following error can occur on busy systems (running SQL Server 2005 I’m afraid):-

“Unable to cycle error log file from ‘…\ERRORLOG’ to …\ERRORLOG.1′ due to OS error ’32(The process cannot access the file because it is being used by another process.)”

When this happens the old log is archived but the new log is not initialised, meaning that no information will be written to the log until it is recycled manually (once you’re in the office). Not good, especially if an issue has occurred in that time.

So to try and combat this, I created the following stored procedure to try to recycle the error log a maximum of three times if an error occurs:-

CREATE PROCEDURE [dbo].[RecycleErrorLog]
AS

DECLARE @Tries TINYINT
SET @Tries = 1
WHILE @Tries <= 3

BEGIN

  BEGIN TRY
		EXEC master.sys.sp_cycle_errorlog
		BREAK
  END TRY

  BEGIN CATCH
		SELECT ERROR_NUMBER() AS ErrorNumber
		SET @Tries = @Tries + 1
		CONTINUE
  END CATCH;

END;

GO

I set the maximum number of re-tries to 3 as if the job fails three times in a row I would say that there is something more severe going on which would need to be investigated.

So I’m feeling pretty clever, the stored procedure is running fine until one day:-

“Unable to cycle error log file from ‘…\ERRORLOG’ to …\ERRORLOG.1′ due to OS error ’32(The process cannot access the file because it is being used by another process.)”

Ah, it’s no problem I tell myself. The error will be generated but the procedure will re-run and recycle the SQL Server Error Log. So…..I stroll into work, check the logs and…..they haven’t recycled. Not only that, nothing has been recorded since the attempt to cycle, I’m back to where I started!

I check the SQL Server Agent Job’s history and the job is telling me that it has run successfully. It seems that the error is not returned to the procedure, there’s a record in the SQL Server Error Log:-

“Attempting to cycle error log. This is an informational message only; no user action is required.”

I look back at the original error, there was an important part that I missed!

“…due to OS error ’32…”

It’s an OS error, it’s not going to be recorded in SQL. The only place the error is recorded is in the Application Event Log. So it looks like I’m going to have to run the stored procedure and then check the Application Event Log for errors. The best way I can think of doing this is using powershell, so here’s the script I wrote:-

#Set variable values to zero
$MaximumRetries = 0
$ErrorChecker = 0

#Open connection to SQL Server
$SQLServer = "MYSERVER"
$SQLDBName = "master"

$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = "Server=$SQLServer;Database=$SQLDBName;Integrated Security=True"
$SqlConnection.Open()

$SQLCmd = new-Object System.Data.SqlClient.SqlCommand("sp_cycle_errorlog", $SQLConnection) 
$SQLCmd.CommandType = [System.Data.CommandType]'StoredProcedure' 

#Run sp_cycle_errorlog
$SQLCmd.ExecuteReader()

#Check application log for errors
$datetime = (Get-Date).AddSeconds(-5)
$events = get-eventlog application -EntryType Error -After $datetime | where-Object {$_.source -like "MSSQL*"}	

do
{
	#If the application log has an error
	if ($events)
		{	
			#Set maximum retries to 3
			if ($MaximumRetries -lt 4)
				{	
					#Re-execute stored procedure
					$sqlCmd.ExecuteReader()		
					
					#Check application log
					$datetime = (Get-Date).AddSeconds(-5)
					$events = get-eventlog application -EntryType Error -After $datetime | where-Object {$_.source -like "MSSQL*"}	
			
					$MaximumRetries = $MaximumRetries + 1		
				}
	#If the application log has no errors, close connection			
	elseif (!$events)
		{
			$SqlConnection.Close()
			$ErrorChecker = 1
		}
		}	
}
while ($ErrorChecker = 0)

I saved this as a .ps1 file and then created a SQL Server Agent Job to run it. I still get alerts for the error but each time the script has done its job and re-run.

Has anyone else seen this behaviour? If so, I’d be interested in hearing how you dealt with it, my email is dbafromthecold@gmail.com

8

Database Design Guidelines For Developers

One of the developers that I work with asked me to write a “brief” (really brief) guide on database design. What I have written below is a summary in bullet point form of best practices when creating databases, tables, indexes, and then querying the data within. It is designed to be short and sweet so the reader can check each section quickly when creating a new database. References are provided so that the reader can use if they wish to explore a certain topic in more detail.

Let me know what you think! Is there anything glaringly obvious that I’ve missed? My email is dbafromthecold@gmail.com

Database Design Guidelines

Database Design

  • PRIMARY filegroup should be reserved for system objects
  • Secondary DATA filegroup should be set to default.
  • Minimum of two physical files mapped to each secondary filegroups
  • Data and Log files presized for a minimum of one year’s data inserts
  • Data and Log files should reside on separate drives
  • Denormalisation should only be considered in special circumstances. A properly normalised OLTP database will outperform a denormalised one
  • Recommended collation is Latin1_General_CI_AS

Table Design

  • Each table created within the database should have a Primary Key and a Clustered Index. These need not be the same column
  • GUID fields should not be used for clustered indexes even if used as table’s Primary Key
  • Composite keys should be avoided. Surrogate INT IDENTITY keys can be used as a replacement
  • Columns set to the smallest size possible – avoiding NVARCHAR(MAX), TEXT etc datatypes
  • Avoid Nullable columns. Nullable columns require extra space within the database to designate the fact that they are nullable
  • Use referential integrity – Foreign keys and unique constraints should be applied

MSDN CREATE TABLE Documentation – http://technet.microsoft.com/en-us/library/ms174979.aspx

Querying Data

  • Stored procedures should be used as much as possible, to allow query execution plans to be re-used
  • Do not use SELECT * – SELECT only the columns needed to keep the query’s memory usage as low as possible
  • As a standard, cursors should be avoided. Removal of RBAR (row-by-agonising-row) processing
  • SET NOCOUNT ON should be at the start of each SQL batch to reduce network traffic
  • Dynamic SQL should be executed using sp_executesql.
  • Do not repeatedly call functions within stored procedures, functions, batches and triggers.
  • Sargability (Search ARGument Able) – Confirm queries executed are able to seek on indexes in database
    • Avoid wildcard characters at the beginning of a word while searching using the LIKE keyword
    • Avoid searching using not equals operators (<> and NOT)
    • Avoid functions in WHERE clause on table columns
    • Avoid implicit conversions in WHERE clause (use CAST/CONVERT if necessary)
BAD:   SELECT ... WHERE Year(StartDate) = 2014
FIXED: SELECT ... WHERE StartDate &gt;= '01-01-2014' AND StartDate &lt; '01-01-2015'

BAD:   SELECT ... WHERE isNull(Surname,'Williams') = 'Williams'
FIXED: SELECT ... WHERE ((Surname = 'Williams') OR (Surname IS NULL))

BAD:   SELECT ... WHERE LEFT(FirstName,1) = 'P'
FIXED: SELECT ... WHERE FirstName LIKE 'P%'

BAD:   SELECT ... WHERE DateDiff(mm,OrderDate,GetDate()) &gt;= 30
FIXED: SELECT ... WHERE OrderDate &lt; DateAdd(mm,-30,GetDate())

Sargability References

Stack Overflow – What makes a SQL Statement sargable? http://stackoverflow.com/questions/799584/what-makes-a-sql-statement-sargable
Rob Farley – Sargable Functions http://blogs.msmvps.com/robfarley/2010/01/21/sargable-functions-in-sql-server/
Brent Ozar – Why %string% is slow http://www.brentozar.com/archive/2010/06/sargable-why-string-is-slow/

Indexes

  •  Naming convention – Prefix_TableName_ColumnName
  •  Prefix – PK for Primary Keys, IX for non-clustered indexes, UC for Unique Indexes
  • Make sure the option ONLINE = ON is in place
  • Correct nonclustered indexes created for incoming queries – avoid RID and KEY lookups on tables
  • No Indexes on columns like “Gender” as this would only have a maximum of three possible entries (Male, Female & NULL) making an index ineffective. Nonclustered indexes must be as selective as possible

 

0

SQL Server 2014 RTM

The new version of SQL Server will be here on Tuesday.

(Someone at Microsoft has a sense of humour in releasing the newest version of SQL Server on April Fool’s Day.)

At some point the usual argument about when a company should upgrade will be had. I’ve worked companies that have waited for the first service pack of a new version of SQL to come out and I’ve worked for companies that start implementing the new version as soon as it comes out.

I can see a lot of companies moving to SQL Server 2014 pretty quickly for one reason, the increase in the memory limit for Standard Edition from 64GB to 128GB. There are companies out there that have large databases running in Standard Edition, simply because they do not want to fork out for an Enterprise Edition licence.

This month will be quite interesting as there are always problems with a new version of software (see the KB article for SQL Server 2012 SP1 ). I’ll be watching the Connect site with interest.

3

Index Fragmentation

So you’ve setup a bunch of indexes in your database to tune the queries coming in. They’re all running much faster than before and you’re leaning back in your chair feeling pretty pleased with yourself. You’re done right?

Well…no. As data gets inserted into and deleted from tables within a database, the indexes on the tables become fragmented. Fragmentation is defined as:-

“Fragmentation exists when indexes have pages in which the logical ordering, based on the key value, does not match the physical ordering inside the data file.”

But does it matter that the logical ordering does not match the physical ordering? Microsoft published a white paper here which showed that with increased fragmentation, the time taken for each read from disk increased whilst the average size of each read decreased. I know it was performed using SQL Server 2000 but it’s still relevant. They summarise the results as:-

“The workload performance increase realized in the small-scale environment ranged from 60 percent at the low level of fragmentation to more than 460 percent at the highest level of fragmentation. The workload performance increased realized for the large-scale environment ranged from 13 percent at the low fragmentation level to 40 percent at the medium fragmentation level”

Note – This is the summary of the DSS workload test, complex queries against multiple levels of fragmentation

To summarise:-
Small-scale environment
After defragmenting the indexes an increase in performance of 60% was gained when there was a low level of fragmentation and there was a gain of 460% at a high level of fragmentation
Large-scale environment
After defragmenting the indexes an increase in performance of 13% was gained when there was a low level of fragmentation and there was a gain of 40% at a high level of fragmentation

Wow, removing the fragmentation in the indexes gave a performance increase between 13% and 460% (across the two environments).

The paper also discusses why the performance in the large-scale environment was less affected by fragmentation…basically the environment had a better IO subsystem…which means that throwing money at index fragmentation can appear to solve the problem but you are really just papering over the cracks.

But, what DBA doesn’t like improving the performance of a system without having to spend any money? “Look, I’ve improved the performance by X%!”. (cue smug face)

So a plan to reduce the fragmentation of the indexes needs to be implemented. But how do you approach this? Rebuild every index nightly? Will your system allow you to do that? What if you are running Standard Edition and you cannot rebuild indexes online?

The way I approach index maintenance is to customise for each database I manage. To do this, the indexes that require either rebuilds or reorganisation need identifying. Microsoft have provided the DMV sys.dm_db_index_physical_stats which will allow us to do so:-

SELECT
	[schemas].name AS [SchemaName],
	[objects].name AS [TableName], 
	[indexes].name AS [IndexName],
	[dm_db_index_physical_stats].index_depth,
	[dm_db_index_physical_stats].avg_fragmentation_in_percent,
	[dm_db_index_physical_stats].fragment_count,
	[dm_db_index_physical_stats].avg_fragment_size_in_pages,
	[dm_db_index_physical_stats].page_count,
	GETUTCDATE() AS [CaptureDate]
FROM 
	[sys].[dm_db_index_physical_stats](DB_ID(),NULL,NULL,NULL,'LIMITED' ) AS [dm_db_index_physical_stats]
INNER JOIN 
	[sys].[objects] AS [objects] ON ([dm_db_index_physical_stats].object_id = [objects].object_id)
INNER JOIN 
	[sys].[schemas] AS [schemas] ON ([objects].[schema_id] = [schemas].[schema_id])
INNER JOIN 
	[sys].[indexes] AS [indexes] ON ([dm_db_index_physical_stats].object_id = [indexes].object_id
								AND  [dm_db_index_physical_stats].index_id = [indexes].index_id )
WHERE 
	[dm_db_index_physical_stats].index_type_desc <> 'HEAP'
AND 
	[dm_db_index_physical_stats].avg_fragmentation_in_percent > 20;
GO

Note – this will pick up data on all indexes that are over 20% fragmented.

By adding GETUTCDATE() we can monitor and track the fragmentation over time, allowing us to identify which tables’ indexes are being fragmented the quickest. Nice and simple to setup, first create a table to hold the data:-

CREATE TABLE dbo.[IndexAnalysis]
		(AnalysisID					INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
		SchemaName					SYSNAME,
		TableName					SYSNAME,
		IndexName					SYSNAME,
		IndexDepth					INT,
		AvgFragmentationInPercent	FLOAT,
		FragmentCount				BIGINT,
		AvgFragmentSizeInPages		FLOAT,
		PageCount					BIGINT,
		CaptureDate					DATETIME);
GO

Then create a stored procedure to capture the data and insert it into the table:-

CREATE PROCEDURE dbo.[CaptureIndexFragmentation] AS

INSERT INTO dbo.[IndexAnalysis]
SELECT
	[schemas].name AS [SchemaName],
	[objects].name AS [TableName], 
	[indexes].name AS [IndexName],
	[dm_db_index_physical_stats].index_depth,
	[dm_db_index_physical_stats].avg_fragmentation_in_percent,
	[dm_db_index_physical_stats].fragment_count,
	[dm_db_index_physical_stats].avg_fragment_size_in_pages,
	[dm_db_index_physical_stats].page_count,
	GETUTCDATE() AS [CaptureDate]
FROM 
	[sys].[dm_db_index_physical_stats](DB_ID(),NULL,NULL,NULL,'LIMITED' ) AS [dm_db_index_physical_stats]
INNER JOIN 
	[sys].[objects] AS [objects] ON ([dm_db_index_physical_stats].object_id = [objects].object_id)
INNER JOIN 
	[sys].[schemas] AS [schemas] ON ([objects].[schema_id] = [schemas].[schema_id])
INNER JOIN 
	[sys].[indexes] AS [indexes] ON ([dm_db_index_physical_stats].object_id = [indexes].object_id
								AND  [dm_db_index_physical_stats].index_id = [indexes].index_id )
WHERE 
	[dm_db_index_physical_stats].index_type_desc <> 'HEAP'
AND 
	[dm_db_index_physical_stats].avg_fragmentation_in_percent > 20;
GO

This can then be run as a nightly job so that you can track the fragmentation of the indexes over time.

SELECT 
	SchemaName AS [Schema Name],
	TableName AS [Table Name],
	IndexName AS [Index Name],
	AvgFragmentationInPercent AS [Current % Avg Fragmentation],
	LAG(AvgFragmentationInPercent,1) OVER (PARTITION BY TableName, IndexName ORDER BY CaptureDate) AS [Previous % Avg Fragmentation],
	AvgFragmentationInPercent -
	LAG(AvgFragmentationInPercent,1) OVER (PARTITION BY TableName, IndexName ORDER BY CaptureDate) AS [Difference],
	CaptureDate AS [Capture Date]
FROM 
	dbo.IndexAnalysis

Note – This SELECT statement uses the LAG() function which is only available in SQL Server 2012 onwards.

Once you have the data you can then decide how to approach the maintenance. You could use something like:-

SELECT 		   
	'ALTER INDEX [' + IndexName + '] ON [' + SchemaName + '].[' + TableName +'] ' +
    (CASE	
     WHEN (avgfragmentationinpercent >= 20 AND avgfragmentationinpercent < 40) THEN 'REORGANIZE'
     WHEN avgfragmentationinpercent > = 40 THEN 'REBUILD' END) AS [SQL Statement]
FROM 
	dbo.[IndexAnalysis] 

This will generate the required ALTER INDEX statements, you could then use sp_executesql to perform the operation. Once you have the data, you can proceed with the best approach that suits your environment. One thing that I have found is that, with large indexes in a busy system, catching fragmentation early and performing frequent REORGANIZE operations instead of less frequent REBUILDs is preferable as REORGANIZE statements are less resource intensive.

0

Deadlock Notifications (who are your victims?)

In my previous post I talked about having your own database(s) and what information you could be collecting. One piece of information that you should be collecting is what deadlocks are occurring. There are a few of ways you can collect information on deadlocks, enabling trace flag 1222 and querying the error log, running profiler (urgh!), or querying the system_health event session (in SQL Server 2008 onwards).

In this post, I’d like to talk through setting up Event Notifications and Service Broker in order to be alerted when a deadlock occurs. I like this way of alerting for deadlocks as I think that you can get the most amount of information with minimal (if any) performance impact. I also administer instances of 2005 to 2012 and like to have one solution that fits all.

DISCLAIMER!! Please don’t do this for the first time in a live environment. Create a test database on a dev server so that you can play around (see the demo at the end).

Pre-requisite – DBMail setup and configured.

Right, here we go! Create a test db and then enable Service Broker for your database and set the TRUSTWORTHY property to ON. The TRUSTWORTHY property needs to be set to ON so that the stored procedure we are going to create later can run sp_send_dbmail.

USE [master];
GO

CREATE DATABASE [DeadlockDemo];
GO

ALTER DATABASE [DeadlockDemo] SET ENABLE_BROKER;
GO

ALTER DATABASE [DeadlockDemo] SET TRUSTWORTHY ON;
GO

Second thing to do is create a table to hold the information on the deadlocks

USE [DeadlockDemo];
GO

CREATE TABLE [dbo].[DeadLockLog] 
([DeadLockID]       INT          IDENTITY (1, 1) NOT NULL,
 [DeadLockDetected] DATETIME     NULL,
 [DatabaseName]     SYSNAME      NULL,
 [DeadLockXML]      XML          NULL,
PRIMARY KEY CLUSTERED ([DeadLockID] ASC));
GO

We then need to create a queue to hold messages and a service to receive messages from the Event Notification we will setup. We specify [http://schemas.microsoft.com/SQL/Notifications/PostEventNotification] as the message type and contract (basically the direction of messages, from Event Notification to Service).

CREATE QUEUE [DeadlockQueue];
GO

CREATE SERVICE [DeadlockService]
ON QUEUE [DeadlockQueue] ([http://schemas.microsoft.com/SQL/Notifications/PostEventNotification]);
GO

Then run the following statement to create the Event Notification:-

CREATE EVENT NOTIFICATION [CaptureDeadlocks]
ON SERVER
WITH FAN_IN
FOR DEADLOCK_GRAPH
TO SERVICE N'DeadlockService', N'current database';
GO

It’s pretty straight forward, server wide, for deadlocks, to the service we have setup within the database. OK, next bit. When a new message about a deadlock is received, we want to record information on that deadlock and then be emailed. The way to do this is to create a stored procedure (substitute your own DBMail profile and email address in):-

CREATE PROCEDURE [dbo].[DeadlockNotification]
AS

DECLARE @DeadlockInfo      XML
DECLARE @EventTime         DATETIME 
DECLARE @MsgBody           XML
DECLARE @MsgType           SYSNAME
DECLARE @DBName            SYSNAME
DECLARE @DBMailBody        SYSNAME
DECLARE @DBMailSubject     SYSNAME
DECLARE @DBMailRecipient   SYSNAME
DECLARE @DBMailProfile     SYSNAME

SET @DBMailProfile   = 'YOUR DBMAIL PROFILE'
SET @DBMailRecipient = 'YOUR EMAIL ADDRESS'

--Retrieve message from queue
WAITFOR 
(RECEIVE TOP(1) 
	@MsgBody	= CAST([message_body] AS XML),
	@MsgType 	= message_type_name
FROM [DeadlockQueue]), TIMEOUT 6000;

--Retrieve information from the XML
SELECT 
@DeadlockInfo = @MsgBody.query('(/EVENT_INSTANCE/TextData/deadlock-list)'),
@DBName       = DB_NAME(@MsgBody.value('(//*/process/@currentdb)[1]', 'varchar(50)')),
@EventTime    = @MsgBody.value('(/EVENT_INSTANCE/PostTime)[1]','datetime')

--Insert record into table if message conforms to correct type
IF @MsgType = 'http://schemas.microsoft.com/SQL/Notifications/EventNotification'
INSERT INTO [dbo].[DeadLockLog]
(DeadLockDetected, DatabaseName, DeadLockXML) 
VALUES 
(@EventTime, @DBName, @DeadlockInfo)

--Set values to be included in email
SET @DBMailSubject = 'A deadlock has occurred on ' + @@SERVERNAME
SET @DBMailBody    = 'A deadlock occurred in database ' + 
			@DBName + ' at ' + CONVERT(VARCHAR(20),@EventTime)

--Send email if message conforms to correct type
IF @MsgType = 'http://schemas.microsoft.com/SQL/Notifications/EventNotification'
EXEC [msdb].[dbo].sp_send_dbmail
@profile_name	= @DBMailProfile,
@Recipients	= @DBMailRecipient,
@subject	= @DBMailSubject,
@body		= @DBMailBody;
GO

The procedure retrieves the message from the queue, pulls (or “shreds”) information from the XML, inserts it into a table and sends an email. Cool eh?

Finally, execute the following statement so that the stored procedure will run when a new message arrives in the queue:-

ALTER QUEUE [dbo].[DeadlockQueue]
    WITH ACTIVATION
    ( STATUS = ON,
      PROCEDURE_NAME = [dbo].[DeadlockNotification],
      MAX_QUEUE_READERS = 1,
      EXECUTE AS SELF
    );
GO

TESTING!!!!!
You can then test the functionality (in the dev environment that you have been doing this in……right?) by creating two tables and running concurrent UPDATE and SELECT statement in open transactions. Here’s the setup:-

CREATE TABLE [TEST1]
(ID1 INT);
GO

CREATE TABLE [TEST2]
(ID2 INT);
GO

INSERT INTO [TEST1]
(ID1)
VALUES
(100)
GO 1000

INSERT INTO [TEST2] 
(ID2)
VALUES
(100)
GO 1000

All setup? Now run the following:-

USE [DeadlockDemo];
GO

BEGIN TRAN

UPDATE [Test1] SET ID1 = 1 WHERE ID1 = 100

Open up another session, and run:-

USE [DeadlockDemo];
GO

BEGIN TRAN

UPDATE [Test2] SET ID2 = 1 WHERE ID2 = 100

SELECT * FROM [Test1];

Go back to you first session, and run:-

SELECT * FROM [Test2];

One of the process will be chosen as the deadlock victim and you will receive an email advising that a deadlock occurred in your database. You can then query your table to get the XML, save it as a .XDL file and view the deadlock graph.

REFERENCES
http://technet.microsoft.com/en-us/library/ms178080.aspx
http://technet.microsoft.com/en-us/library/ms166029(v=sql.90).aspx
https://www.simple-talk.com/sql/database-administration/handling-deadlocks-in-sql-server/