Using XACT_STATE in a CATCH block

The XACT_STATE function can be used in a CATCH block to decide whether to commit or roll back a transaction.

In the example below, the UPDATE statement raises an error and control is passed to the CATCH block. In the CATCH block, XACT_STATE is used to check that the transaction can be rolled back. In this case, the INSERT statement before the failed UPDATE statement is rolled back and CountyTown remains an empty table.

/* If XACT_ABORT is ON, an error will cause the entire
transaction to be terminated and rolled back. It is
recommended to set XACT_ABORT ON so that command timeouts
won't leave the transaction open. For the purposes of this
post XACT_ABORT is set to OFF to show how XACT_STATE can
be used to commit part of a transaction. */

SET XACT_ABORT OFF

IF OBJECT_ID('CountyTown','U') IS NOT NULL
	DROP TABLE CountyTown;
GO

CREATE TABLE CountyTown
(
	[County] NVARCHAR(10)
	,[Town] NVARCHAR(10)
);
GO

BEGIN TRY
	BEGIN TRANSACTION;
	
	/* succeeds */
	INSERT INTO CountyTown VALUES
	(N'Tipperary',N'Clonmel')
	,(N'Clare',N'Ennis');		
	
	/* fails - error occurs */
	UPDATE CountyTown
	SET [County] = N'This string is too long';	
	
	/* does not execute as control is
	passed to CATCH block */
	INSERT INTO CountyTown VALUES
	(N'Kerry',N'Tralee');
	
	/* would commit the transaction if 
	an error did not occur */
	COMMIT TRANSACTION;
END TRY
BEGIN CATCH
	/* rolls back the transaction
	if this is possible (i.e. if XACT_STATE is
	not equal to 0) */
	IF XACT_STATE()  0
	BEGIN
		PRINT N'Error encountered'
		PRINT N'Error Msg: '+ERROR_MESSAGE()+N' Line: '
                      +CAST(ERROR_LINE() AS NVARCHAR(10))
		PRINT N'Rolling back transaction'
		ROLLBACK TRANSACTION;
	END
END CATCH
GO

In the following example, XACT_STATE is used to commit the statements that executed before the error occurred; that is, the first INSERT statement is committed and the table CountyTown will contain 2 rows:

County Town
Tipperary Clonmel
Clare Ennis
/* If XACT_ABORT is ON, an error will cause the entire
transaction to be terminated and rolled back. It is
recommended to set XACT_ABORT ON so that command timeouts
won't leave the transaction open. For the purposes of this
post XACT_ABORT is set to OFF to show how XACT_STATE can
be used to commit part of a transaction. */

SET XACT_ABORT OFF

IF OBJECT_ID('CountyTown','U') IS NOT NULL
	DROP TABLE CountyTown;
GO

CREATE TABLE CountyTown
(
	[County] NVARCHAR(10)
	,[Town] NVARCHAR(10)
);
GO

BEGIN TRY
	BEGIN TRANSACTION;
	
	/* succeeds */
	INSERT INTO CountyTown VALUES
	(N'Tipperary',N'Clonmel')
	,(N'Clare',N'Ennis');		
	
	/* fails - error occurs */
	UPDATE CountyTown
	SET [County] = N'This string is too long';	
	
	/* does not execute as control is
	passed to CATCH block */
	INSERT INTO CountyTown VALUES
	(N'Kerry',N'Tralee');
	
	/* would commit the transaction if 
	an error did not occur */
	COMMIT TRANSACTION;
END TRY
BEGIN CATCH
	/* rolls back transaction
	if transaction is uncommittable */
	IF XACT_STATE() = -1
	BEGIN
		PRINT N'Error encountered'
		PRINT N'Error Msg: '+ERROR_MESSAGE()+N' Line: '
                      +CAST(ERROR_LINE() AS NVARCHAR(10))
		PRINT N'Rolling back transaction'
		ROLLBACK TRANSACTION;
	END
		
	/* commits the transaction
	if transaction is committable */
	IF XACT_STATE() = 1
	BEGIN
		PRINT N'Error encountered'
		PRINT N'Error Msg: '+ERROR_MESSAGE()+N' Line: '
                      +CAST(ERROR_LINE() AS NVARCHAR(10))
		PRINT N'Committing transaction'
		COMMIT TRANSACTION;
	END
END CATCH
GO

Excel User DSN Error

In a previous post, I described how to set up an ODBC data source on your machine for use in an Excel workbook (Import Data into Excel using a SQL Query). Now if you send this workbook to your colleagues and they try to refresh the data, they are likely to get the following error message:

[Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified.

This is because your colleagues also need to set up the data source on their machine (step 1 of Import Data into Excel using a SQL Query). They will also need to edit the connection string in the Excel workbook. To do this, open the workbook and go to “Data”, and then “Connections”.
Excel Data Tab
Select the connection in the dialog which pops up and then click on “Properties”.
Excel Connections Dialog
In the “Properties” dialog, click on the “Definition Tab”. The connection string, which specifies information about the data source and how to connect to it, can now be seen. Your colleague will need to edit the user ID (UID) and workstation ID (WSID). They will also need to edit the DSN and Description fields if they entered a different name and description when connecting to the data source. Your colleague can find their WSID, or computer name, under System on the Control Panel.
Excel Connection Properties

Import Data into Excel using a SQL Query

(Excel 2010, Windows 7)
Follow the steps below if you want to import data into Excel from a SQL Server database using a SQL query.
Step 1: Create the data source on your machine.
Type “ODBC” into the Start Menu search and then click on “Data Sources (ODBC)”.
ODBC
The ODBC Data Source Administrator should open.
ODBC Admin
Click on “Add” and then enter a Name and Description for the SQL Server which you wish to connect to.
ODBC Admin
When you have successfully created your data source, you should see it listed as a user DSN (Data Source Name).
ODBC Admin
Step 2: Connect to the data source from within Excel.
Open Excel. Click on the “Data” tab, then “From Other Sources” and finally “From Microsoft Query”.
Excel Data Tab
Select the data source you created in step 1, un-tick “Use the Query Wizard to create/edit queries” and then click OK.
Excel Source
Close the “Add Tables” dialog which pops up, and then click on the “SQL” button in the “Microsoft Query” dialog.
Excel Source
Paste your SQL into the “SQL” dialog which pops up.
SQL Query
Click OK and then click OK again when asked “SQL Query can’t be represented graphically. Continue anyway?” Finally, click on the “Return Data” button to return the data to an Excel sheet.
Return Data

How to write a query that returns a list of numbers in T-SQL

Suppose you want to write a query that returns the integers from 0 to 4. As shown on stackoverflow a recursive common table expression (CTE) can be used. A recursive CTE includes references to itself:

WITH Numbers
AS
(
-- anchor member
SELECT 0 AS Number
-- set operator
UNION ALL
-- recursive member
SELECT Number + 1 FROM Numbers WHERE Number < 4
)
SELECT Number FROM Numbers;

which outputs

0
1
2
3
4

Here is how the list is created:
Step 1: The anchor member query runs and returns 0.
Step 2: Since 0 is less than 4, the recursive member query adds 1 to 0 and returns 1.
Step 3: Since 1 is less than 4, the recursive member query adds 1 to 1 and returns 2.
Step 4: Since 2 is less than 4, the recursive member query adds 1 to 2 and returns 3.
Step 5: Since 3 is less than 4, the recursive member query adds 1 to 3 and returns 4.
Step 6: Since 4 is not less than 4, the recursive member query returns NULL and the loop terminates.

Using a variable with the TOP clause

Suppose you want to declare a variable and use that variable with the TOP clause. The following SQL will return an error message:

DECLARE @numberRows INT = 5;

SELECT TOP @numberRows * 
FROM [mydb].[myschema].[mytable];

Msg 102, Level 15, State 1, Line 3
Incorrect syntax near ‘@NumberRows’.

The solution is to use brackets around the variable in the TOP clause:

DECLARE @numberRows INT = 5;

SELECT TOP (@numberRows) *
FROM [mydb].[myschema].[mytable];

How to remove all non-alphanumeric characters from a string

The following SQL illustrates a method for removing all non-alphanumeric characters from a string:

-- the string that contains unwanted spaces and an exclamation mark
DECLARE @testString VARCHAR(50) = 'Jack and Jill!';
-- keep alphanumeric characters
-- the ^ wildcard means match any single character not within
-- the set specified inside the square brackets
DECLARE @charactersToKeep VARCHAR(50) = '%[^A-Za-z0-9]%';

-- while unwanted characters are still present in @testString
-- remove the unwanted characters one by one
WHILE PatIndex(@charactersToKeep, @testString) > 0
BEGIN
	-- display the index of the unwanted character
	PRINT PatIndex(@charactersToKeep, @testString)
	
	-- use the stuff function to replace the unwanted character with an empty string
	SET @testString = Stuff(@testString, PatIndex(@charactersToKeep, @testString), 1, '')
	
	-- display the new string with the unwanted character removed
	PRINT @testString
END

How to concatenate the text in several rows into one string using the FOR XML clause

The following examples use the FOR XML clause with the PATH mode to generate XML from a SELECT query.

The final example shows how to concatenate the text in several rows into one delimited string.

First, create a table variable.

DECLARE @my_table TABLE (
    Question VARCHAR(500) NOT NULL
    ,Answer VARCHAR(500) NOT NULL);
 
INSERT INTO @my_table VALUES
('Clean, Jerk, and Snatch are terms used in which activity?','Weightlifting')
,('How many men have walked on the moon: 4, 8, or 12?','12')
,('What does a sommelier do?','Taste wine');

The table’s contents can be converted to XML with the column names as tags using

SELECT Question, Answer
FROM @my_table
FOR XML PATH;

which outputs

<row><Question>Clean, Jerk, and Snatch are terms used in which activity?</Question><Answer>Weightlifting</Answer></row><row><Question>How many men have walked on the moon: 4, 8, or 12?</Question><Answer>12</Answer></row><row><Question>What does a sommelier do?</Question><Answer>Taste wine</Answer></row>

To get rid of the row tags, we use

SELECT Question, Answer
FROM @my_table
FOR XML PATH('');

which outputs

<Question>Clean, Jerk, and Snatch are terms used in which activity?</Question><Answer>Weightlifting</Answer><Question>How many men have walked on the moon: 4, 8, or 12?</Question><Answer>12</Answer><Question>What does a sommelier do?</Question><Answer>Taste wine</Answer>

Alternatively, we can replace the row tag with a Quiz tag by using

SELECT Question, Answer
FROM @my_table
FOR XML PATH('Quiz');

which outputs

<Quiz><Question>Clean, Jerk, and Snatch are terms used in which activity?</Question><Answer>Weightlifting</Answer></Quiz><Quiz><Question>How many men have walked on the moon: 4, 8, or 12?</Question><Answer>12</Answer></Quiz><Quiz><Question>What does a sommelier do?</Question><Answer>Taste wine</Answer></Quiz>

We can just select one column, say the Answer column, using

SELECT Answer
FROM @my_table
FOR XML PATH('');

which outputs

<Answer>Weightlifting</Answer><Answer>12</Answer><Answer>Taste wine</Answer>

We can get rid of the tags and use commas instead using

SELECT Answer+','
FROM @my_table
FOR XML PATH('');

which outputs

Weightlifting,12,Taste wine,

We can get rid of the trailing comma by following the example on stackoverflow:

SELECT SUBSTRING(
            (SELECT ','+Answer
            FROM @my_table
            FOR XML PATH(''))
            ,2,1000);

which outputs

Weightlifting,12,Taste wine

Excel and SQL Server epoch dates and treatment of the year 1900

Excel stores a date and time as a number where the integer part represents the number of days since 1900-01-00 and the fractional part represents the time as a fraction of a 24 hour day e.g. 18/03/2014 21:09 as the number 41716.88125. Now the integer part is actually out by 1 day as Excel incorrectly assumes that the year 1900 is a leap year and so counts 1900-02-29 as a day.

So, if we calculate the number of days between the Epoch date and a test date we can see that 1900-02-29 is counted as a day:

Epoch Date Test Date Difference in Days
00/01/1900 00:00 28/02/1900 00:00 59
00/01/1900 00:00 01/03/1900 00:00 61

Now if we import the number representing a datetime in Excel into SQL Server and then in SQL Server convert it to a datetime, we can expect to be out by 2 days for dates after 1900-02-28: 1 day because SQL Server doesn’t count 1900-02-29 as a day plus another day because SQL Server uses 1900-01-01 as its Epoch date:

SELECT CONVERT(DATETIME, 0);
--returns 1900-01-01 00:00:00.000
SELECT DATEDIFF(DAY, CONVERT(DATETIME, 0), CONVERT(DATETIME, '1900-02-28'));
--returns 58
SELECT DATEDIFF(DAY, CONVERT(DATETIME, 0), CONVERT(DATETIME, '1900-03-01'));
--returns 59

T-SQL: Convert a 7 digit date to date type

Suppose you have a date in the format yyyyddd where yyyy is the year and ddd is the day in the year (1 to 365 or 366 in a leap year), and you want to convert it to date type. The sql below performs this conversion.

--http://www.c-sharpcorner.com/Forums/Thread/201996/cast-7-digit-string-format-date-into-date-in-sql.aspx
DECLARE @date INT = 2011211;
 
SELECT DATEADD(DAY, CONVERT(INT,SUBSTRING(CONVERT(CHAR(7),@date),5,3)) - 1, CONVERT(DATE,SUBSTRING(CONVERT(CHAR(7),@date),1,4) + '-01-01'));

The conversion essentially has three steps:

  1. Convert to string. Get the year part (first 4 characters). Add ‘-01-01’. Convert to date.
  2. Convert to string. Get the day number part (last 3 characters). Convert to int. Subtract 1 (to remove the 1st Jan).
  3. Add the number of days calculated in step 2 to the date calculated in step 1.

E.g. the query

SELECT
      @date AS SevenDigitDate
      , CONVERT(DATE,SUBSTRING(CONVERT(CHAR(7),@date),1,4) + '-01-01') AS Step1
      , CONVERT(INT,SUBSTRING(CONVERT(CHAR(7),@date),5,3)) - 1 AS Step2
      , DATEADD(DAY, CONVERT(INT,SUBSTRING(CONVERT(CHAR(7),@date),5,3)) - 1, CONVERT(DATE,SUBSTRING(CONVERT(CHAR(7),@date),1,4) + '-01-01')) AS Step3;

returns

SevenDigitDate Step1 Step2 Step3
2011211 2011-01-01 210 2011-07-30

T-SQL: Calculate age in years from date of birth

On stackoverflow I found the following query to calculate a person’s age in years from their date of birth:

DECLARE @date_of_birth date = '2003-01-21';
DECLARE @todays_date date = '2014-03-01';

SELECT
    (CONVERT(INT,CONVERT(VARCHAR(8),@todays_date,112)) - CONVERT(INT,CONVERT(VARCHAR(8),@date_of_birth,112)))/10000;

Breaking down this query, we can see how it works:

DECLARE @date_of_birth date = '2003-01-21';
DECLARE @todays_date date = '2014-03-01';
 
SELECT
    @date_of_birth
    , @todays_date
    , CONVERT(INT,CONVERT(VARCHAR(8),@date_of_birth,112))
    , CONVERT(INT,CONVERT(VARCHAR(8),@todays_date,112))
    , CONVERT(INT,CONVERT(VARCHAR(8),@todays_date,112)) - CONVERT(INT,CONVERT(VARCHAR(8),@date_of_birth,112))
    , (CONVERT(INT,CONVERT(VARCHAR(8),@todays_date,112)) - CONVERT(INT,CONVERT(VARCHAR(8),@date_of_birth,112)))/10000;

which returns:

2003-01-21 2014-03-01 20030121 20140301 110180 11

For those born on a Leap day, this query assumes their birthday is on the 1st March in non-Leap years:

DECLARE @dates_table TABLE (
    Date_of_Birth DATE NOT NULL
    ,Todays_Date DATE NOT NULL);
 
INSERT INTO @dates_table
VALUES ('2000-03-01','2013-03-01'),('2000-03-01','2013-11-07')
,('2000-12-01','2013-11-07'),('2000-11-06','2013-11-07')
,('2000-11-08','2013-11-07'),('2004-02-29','2012-01-28')
,('2004-02-29','2012-02-28'),('2004-02-29','2012-02-29')
,('2004-02-29','2012-03-01'),('2004-02-29','2013-02-28')
,('2004-02-29','2013-03-01'),('2004-02-29','2014-02-28')
,('2004-02-29','2014-03-01');
  
SELECT
    Date_of_Birth
    , Todays_Date
    , CONVERT(INT,CONVERT(VARCHAR(8),Date_of_Birth,112))
    , CONVERT(INT,CONVERT(VARCHAR(8),Todays_Date,112))
    , CONVERT(INT,CONVERT(VARCHAR(8),Todays_Date,112)) - CONVERT(INT,CONVERT(VARCHAR(8),Date_of_Birth,112))
    , (CONVERT(INT,CONVERT(VARCHAR(8),Todays_Date,112)) - CONVERT(INT,CONVERT(VARCHAR(8),Date_of_Birth,112)))/10000
FROM
    @dates_table;

which returns:

2000-03-01 2013-03-01 20000301 20130301 130000 13
2000-03-01 2013-11-07 20000301 20131107 130806 13
2000-12-01 2013-11-07 20001201 20131107 129906 12
2000-11-06 2013-11-07 20001106 20131107 130001 13
2000-11-08 2013-11-07 20001108 20131107 129999 12
2004-02-29 2012-01-28 20040229 20120128 79899 7
2004-02-29 2012-02-28 20040229 20120228 79999 7
2004-02-29 2012-02-29 20040229 20120229 80000 8
2004-02-29 2012-03-01 20040229 20120301 80072 8
2004-02-29 2013-02-28 20040229 20130228 89999 8
2004-02-29 2013-03-01 20040229 20130301 90072 9
2004-02-29 2014-02-28 20040229 20140228 99999 9
2004-02-29 2014-03-01 20040229 20140301 100072 10