DBCC CleanTable

Being day eight of the DBCC Command month at SteveStedman.com, today’s featured DBCC Command is DBCC CLEANTABLE.

Many times I have worked on a database that has evolved over 10 or more years of changes from different developers and DBAs. One of the signs I see in databases like this is the waste associated with obsolete columns.  For instance someone had an email address column as a varchar(1024) just to have plenty of space, then someone decided to do some normalization and they created a table just to hold email addresses, and the original table only had a id to represent the email address that was foreign keyed to the new table. In a good case the developer was wise enough to remove the original column, but not always. Assume the case where it wasn’t delete, so we do a check, determine that the old varchar(1024) column is not being used so we drop the column. After dropping the column we notice that the table didn’t get any smaller. Thats OK, maybe we will just use that space that we freed up from the column we dropped over time as the table grows, but NO. The space associated with that column is left around.

So to actually free up that space associated with the dropped columns so that it can be used there are 2 common options.

  1. DBCC CleanTable 
  2. Rebuild the clustered index

The purpose of this post is to demonstrate how DBCC CleanTable can be used to regain wasted space.

Description:

DBCC CleanTable is used for clean up waste left around by deleting variable length columns.

DBCC CLEANTABLE Syntax:

dbcc cleantable
(
    { 'database_name' | database_id | 0 }
    , { 'table_name' | table_id | 'view_name' | view_id }
    [ , batch_size ]
)
    [ WITH NO_INFOMSGS ]

The Third Argument is Batch size: This is the number of rows processed in one transaction. If not specified or if value is 0, it means all rows of table will be in 1 transaction. For larger tables it may make sense to process parts of the table in smaller batches.

Example:

First we will create a table to use for the following example

CREATE TABLE [LotsOfWaste] (
 id int, --would normally be an INT IDENTITY
 firstName VARCHAR (8000),
 MiddleName VARCHAR (8000),
 lastName VARCHAR (8000),
 SocialSecurityNumber VARCHAR (8000),
 FavoriteFood VARCHAR (8000),
 FavoriteColor VARCHAR (8000),
 archived bit default 0
 );

First we will fill up the table with 10, 000 rows using the GO statement to repeat our INSERT statement 10,000 times.

GO

set nocount on;
INSERT INTO [dbo].[LotsOfWaste]
 ([firstName]
 ,[MiddleName]
 ,[lastName]
 ,[SocialSecurityNumber]
 ,[FavoriteFood]
 ,[FavoriteColor]
 ,[archived])
VALUES (
  REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,1)
GO 10000

Now to check on the number of pages allocated for this table, and the percentage used. Below we see that there are 1426 page allocated, and of the memory associated with those 1426 pages, 86.3% of that memory is used to accomodate the 10,000 records.

SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

DBCC_CleanTable1

Now lets drop 4 columns from the table, and take a look at the pages allocated and the space used.


SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

ALTER TABLE [dbo].[LotsOfWaste] DROP COLUMN [FavoriteColor];
ALTER TABLE [dbo].[LotsOfWaste] DROP COLUMN [FavoriteFood];
ALTER TABLE [dbo].[LotsOfWaste] DROP COLUMN [SocialSecurityNumber];
ALTER TABLE [dbo].[LotsOfWaste] DROP COLUMN [MiddleName];

SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

The number of pages, percent used, and record count are exactly the same before and after deleting the columns.  This table is not using any less space now that those 4 large columns were deleted.
DBCC_CleanTable2

So maybe we can just insert more rows and that will take advantage of the space that was being used by the 4 deleted columns.  Perhaps…?


SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

GO
INSERT INTO [dbo].[LotsOfWaste]
 ([firstName]
 ,[lastName]
 ,[archived])
 VALUES (
  REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,1)
GO 100
SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

No, inserting more rows, even rows that are smaller than the original rows does not make use of the space left behind by the column removal.  In this case inserting 100 rows ended up allocating another 4 data pages.

DBCC_CleanTable3

So where do we go from here, how do we reclaim the space associated with the columns that were deleted? Now we will try DBCC CleanTable since that is the topic of this posting.  Lets see what happens.

SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

DBCC CleanTable(dbcc_corruption, LotsOfWaste);

SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

Here we see that the number of pages for the table hasn’t changed, and the record count hasn’t changed, but the average spaced used has changed from 86% to 30%.

DBCC_CleanTable4

There are still the same number of pages, but can we actually use those pages? Lets give it a try. Here we will insert 100 rows, and see if the page count grows as we saw it grow earlier…


SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

GO
INSERT INTO [dbo].[LotsOfWaste]
 ([firstName]
 ,[lastName]
 ,[archived])
 VALUES (
  REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,REPLICATE('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', cast(rand() * 10 as int))
 ,1)
GO 100
SELECT alloc_unit_type_desc, page_count,
 avg_page_space_used_in_percent, record_count
FROM sys.dm_db_index_physical_stats( DB_ID('dbcc_corruption'),
 OBJECT_ID(N'LotsOfWaste'),
 NULL,NULL,'Detailed')

Here we see that the page count has not increased from the before to the after check, the record count has increased due to the 100 rows inserted, and the average spaced used has changed slightly.

DBCC_CleanTable5

What we did here was insert those last 100 rows into space that was not usable prior to running DBCC CleanTable. By cleaning up a table this way we don’t decrease the overall number of pages, the size of the table, but we do allow for possibly many more rows to be inserted into the space that has been allocated.

Warning:

DBCC CleanTable for large tables may cause the transaction log to grow.

Notes:

DBCC CleanTable is doesn’t work on system tables or temporary tables.

For more information see TSQL Wiki DBCC cleantable.

DBCC Command month at SteveStedman.com is almost as much fun as looking at paint swatches at the home improvement store.

DBCC CheckTable

Being day seven of the DBCC Command month at SteveStedman.com, today’s featured DBCC Command is DBCC CHECKTABLE.

Description:

DBCC CheckTable is used to check the structure of a table to verify the integrity of every data page associated with that table, and all of the indexes associated with that table. If you have used DBCC CheckDB, and a problem has been shown with a table, you can use DBCC CheckTable to attempt to correct the problem.

DBCC CHECKTABLE Syntax:

dbcc checktable
(
    { 'table_name' | 'view_name' }
    [ , NOINDEX
    | index_id
    | { REPAIR_ALLOW_DATA_LOSS
    | REPAIR_FAST
    | REPAIR_REBUILD
    } ]
)
    [ WITH
        {
            [ ALL_ERRORMSGS ]
            [ , [ NO_INFOMSGS ] ]
            [ , [ TABLOCK ] ]
            [ , [ ESTIMATEONLY ] ]
            [ , [ PHYSICAL_ONLY ] ]
            [ , [ EXTENDED_LOGICAL_CHECKS  ] ]
        }
    ]

Parameters:

The REPAIR_FAST parameter is only there for reverse compatibility and does nothing.  This was removed in SQL Server 2005.

Example:

The following example….

DBCC CheckTable(Departments);

Which produces the following output indicating success with removal of the corruption.

DBCC results for 'Departments'.
There are 4 rows in 1 pages for object "Departments".
DBCC execution completed. If DBCC printed error messages, contact your 
system administrator.

That is the best case scenario, everything worked fine.   Now we will take a look at what to do if there is corruption.

Example with Corruption:


DBCC CheckTable(Departments);

Produces the following erorr:

Msg 8951, Level 16, State 1, Line 1
Table error: table 'Departments' (ID 245575913). Data row does not 
have a matching index row in the index 'DepartmentsNonClustered' (ID 2). 
Possible missing or invalid keys for the index row matching:

Msg 8955, Level 16, State 1, Line 1
Data row (1:231:0) identified by (id = 1) with index values 'department = 
'aaaping' and parent = NULL and archived = 0 and id = 1'.

Msg 8952, Level 16, State 1, Line 1
Table error: table 'Departments' (ID 245575913). Index row in index 
'DepartmentsNonClustered' (ID 2) does not match any data row. Possible 
extra or invalid keys for:

Msg 8956, Level 16, State 1, Line 1
Index row (1:273:0) with values (department = 'Camping' and parent = NULL
 and archived = 0 and id = 1) pointing to the data row identified by 
(id = 1).
DBCC results for 'Departments'.

There are 4 rows in 1 pages for object "Departments".
CHECKTABLE found 0 allocation errors and 2 consistency errors in table
 'Departments' (object ID 245575913).
repair_rebuild is the minimum repair level for the errors found by DBCC
 CHECKTABLE (dbcc_corruption.dbo.Departments).
DBCC execution completed. If DBCC printed error messages, contact your
 system administrator.

Indicating that the an index, the DepartmentsNonClustered is corrupt.

However when we query the table, all of the data looks fine.

dbcc_checktable_table_looks_good

So to fix the corrupt index, the best option is probably to just drop the index and recreate it. Lets give that a try:


DROP INDEX [DepartmentsNonClustered] ON [dbo].[Departments];
CREATE NONCLUSTERED INDEX [DepartmentsNonClustered] ON [dbo].[Departments]
(
 [department] ASC,
 [parent] ASC,
 [archived] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
       DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
       ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];
GO

DBCC CheckTable(Departments);

Which produces the following output indicating success with removal of the corruption.

DBCC results for 'Departments'.
There are 4 rows in 1 pages for object "Departments".
DBCC execution completed. If DBCC printed error messages, contact your 
system administrator.

In this case we were lucky, the table itself wasn’t corrupt, it was just an index which was easy enough to drop and recreate. Had the table structure itself, or the clustered index been corrupt that may have required more drastic steps, either restoring from backup, or saving the data we could as shown in my DBCC CheckDB blog article from a few days ago.

Notes:

DBCC CheckAlloc along with DBCC CheckTable for every object in the database are called when DBCC CheckDB is run. Running DBCC CheckAlloc or DBCC CheckTable would be redundant after running DBCC CheckDB.

The REPAIR_FAST parameter is only there for reverse compatibility and does nothing.  This was removed in SQL Server 2005.

For more information see TSQL Wiki DBCC checktable.

DBCC Command month at SteveStedman.com is almost as much fun as building Robotic Dinosaurs.

DBCC CheckIdent

Being day six of the DBCC Command month at SteveStedman.com, today’s featured DBCC Command is DBCC CHECKIDENT.

Description:

DBCC CHECKIDENT is used for check on the current value in the identity column for a table.  It also reports on the largest value in that column. DBCC CheckIdent can also be used to update or set the next identity value on a table.

If you call DBCC CheckIdent with just a table name, it will report back information, and it will also update the next identity value if there is a value in the table larger than the current next identity value.  This can be dangerous if you are not expecting this. Use the NORESEED parameter to tell DBCC CheckIdent to not make any changes. You can also use the RESEED parameter with a specific number to set the identity value to a that number.

DBCC CHECKIDENT Syntax:

dbcc checkident
(
    'table_name'
    [ , { NORESEED
        | { RESEED [ , new_reseed_value ] }
    } ]
)
    [ WITH NO_INFOMSGS ]

Example:

The following example….

First we create a table called TestSmallIntIdentity with an id of type SMALLINT and a log time.  Keep in mind that a SMALLINT accommodates values from -32768 to 32767. The default on the identity is to default to zero. If you are not familiar with this, take a look at my Are you wasting half the capacity of IDENTITY article.

Notice the table is created with the primary key being the id column to prevent duplicates.


CREATE TABLE [dbo].[TestSmallIntIdentity](
 [id] [smallint] IDENTITY(1,1) NOT NULL,
 [logTime] [datetime] NOT NULL DEFAULT GetDate(),
 CONSTRAINT [PK_TestSmallIntIdentity] PRIMARY KEY CLUSTERED ([id] ASC)
) ON [PRIMARY];

Once the table has been created we insert a single row, and look at the contents of the table, and the number of rows.

INSERT INTO TestSmallIntIdentity DEFAULT VALUES;

SELECT * FROM TestSmallIntIdentity;
SELECT count(*) FROM TestSmallIntIdentity;

DBCC_checkIdent1
At this point we have 1 row in the table. Now lets just add 1000 more rows into the table. This may take a minute to run depending on your server speed.  Once complete, the SELECT * should return 1001 rows, and the count(*) should show 1001.

SET NOCOUNT ON;
INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
GO 1000

SELECT * FROM TestSmallIntIdentity ORDER BY id DESC;
SELECT count(*) FROM TestSmallIntIdentity;

DBCC_checkIdent2In the above example, we can see that the maximum id value in the table is 1001, and therefore the next identity value would be 1002.

Now we delete everything from the table, insert a single row, take a look at the id of the row, and run DBCC CheckIdent to see what we can about the identity.

DELETE FROM TestSmallIntIdentity;

INSERT INTO TestSmallIntIdentity DEFAULT VALUES;

SELECT * FROM TestSmallIntIdentity;
SELECT count(*) FROM TestSmallIntIdentity;

DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);



DBCC_checkIdent3

DBCC_checkIdent4
As we speculated above the identity value that was inserted is 1002.

Next we delete everything from the table and check the identity, notice that the current identity value is 1002, and the current column value is ‘NULL’ implying in this case that there are no rows in the table. Since the table is empty it should be safe at this point to set the identity value to anything we want it to be (we will get to that in a minute).

DELETE FROM TestSmallIntIdentity;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent5
Next we insert one thousand more rows, and check the identity value, and it will show 2002 as the current value.

SET NOCOUNT ON;
INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
GO 1000

DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent6
So what happens if we delete part of the table.  We know that the rows in the table now have id values of 1002 to 2002, so lets delete those that are greater than 1500. From here we check the identity, and see that the identity value is 2002, and the maximum column value is 1500.

DELETE FROM TestSmallIntIdentity WHERE id > 1500;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent7

Knowing that the maximum value is 1500, if we are using trusted foreign keys, and can confirm that there are no other references to values in this table that have been deleted, we can just adjust the identity value to be 1501 as shown below.

DBCC CHECKIDENT (TestSmallIntIdentity, RESEED, 1501);

DBCC_checkIdent8

Check the identity value with DBCC CheckIdent, and we see (shown below) that the current identity value is set to 1501, and the max value in the column is 1500.

DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent9
Next we  insert a row into the TestSmallIntIdentity table, and check the the identity after, showing 1502 as the current value in the table, and the current identity value.

INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent10
Now what happens when the identity is full, meaning it has reached its maximum value for the data type used. We will try this by inserting almost enough rows to ready the 32,767 maximum of a SMALLINT data type.

DELETE FROM TestSmallIntIdentity;
DBCC CHECKIDENT (TestSmallIntIdentity, RESEED, 0);
INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);
GO
INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
GO 32765 -- Run it 32,765 times

DBCC_checkIdent11
Next we insert one row, and check to see where we are at with the identity. Everything looks good at this point, we can insert one more row.

DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent12
So we insert another with no errors, and check our current value, which shows as 32767, but we know the max value in a SMALLINT is 32767, so it must be full.

INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent13
What happens when we insert one more as follows.

INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent14

The Arithmetic overflow error message is shown:
Msg 8115, Level 16, State 1, Line 1
Arithmetic overflow error converting IDENTITY to data type smallint.
Arithmetic overflow occurred.

What do we do now?

Now we can do one of the following:

  • Stop inserting rows into this table.
  • Change the size of the identity column and everywhere else that is foreign keyed to this ID to be something larger than a SMALLINT.
  • Just reset the identity to be the most negative number possible, to buy us some time.

Lets take a look at resetting the identity to be a negative number.  Keep in mind that this only buys us some time, if we filled up the positive numbers is a SMALLINT, we will eventually use up the negative numbers. Warning, you may run into problems later once this increases to 0, and there are no more numbers left in the SMALLINT range.

Here is how to reset the identity to a new value, in this case -32768, or the most negative number that will fit into a SMALLINT.


DBCC CHECKIDENT (TestSmallIntIdentity, RESEED, -32768);

DBCC_checkIdent15
Notice in the above output, when we set the value to -32,768, there was not output that showed we had set it to -32,768. So let’s check to see what is is, using DBCC CheckIdent.  Be sure to use the NORESEED parameter on CheckIdent.  The NORESEED indicates that the check does not adjust the seed value for the identity.  If this is run in this specific case without the NORESEED parameter, it will automatically adjust the identity value since it is less than the column value. It would set it to 32767, and we would end up in the same place as we were before we tried setting it to -32768.


INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent16

Now we will use up most of the negative identity values from the SMALLINT, to get to the point that the identity value is -2.  Here we know that all of the numbers that can fit into the SMALLINT that are less than or equal to -2 have been used, and we know that all the values greater than 0 have been used. the only numbers left are -1, and 0.

GO
INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
GO 32765 -- Run it 32,765 times

-- now check to see where we are at.
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent17Next one more insert.


-- insert another and check
INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent18

Now we insert one more row, which uses up 0, and there no more numbers left after this insert.


-- and another
INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent19

Now we try one more insert anyway, and find out that we are violating the PRIMARY KEY constraint with duplicate keys.


-- and another
INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent21

Msg 2627, Level 14, State 1, Line 1
Violation of PRIMARY KEY constraint 'PK_TestSmallIntIdentity'. Cannot insert duplicate key in object 'dbo.TestSmallIntIdentity'. The duplicate key value is (1).
The statement has been terminated.

That’s it, we are now out of numbers for this primary key ID value with a SMALLINT data type. The next thing to try would be a larger data type like INTEGER.
Now lets check the identity value, and see how it looks. If we just run DBCC CheckIdent wihtout the NORESEED value it will reseed the value to 32767 without telling us. So lets run it a second time to check as shown here.


DBCC CHECKIDENT (TestSmallIntIdentity);
-- now check, and see that it has already changed.
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent22

Now we try the final insert, this time it is trying to insert 32767 + 1 which as shown below causes an arithmetic overflow.

INSERT INTO TestSmallIntIdentity DEFAULT VALUES;
DBCC CHECKIDENT (TestSmallIntIdentity, NORESEED);

DBCC_checkIdent23

Summary

You can use DBCC CheckIdent to see what the current identity value is. You can also use it to optionally set the identity value to something you specify, or end up having it automatically set for you.

 

Related:

DBCC CHECKCONSTRAINTS – Check integrity of table constraints.

Are you wasting half the capacity of IDENTITY?

Notes:

For more information see TSQL Wiki DBCC checkident.

DBCC Command month at SteveStedman.com is almost as much fun as calculating identity values.

SQL Server 2012 IIF Statement

SQL Server 2012 introduces the IIF statement. It is very similar to the IIF statement in MS Access.

I have extracted the IIF part of my presentation on Whats new in SQL Server 2012, and turned it into a video with a demo of how to use the IIF statement. The IIF statement is a quick shortcut to simple CASE statements.

Here is the T-SQL references in the video:

USE [Master];
CREATE DATABASE [tsql2012];
GO

USE [tsql2012];

-- Table to be used by Over Clause Rows/Range
CREATE TABLE REVENUE
(
[DepartmentID] int,
[Revenue] int,
[Year] int
);

insert into REVENUE
values (1,10030,1998),(2,20000,1998),(3,40000,1998),
(1,20000,1999),(2,60000,1999),(3,50000,1999),
(1,40000,2000),(2,40000,2000),(3,60000,2000),
(1,30000,2001),(2,30000,2001),(3,70000,2001),
(1,90000,2002),(2,20000,2002),(3,80000,2002),
(1,10300,2003),(2,1000,2003), (3,90000,2003),
(1,10000,2004),(2,10000,2004),(3,10000,2004),
(1,20000,2005),(2,20000,2005),(3,20000,2005),
(1,40000,2006),(2,30000,2006),(3,30000,2006),
(1,70000,2007),(2,40000,2007),(3,40000,2007),
(1,50000,2008),(2,50000,2008),(3,50000,2008),
(1,20000,2009),(2,60000,2009),(3,60000,2009),
(1,30000,2010),(2,70000,2010),(3,70000,2010),
(1,80000,2011),(2,80000,2011),(3,80000,2011),
(1,10000,2012),(2,90000,2012),(3,90000,2012);

USE [tsql2012];
-- first lets look at the REVENUE table
SELECT *
FROM Revenue;

-------------------------------------------------------------------

-- IIF
-- http://stevestedman.com/?p=1578
-- assume we want to display an indicator to see if we are above
-- or below average. First we start with the average over departmentID
SELECT Year, DepartmentID, Revenue,
avg(Revenue) OVER (PARTITION by DepartmentID) as AverageRevenue
FROM REVENUE
ORDER BY DepartmentID, year;
-- without IIF using the CASE statement we would get the following
-- http://stevestedman.com/?p=1578
SELECT Year, DepartmentID, Revenue, AverageRevenue,
CASE WHEN Revenue > AverageRevenue
THEN 'Better Than Average'
ELSE 'Not Better'
END as Ranking
FROM (SELECT Year, DepartmentID, Revenue,
avg(Revenue) OVER (PARTITION by DepartmentID) as AverageRevenue
FROM REVENUE ) as t
ORDER BY DepartmentID, year;

-- now the same functionality using IIF and simplifying the code
-- http://stevestedman.com/?p=1578
SELECT Year, DepartmentID, Revenue, AverageRevenue,
iif(Revenue > AverageRevenue, 'Better Than Average', 'Not Better') as Ranking
FROM (SELECT Year, DepartmentID, Revenue,
avg(Revenue) OVER (PARTITION by DepartmentID) as AverageRevenue
FROM REVENUE ) as t
ORDER BY DepartmentID, year;

----------------------------------
SELECT IIF(3=2,'true',NULL) -- ERROR ?

----------------------------------

SELECT IIF(1=7,NULL,NULL) -- ERROR !
--Do you think that IIF actually uses the CASE statement?

TSQL 2012 CHOOSE Function

In SQL Server 2012 there is a new function called CHOOSE that takes in an offset, and a list of options.  Choose is a function that returns the item at a specific index.

Syntax:
—CHOOSE(index, val_1, val_2, val_3, …)
—If the index is greater than the number of values or less than 1 it returns NULL
—Easier than using a CASE statement in some examples.
Example:
</div>
<div>
-- CHOOSE
-- returns the item at a specific index
declare @corners as int = 6
SELECT choose(@corners, 'point', 'line', 'triangle', 'square',
 'pentagon', 'hexagon', 'heptagon', 'octagon')

-- the old way using case.
SELECT CASE @corners
 WHEN 1 THEN 'point'
 WHEN 2 THEN 'line'
 WHEN 3 THEN 'triangle'
 WHEN 4 THEN 'square'
 WHEN 5 THEN 'pentagon'
 WHEN 6 THEN 'hexagon'
 WHEN 7 THEN 'heptagon'
 WHEN 8 THEN 'octagon'
 else NULL
 END;

In the above example, the CASE statement and CHOOSE function do the same thing, but the CHOOSE function is a bit easier to write.Here is another example

-- CHOOSE day of week example
DECLARE @day as int=4
SELECT CHOOSE(@day,'Sunday','Monday', 'Tuesday',
              'Wednesday','Thursday','Friday','Saturday')
For choose and other new TSQL functionality in Server 2012 take a look at my What’s New in TSQL 2012 presentation, which will be presented at Seattle Code Camp on June 16th.

Recursive CTE’s

As part of my planning for the SQL Saturday Presentation in Vancouver I am creating an hour long presentation on Common Table Expressions.

The easiest way to do a recursive query in SQL server is to use a recursive CTE (Common Table Expression).

What is a Common Table Expression in SQL Server?

  • Similar to the ease of a temporary table without the performance problems of a temp table.
  • Temporary Named Result Set
  • Acts like a temporary view
  • Can be use to
    • Create a recursive query
    • Simplify a query by using a result set multiple times
    • Self JOIN a subquery without running the subquery twice

Here is a transact SQL sample based on the AdventureWorksLt database demonstrating a CTE for a recursive query.

WITH CategoryCTE(CategoryId, Name, Level) AS (
SELECT ProductCategoryId AS CatId,
Name, 0 as Level
FROM saleslt.ProductCategory
WHERE ParentProductCategoryId is NULL
UNION ALL -- and now for the recursive part 
SELECT c.ProductCategoryId AS CatId,
c.Name, ccte.CategoryId + 1 as Level
FROM saleslt.ProductCategory c
INNER JOIN CategoryCTE as ccte
ON ccte.CategoryId = c.ParentProductCategoryID
)
SELECT *
FROM CategoryCTE
ORDER BY Level, Name;

 

CTE’s and Recursive CTE’s were first introduced in SQL Server 2005, and are available in 2008 and 2008R2 with the same syntax.

 

 

For another example of CTE’s (non-recursive) take a look at the query behind the Backup Set SQL Server Health Report.