Dec 162008
 

There are a few examples of DDL triggers available on the web, which use XML to retrieve the EventData() of the trigger.  EventData() retrieves an XML object containing properties about the event.  For example, Books Online describes ALTER TABLE event as:

<EVENT_INSTANCE>
    <EventType>type</EventType>
    <PostTime>date-time</PostTime>
    <SPID>spid</SPID>
    <ServerName>name</ServerName>
    <LoginName>name</LoginName>
    <UserName>name</UserName>
    <DatabaseName>name</DatabaseName>
    <SchemaName>name</SchemaName>
    <ObjectName>name</ObjectName>
    <ObjectType>type</ObjectType>
    <TSQLCommand>command</TSQLCommand>
</EVENT_INSTANCE>

By using XML query methods, we can extract data out of this XML structure.  For example:

DECLARE @eventdata xml
SELECT @eventdata = EventData()
SELECT @eventdata.value('(/EVENT_INSTANCE/EventType)[1]'
    , 'nvarchar(100)')

This works well in most cases, but there is one little gotcha that occasionally springs up.  To use the XML methods, you need to have a specific set of SET options applied to your session.  If you have a DDL trigger that uses XML methods to parse out the EventData, and you have incompatible SET options set in your session, your trigger will generate an error, and rollback the entire transaction.

Now, although you can change set options inside a trigger, and they will automatically roll back to their previous values once the trigger ends, you may not be aware that you need to do this, and once you deploy the trigger into production, you start getting errors from some clients.  The easiest way around this is to not use the XML methods in your trigger, and just retrieve EventData() and store it in an XML column.  You can then use a separate process which has the correct session options set to process the XML in greater detail.

The alternative option is to ensure you set the set options correctly in your trigger.  The correct settings to use are:

SET CONCAT_NULL_YIELDS_NULL ON
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET NUMERIC_ROUNDABORT OFF
SET ANSI_WARNINGS ON

If you change any of these and attempt to run an XML method, you’ll get an error message telling you which option needs changing:

DECLARE @xml xml
SET @xml = '<Root><Product ID="1" Name="Widget, 25cm">
</Product></Root>'
SELECT @xml.query('/Root/Product')

Msg 1934, Level 16, State 1, Line 4
SELECT failed because the following SET options have incorrect settings: ‘ANSI_NULLS, QUOTED_IDENTIFIER, CONCAT_NULL_YIELDS_NULL, ANSI_WARNINGS, ANSI_PADDING’. Verify that SET options are correct for use with indexed views and/or indexes on computed columns and/or
query notifications and/or xml data type methods.

Applying a DDL trigger into production without first checking that the session values are compatible could be disastrous!

Special thanks to Jon Kehayias for technical reviewing of this post.

Dec 152008
 

I experienced an issue recently where a cluster could not start SQL Server as I had modified the startup parameters and mistakenly entered one incorrectly.  I didn’t notice until after I’d stopped SQL Server, and was unable to restart it.  I changed the value again in SQL Server Configuration Manager, but discovered that every time I attempted to bring SQL Server back up using Cluster Administrator, the incorrect value was back in place.

The reason for this is that the cluster controls the startup parameters, and will replicate the value through all nodes of the cluster.  The only way to change this parameter is to change it on the active SQL Server node.  However, if you don’t have an active node, because you’ve just broken it and it won’t start…

Now that you understand the Catch-22, what can you do to fix it?  There’s really only two solutions – tell the cluster to replicate a different value to all the nodes (impossible without PSS’s help, as far as I know), or get SQL Server running without the cluster.

There’s two ways of booting SQL Server in this instance.  The first is to start SQL Server (sqlsrvr.exe) from the command line, specifying the paths to the master database (remember, we can’t read from the registry due to the cluster updating it).  The second option is to modify the registry, and then start SQL Server via the Services control panel.  Note that modifying the registry is highly NOT recommended, but may be your only option. 

Once SQL Server has started locally, use Cluster Administrator to start SQL Server on the cluster.  As SQL Server is already running, the Cluster Administrator hooks right into it.  From there, you can use Configuration Manager to fix the startup parameter, have it replicate through the cluster, and restart SQL Server.

Phew.  The moral of the story is to double check your parameters before shutting down SQL Server!

I’m happy to hear any other possible solutions to this problem that I might have missed.

Dec 142008
 

I haven’t posted in a while, due to a combination of work that hasn’t been very blog-worthy and family life.  However, I did attempt the SQL Server 2008 Beta exam 71-451: Designing Database Solutions and Data Access Using Microsoft SQL Server 2008 in late September.  It wasn’t a bad exam, and afterwards I received an invitation to participate in a workshop in Redmond in October to help finalise the exam.

It was my first trip to the US, and although I spent more time in the air than I did on the ground, it was a fantastic four days.  I was hoping to meet Jonathan Kehayias over there,  but he wasn’t able to make it.  Instead, I met a whole group of SQL Server guys, mostly from Europe, and they showed me around Seattle.  Seattle reminded me a lot of Melbourne, but with wonderful shades of autumn red, orange and yellow that Melbourne just doesn’t have.

I received the results of the exam this week, and the news was quite pleasant!

Sep 252008
 

I received a phone call from a colleague last night who was trying to remember the process for fixing TempDB if the disk goes missing.  The server had crashed, and the disk that TempDB was assigned to was not available, and so SQL Server could not start.  It’s not the most common problem so it took a few minutes to remember how to do it.

Restart SQL Server with the command line parameter -T3608, and then run alter database commands to move the location of TempDB:

ALTER DATABASE TempDB MODIFY FILE  (name = tempdev, 
    filename = 'E:\Data\tempdb.mdf')
GO

ALTER DATABASE TempDB MODIFY FILE (name = templog, 
    filename = 'F:\Logs\templog.ldf')
GO

Restart SQL Server without -T3608, and TempDB should be created in the new location. 

The alternative is to not reconfigure SQL Server at all, but rename an existing disk to the missing drive letter, just to get SQL Server started.

Sep 222008
 

A question that seems to come up often is how to attach a data file (.MDF) without a transaction log?  If a database has been detached from SQL Server cleanly, then there should be no information inside the transaction log of any use.  If the database was not detached cleanly (for example, if the machine crashed, and SQL Server has not been restarted, or if a copy of the file is made when SQL Server is still running) then the database may be in an inconsistent state, requiring the information currently contained inside the transaction log file.

Assuming that the database was shut down cleanly, the transaction log file can be recreated.  There are two methods: sp_attach_single_file_db and CREATE DATABASE.

The sp_attach_single_file_db is a shorthand way of calling CREATE DATABASE FOR ATTACH.  You can see exactly what sp_attach_single_file_db does by running “sp_helptext sp_attach_single_file_db” in Management Studio.  The main restriction for FOR ATTACH when used in this fashion is that only one transaction log can be created.  The new transaction log will be created in a default location, or if the database is read only, in the original location specified in the .MDF file.  The basic syntax is:

EXEC sp_attach_single_file_db @dbname='MyDatabase', 
    @physname='E:\Database\MyDatabase.mdf'
 

If you need two transaction log files recreated, you need to use CREATE DATABASE FOR ATTACH_REBUILD_LOG.

CREATE DATABASE MyDatabase ON 
    (FILENAME = 'E:\Database\MyDatabase.mdf') 
FOR ATTACH_REBUILD_LOG

If possible, I recommend taking backups and then restoring the backup, rather than try to attach a .MDF file.  It’s a safer and more reliable method of moving a database from server to server.

Jul 162008
 

I looked up Books Online for information about @@version yesterday, and was pleasantly surprised to discover that @@version simply calls a stored procedure named xp_msver, which returns all the different parts of @@version, but in a result set.  It doesn’t really give you much more information than what @@version provides, but something might come in handy, particularly if you wanted to pull out a single part of the @@version string programmatically.

Values include: Product Name, ProductVersion,  Language, Platform, Comments, CompanyName, FileDescription, FileVersion, InternalName, LegalCopyright, LegalTrademarks, OriginalFilename, PrivateBuild, SpecialBuild, WindowsVersion, ProcessorCount, ProcessorActiveMask, ProcessorType, PhysicalMemory, Product ID

Jul 012008
 

I was posed a question the other day – “Can query plan guides help when an index is explicitly chosen via a hint in the query?”.  The short answer is yes, but let’s have a look at the process of creating a plan guide.

First, we need to create a situation where there’s two indexes that could be used.  I’ve chosen to use the Production.Product table in the AdventureWorks database, and created an index  on ProductID and Name:

CREATE NONCLUSTERED INDEX ncix_ProductID_Name ON Production.Product (ProductID, Name)

 

If we then execute the following query, the new index will be used:

SELECT ProductID, Name
FROM Production.Product
WHERE ProductID BETWEEN 300 AND 320

 

image image

However, if we use an index hint, we can force the query to use the clustered index:

SELECT ProductID, Name
FROM Production.Product WITH (INDEX = PK_Product_ProductID)
WHERE ProductID BETWEEN 300 AND 320

 

image

Now, let’s create a query plan that will always use the non-clustered index, regardless of whether the index hint says to use the clustered index or not.  First, we need to create a Profiler trace to capture the XML Show Plan event, and we run the query, forcing the non-clustered index.  This results in an XML plan of, which we then create a Plan Guide with:

EXEC sp_create_plan_guide
    @name = N'TestPlanGuide1',
    @stmt = N'SELECT ProductID, Name
FROM Production.Product WITH (INDEX = PK_Product_ProductID)
WHERE ProductID BETWEEN 300 AND 320',
    @type = N'SQL',
    @module_or_batch = NULL,
    @params = NULL,
    @hints = N'OPTION(USE PLAN N''<ShowPlanXML ...</ShowPlanXML>'')'

 

This doesn’t quite fit on the screen nicely (and temporarily removed as it doesn’t play nice with IE6), but you get the idea.  Now, we can run the query above, providing a WITH (INDEX = PK_Product_ProductID) hint, and we can see that the hint has been ignored in favour of the plan guide.

SELECT ProductID, Name
FROM Production.Product WITH (INDEX = PK_Product_ProductID)
WHERE ProductID BETWEEN 300 AND 320

 

image

Finally, drop the plan guide using this command:

sp_control_plan_guide N'DROP', N'TestPlanGuide1'

 

So, even if you have queries that you can’t modify that have index hints, you can still add in a plan guide to force the plan you require.

Jun 262008
 

Cascading deletes are one of those features that people either love or hate.  The haters prefer to delete all child rows first, and then delete the parent, while the lovers like the idea of deleting from one table and having every other table in the database empty out.  I have to admit that I fall into both camps – I don’t use it too often, and most of the time I’ll opt for deleting the children first, even with the cascading delete turned on.

However, performance can suffer when you have a cascading delete, and as it’s more hidden than the two step Delete-Children-First method, it can be harder to realise how an index can increase performance.

Imagine that you have three tables: Employee, Equipment, and EquipmentLoans, where an employee can borrow equipment:

CREATE TABLE Employee (
      EmployeeID int IDENTITY NOT NULL PRIMARY KEY
    , FirstName varchar(30)
    , LastName varchar(30)
)

CREATE TABLE Equipment (
      EquipmentID int IDENTITY NOT NULL PRIMARY KEY
    , Description varchar(30)
)

CREATE TABLE EquipmentLoans (
      EquipmentID int not null
    , EmployeeID int not null
    , DateDue datetime not null
    , CONSTRAINT fk_EquipLoans_EquimentID FOREIGN KEY (EquipmentID) 
            REFERENCES Equipment(EquipmentID) ON DELETE CASCADE
    , CONSTRAINT fk_EquipLoans_EmployeeID FOREIGN KEY (EmployeeID) 
            REFERENCES Employee(EmployeeID) ON DELETE CASCADE
    , PRIMARY KEY (EquipmentID, EmployeeID)
)

INSERT INTO Equipment (Description) VALUES ('Laptop');
INSERT INTO Equipment (Description) VALUES ('Projector');
INSERT INTO Equipment (Description) VALUES ('Laser Pointer');

INSERT INTO Employee (FirstName, LastName) VALUES ('Scott', 'Wood');
INSERT INTO Employee (FirstName, LastName) VALUES ('John', 'Otto');
INSERT INTO Employee (FirstName, LastName) VALUES ('Bart', 'Johnson');

INSERT INTO EquipmentLoans VALUES (1,1, '2008-08-14')
INSERT INTO EquipmentLoans VALUES (3,1, '2008-08-14')
INSERT INTO EquipmentLoans VALUES (2,2, '2008-07-21')

Now we have a clustered index on Employee (EmployeeID), one on Equipment(EquipmentID), and one on EquipmentLoans(EquipmentID, EmployeeID).  Because we’ve added cascading deletes on the EquipmentLoans table for both Employees and Equipment, we can automatically delete rows from EquipmentLoans when either the Employee or Equipment is deleted.  Let’s have a look at how this works.  If we run this command:

DELETE FROM Equipment WHERE EquipmentID = 3

Then the clustered index of EquipmentLoans will be looked up to find any Equipment with an ID of 3 and delete that row.  Next, it will look up the clustered index of Equipment and delete the row with an EquipmentID of 3.  This is very fast, as it uses index lookups.

However, what if we delete an employee?

DELETE FROM Employee WHERE EmployeeID = 2

There is only the clustered index on EquipmentLoans, so to find any rows with EmployeeID = 2, the clustered index must be scanned.  If there are a lot of rows, this could take a while.  After this is done, the clustered index for Employees will be looked up, and the Employee with ID 2 will be deleted. 

If there was no cascading delete, you would still need to perform two commands:

DELETE FROM EquipmentLoans WHERE EmployeeID = 2
DELETE FROM Employee HERE EmployeeID = 2

However, it’s easy to see which of these two commands is performing badly (i.e. the first one), and a nonclustered index on EquipmentLoans(EmployeeID) is an obvious fix.  Once this index is added, it can be used to find all EquipmentLoans where EmployeeID = 2 and delete them – no more table scan.  This applies whether it’s in the cascading delete or not.

Note that if you look at the execution plans for these queries, you will only see table scans.  This is because these demo tables are all small enough to fit into a single 8 KB page.  If there were thousands of rows, the indexes would come into play.

Jun 252008
 

I need to import Excel spreadsheets into SQL Server 2005 every few months, and I always end up banging my head on the same problem.  At least I know what to look for now!

Using the Import Data Wizard in Management Studio, is my preferred method of getting data from spreadsheet format into the database, and from there I can massage the data to load it into the necessary tables. The issue is that when you import a column that contains data that could be read as a number or text, for example a classification column containing values like 150, 160, K20, 146, 12G, Excel’s ISAM driver will take a guess as to what type of data will be going into this column, usually based on the first 8 rows.  If it determines that it’s text, you’ll get an nvarchar(255), and if it’s a number you’ll get a float.  However, if it decides that a nvarchar is needed, then all numbers will be imported as null.  Of course, it’s the same if a float is used.  If all 8 rows have a value of NULL, then the entire column will always be assumed to be NULL.

There’s a few ways around this.  The first is to separate your data so that all your “float” values are in one spreadsheet, and all your “nvarchars” are in another.  This isn’t a very attractive idea as it essentially doubles the work.

Another option is to modify the source document to ensure that text data is used for every cell, but just formatting the cells as text isn’t sufficient.  You have to reapply the data for each cell individually.  Needless to say, I didn’t go down this route, but it’s detailed in a link below.

The solution was to put IMEX=1 into the connection string.  This puts the Excel ISAM driver into “Import” mode, and all values come in correctly.  The main drawback to this method is that you can’t modify the connection string used in the Management Studio Import Data Wizard, and so you have to save it as an SSIS package and then make the change. “IMEX=1″ comes just after HDR, and in the same quotes:  “Excel 8.0;HDR=NO;IMEX=1;”. It’s important to get this right or you won’t be able to connect.  Note that even with IMEX=1, you still want to have data that isn’t just numeric in your first 8 rows. More information is available at http://support.microsoft.com/kb/194124/EN-US/.

A good overview on importing data from Excel to SQL Server with a variety of techniques is available at http://support.microsoft.com/kb/321686.

Jun 182008
 

I installed SQL Server 2008 RC0 over the weekend.  I was expecting a fairly smooth installation, but it ended up taking me most of an evening.  First I had to upgrade to Windows Server 2003 R2 SP2 (and had a blue screen occur while halfway through the install!), but that is hardly SQL Server’s fault.  I opted to install everything I could, and received some strange error messages (since forwarded onto Microsoft).  However, the summary at the end indicated that these problems are only related to full text search, and the rest of SQL Server appears to be functioning normally.  The installation application was brilliant, and the debug logging exceptional.

Now to get in and play with it!