Microsoft SQL ISV Program Management Team

  • SQL Server 2008 : new binary – hex string conversion functionality can dramatically improve related query performance by orders of magnitude.

    In previous SQL Server releases it wasn’t possible to convert binary data to string characters in hex format directly, because SQL Server did not have a built-in Transact-SQL command for converting binary data to a hexadecimal string. The Transact-SQL CONVERT command converted binary data to character data in a one byte to one character fashion. SQL Server would take each byte of the source binary data, convert it to an integer value, and then uses that integer value as the ASCII value for the destination character data. This behavior applied to the binary, varbinary, and timestamp datatypes.

     

    The only workarounds were to use either a stored procedure as described in a Knowledge Base Article:  "INFO: Converting Binary Data to Hexadecimal String" (  http://support.microsoft.com/kb/104829   ) or by writing a CLR function.

     

    An ISV I work with doesn’t support CLR and therefore they implemented their own version of a custom convert function in form of a stored procedure. This one was even faster than everything else they found on the Internet.

     

    NEW – IN SQL SERVER 2008 the convert function was extended to support binary data – hex string conversion. It looks like a tiny improvement almost not worth mentioning.

     

    However, for the ISV it was a big step forward as some critical queries need this functionality. Besides the fact that they no longer have to ship and maintain their own stored procedure, a simple repro showed a tremendous performance improvement.

     

    Repro:

    =====

     

    I transformed the procedure described in the KB article mentioned above into a simple function. The stored procedure below will create a simple test table with one varbinary column and insert some test rows in 10K packages ( e.g. nr_rows = 100 -> 1 million rows in the table ).

     

    The repro shows two different test cases:

    1. insert 0x0 two million times

    2. insert 0x0123456789A12345 two million times

     

    Depending on the length of the value the disadvantage of the stored procedure solution will be even bigger. On my test machine the results of the test queries below were:

    (both tests were done with the same SQL Server 2008 instance - no change of any settings)

     

    1. two million times value 0x0

     

        a, using stored procedure : about 3460 logical reads, no disk IO, ~52 secs elapsed time

        b, using new convert feature : about 5200 logical reads,  no disk IO, < 1 sec elapsed time

     

    2. two million times value 0x0123456789A12345

        a, using stored procedure : about 3460 logical reads, no disk IO, ~157 secs elapsed time

        b, using new convert feature : about 5200 logical reads,  no disk IO, < 1 sec elapsed time

     

    Repro Script:

    ========

     

    create function sp_hexadecimal ( @binvalue varbinary(255) )

    returns varchar(255)

    as

    begin

          declare @charvalue varchar(255)

          declare @i int

          declare @length int

          declare @hexstring char(16)

          select @charvalue = '0x'

          select @i = 1

          select @length = datalength(@binvalue)

          select @hexstring = '0123456789abcdef'

          while (@i <= @length)

          begin

                declare @tempint int

                declare @firstint int

                declare @secondint int

                select @tempint = convert(int, substring(@binvalue,@i,1))

                select @firstint = floor(@tempint/16)

                select @secondint = @tempint - (@firstint*16)

                select @charvalue = @charvalue +

                substring(@hexstring, @firstint+1, 1) +

                substring(@hexstring, @secondint+1, 1)

                select @i = @i + 1

          end

    return ( @charvalue )

    end

     

     

    create procedure cr_conv_test_table ( @value varbinary(16), @nr_rows int )

    as

    begin

          declare @exist int

          declare @counter int

          set NOCOUNT ON

          set statistics time off

          set statistics io off

          set statistics profile off

          set @exist = ( select count(*) from sys.objects

                          where name = 'conv_test_table' and

                                type = 'U' )

          if( @exist = 1 )

                drop table conv_test_table

     

          set @exist = ( select count(*) from sys.objects

                          where name = 'conv_test_table_temp' and

                                type = 'U' )

          if( @exist = 1 )

                drop table conv_test_table_temp

     

          create table conv_test_table ( varbincol varbinary(16) )

          create table conv_test_table_temp ( varbincol varbinary(16) )

          set @counter = 10000

          while @counter > 0

                begin

                      insert into conv_test_table_temp values ( @value )

                      set @counter = @counter - 1

                end

          set @counter = @nr_rows

          while @counter > 0

          begin

                insert into conv_test_table select * from conv_test_table_temp

                set @counter = @counter - 1

          end

    end

     

    -- create 2 million test rows

    execute cr_conv_test_table 0x0, 200

     

    set statistics time on

    set statistics io on

     

    -- compare runtime of stored procedure with new convert feature

    select count(*) from conv_test_table

     where dbo.sp_hexadecimal(varbincol) = '0x00'

    select count(*) from conv_test_table

     where CONVERT(varchar(255),varbincol,1) = '0x00'

     

    -- create 2 million test rows

    execute cr_conv_test_table 0x0123456789A12345, 200

     

    set statistics time on

    set statistics io on

     

    -- compare runtime of stored procedure with new convert feature

    select count(*) from conv_test_table

     where dbo.sp_hexadecimal(varbincol) = '0x0123456789A12345'

    select count(*) from conv_test_table

     where CONVERT(varchar(255),varbincol,1) = '0x0123456789A12345'

     

     

     

  • Designing Composite Indexes

     When it comes to creating composite indexes there are two questions I get asked most often by the ISVs I work with as well as their customers.

    1.      What is the optimal number of columns to include in a composite index?

    2.      What should the order of those columns be?

    While in most cases the answer is “it depends’, there are a handful of considerations which help in defining optimal composite indexes.  This BLOG attempts to offer some guidance on these and also elaborate on some of the tradeoffs.

     

    To start with let’s make sure we’re all on the same page with the definition of a composite index: “A composite index is one which is created across two or more key columns”.  Composite indexes are often also referred to as ‘multi-column’ index.  For example, an index (idx1) created on table (TabA) columns (col1, col2, col3 (CREATE CLUSTERED INDEX idx1 ON TabA (col1, col2, col3);).  Index columns refer only to key columns of the index; although INCLUDED columns are part of the physical index structure, they are not part of the index key and therefore not considered to be a part of a composite index for the purpose of this discussion.

     

    Now let’s drill into at the answers to the two questions individually.

     

    1.     Optimal number of columns to include in a composite index

    As mentioned earlier, there is no one answer for this question, every query and table situation is different and needs to be evaluated on a case by case basis.  That being said there are a few rules of thumb that can be used to effectively determine which columns to include in the composite index.

     

    a.       Only include columns that are selective.  Columns that have just a few, or in an extreme case just 1, values across the entire table add little or no value in index search operations and should not be included in an index even if they are used as query predicates.

     

    b.      Volatile columns (columns that are frequently updated) should not be indexed.   When the value of the column that’s part of an index is modified, it usually results in the index needing to be reorganized as well.   This results in the database engine having to perform multiple writes and thereby having additional overhead.  For example if a volatile column (col1) is included in a non-clustered index, every time any data in col1 changes, the database engine has to perform two writes, one to update the col1 data itself and the other to update the non-clustered index that includes col1.  If the table has more than one non-clustered index that includes col1, then for every change equal to the number of such non-clustered indexes need to be performed.

     

    c.       Columns included in query predicates.  Columns that are commonly used as query predicates in multiple frequently executed queries should be included in the composite index as long as they qualify the checks mentioned in 1.a and 1.b above.

     

    d.      Limit the number of columns.  While SQL Server limits the maximum number of columns in an index key to 16, in my opinion that limit is of little significance as in most cases you should be able to create an optimal indexes with far fewer columns.  As a rule you should use the fewest number of columns in a composite index as required to give you optimal performance. For example, if the combination of two columns (col1 and col2) results in uniquely qualifying every row of the table, there is little value in adding additional columns to the composite index to make the index seek operations more efficient.  In most cases I have seen well designed applications to have composite indexes built across no more than 6 columns.  Every column in a composite index has disk space, performance overhead related to disk I/O and index maintenance overhead associated with it.  To yield a net positive gain from having the index you should make sure that the benefit of having a column in a composite index outweighs the overheads.  Therefore the fewer the columns in a composite index the lower the overhead and higher the probability of a net gain.  This is particularly applicable to clustered indexes because the length of the index key length will affect all the non-clustered indexes.

     

    e.       Use INCLUDED columns.   Scenarios where a column is added to a composite index for the sole purpose of creating a ‘covering index’ (an index where the SELECT clause is fully serviced by the index itself and the underlying table doesn’t need to get referenced) can be made to use INCLUDED columns instead.  The INCLUDED columns provide the same benefits as far as covering indexes go, but do not have the index maintenance overhead associated with the actual composite index columns. You can get additional information on INCLUDED columns as well as read some of the other advantages they offer by referring to: http://msdn.microsoft.com/en-us/library/ms190806.aspx.

     

    f.        Index key record size limited to 900 bytes. In SQL Server 2008 the sum of the lengths of all columns in an index key cannot exceed 900 bytes (this limit does not apply to XML indexes or spatial indexes).  When using composite indexes you need to make sure that you do not exceed this limit.  For covering index case where you need to have indexes whose total length is greater than 900 bytes, you can consider the use of INCLUDED columns as explained above.  For cases where the limit is exceeded due to the number and sizes of the columns, you have no choice but to either eliminate some of the previously selected columns, or change the data types of some of the columns if possible.

     

    g.       Star join optimization.  There are some queries, especially in data warehouse workloads, where creating a composite index on two dimension columns in the fact table increases performance by the use of the ‘safe crossjoin’ star join optimization.  In such cases, the database engine is able to crossjoin and apply combined selectivity of the two dimensions before digging into the fact table, potentially yielding a significant reduction in I/O. For example, in the following query:

     

    SELECT Fact.* FROM Fact

    JOIN DimA ON Fact.SK_A = DimA.SK_A

    JOIN DimB ON Fact.SK_B = DimB.SK_B

    WHERE DimA.Col IN (42, 13, 7)

    AND DimB.Col IN (13, 42, 7);

     

    a composite index on columns SK_A, SK_B (CREATE INDEX IX_StarJoin ON Fact (SK_A, SK_B)) enables the use of the ‘safe crossjoin’ star join optimization as can be seen in the corresponding query plan below.

     

     

    2.     Order of columns in a composite index

    Once we’ve determined which indexes to include in a composite index, the next challenge is to determine the optimal order for these columns.  As with the previous question, there is no clear-cut answer here either, but the points below should help provide guidance around the key factors to consider.

     

    a.       Ensure that the leading columns are selective.  This has a two-fold benefit: (1) the effectiveness of index seek operation is increased; and (2) since the intra-query parallelism with which an index create or rebuild operation is executed is limited to the selectivity of the first column, having a selective leading column ensures that the database engine can execute the operation with the highest degree of parallelism possible on the server at the given time.  For example, if the first column of an index only has one data value (example: all values are ‘Region001’), the index rebuild operation can at most be executed with a parallelism of 1, i.e. it is executed with a serial query plan.   I have seen real-world customer deployments where the design of the database has been such that the leading columns of certain large tables have had low (all values the same) selectivity and because of this their index rebuild operations were executed with a serial query plan.  This resulted in the task taking a really long time to complete even though they had a relatively lightly loaded 32-core server and had all the other database server configurations (‘max degree of parallelism’, ‘cost threshold for parallelism’) set correctly.

     

    One possible exception to this point may arise with regards to index fragmentation.  When selecting the leading column of an index (clustered index especially) you should take into consideration potential for fragmentation, and any column for which you are inserting data values all over key range should preferably not be placed as the leading column, especially if there's a high insert or update activity on the table.

     

    b.      Specify columns used in inequality predicate towards the end of the list.  When creating composite indexes for queries that use a mix of equality and inequality (>, <, !=, BETWEEN) predicates, the columns corresponding to the selective equality predicates should specified first in the composite index, followed by the inequality predicates.  This is because the depth with which an index seek operation is performed is limited to the occurrence of the first inequality predicate.  For example, if the following query was to execute and use the idx1 index defined above, the index seek operation would only be able to seek on the first column (col1) of the index even though there are valid equality predicates for col3.

     SELECT col1 FROM TabA

    WHERE col1 > 28

          AND col2 = 99

          AND col3 = 0;

    GO

     

    The query execution plan text (viewed using SET SHOWPLAN_TEXT ON) for this query would look like:

    |--Clustered Index Seek(OBJECT:([tempdb].[dbo].[TabA].[idx1]), SEEK:([tempdb].[dbo].[TabA].[col1] > CONVERT_IMPLICIT(int,[@1],0)),  WHERE:([tempdb].[dbo].[TabA].[col2]=CONVERT_IMPLICIT(int,[@2],0) AND [tempdb].[dbo].[TabA].[col3]=CONVERT_IMPLICIT(int,[@3],0)) ORDERED FORWARD)

     

    In this case you can notice that the index is not optimally used due to the SEEK operation only being executed on col1 while the predicate values of col2 and col3 are filtered out using a WHERE clause (SCAN operation).

     

    If the index idx1 was primarily created to help the performance of this query it may help to reorganize the index columns such that the one (col1) which is used in the inequality predicate is specified towards the end (right-most column) in the composite index, i.e. the order should be:  col2, col3, col1.  This would permit the database engine to optimally SEEK into columns col2, col3 and col1 as can be seen in the query execution plan text below.

    |--Clustered Index Seek(OBJECT:([tempdb].[dbo].[TabA].[idx1]), SEEK:([tempdb].[dbo].[TabA].[col2]=CONVERT_IMPLICIT(int,[@2],0) AND [tempdb].[dbo].[TabA].[col3]=CONVERT_IMPLICIT(int,[@3],0) AND [tempdb].[dbo].[TabA].[col1] > CONVERT_IMPLICIT(int,[@1],0)) ORDERED FORWARD)

     

    After making this index change you will notice that there is no WHERE clause in the query plan text and the number of logical read operations (observed via SET STATISTICS IO ON) decrease.

     

    NOTE: You cannot view these internal index operations using the graphical showplan available in SQL Server Management Studio.

     

    If a query has more than 1 inequality predicate it is best to specify the most selective inequality predicate immediately following any equality predicates.

     

    c.       Specify appropriate ascending or descending column order.  The columns in an index can be specified to be ascending (ASC) or descending (DESC).  For composite index, qualifying the column order to match the ORDER BY clause in a query optimizes the use of the index.  For example, for the query below it would help to specify the order of the index columns as col1 ASC, col2 DESC, col3 ASC.

    SELECT col1, col2, col3 FROM TabA

    ORDER BY col1 ASC, col2 DESC, col3 ASC;

     

    NOTE:  SQL Server is able to perform index seeks and scans in either direction; this means that you do not need to create 2 indexes if one query orders the result set using “ORDER BY col1 ASC” and the other uses “ORDER BY col1 DESC”.

     

    Once you’ve created the composite index, you can use the SQL Server built-in stored procedure sp_spaceused to view the total size of all the indexes present on a table. For example: sp_spaceused  [dbo.TbA] displays:

     

    name        rows        reserved    data        index       unused

    TabA        869088     187288 KB   158032 KB   29192 KB    64 KB

    In this output we can see that the total space used by all the indexes in TabA is 29.19 MB.

     

    If you’re interested in drilling in further, you can use the following query to view details about each index level of every index for the table (TabA).

    SELECT OBJECT_NAME(OBJECT_ID), *

    FROM sys.dm_db_index_physical_stats (DB_ID(),NULL, NULL , NULL, 'SAMPLED')

    WHERE OBJECT_ID=OBJECT_ID('TabA')

     

    Please refer to SQL Server Books OnLine for details about each of the columns displayed (http://msdn.microsoft.com/en-us/library/ms188917.aspx).

     

    The points mentioned above apply equally to clustered as well as non-clustered composite indexes, unless explicitly stated, and therefore no distinction has been drawn between them.

     

    NOTE: It is a SQL Server best practice to have a clustered index on all tables, please refer to the SQL Server Best Practices Article: ‘Comparing Tables Organized with Clustered Indexes versus Heaps’ (http://sqlcat.com/whitepapers/archive/2007/12/16/comparing-tables-organized-with-clustered-indexes-versus-heaps.aspx) for additional information on this topic.

     

     

  • How to create an autonomous transaction in SQL Server 2008

    I have been asked by many customers and partners, especially those migrating from Oracle, this question: how to create an autonomous transaction in SQL Server? It turns out to be a tricky thing to do since SQL Server doesn't have built-in autonomous transaction support like Oracle.

    An Autonomous transaction is essentially a nested transaction where the inner transaction is not affected by the state of the outer transaction. In other words, you can leave the context of current transaction (outer transaction) and call another transaction (autonomous transaction). Once you finish work in the autonomous transaction, you can come back to continue on within current transaction. What is done in the autonomous transaction is truly DONE and won't be changed no matter what happens to the outer transaction. To make it easier to understand, here is an example of the described scenario.

    BEGIN TRAN OuterTran

          INSERT TABLE1

          BEGIN “AUTONOMOUS” TRAN InnerTran

                INSERT TABLE2

          COMMIT “AUTONOMOUS” TRAN InnerTran

    ROLLBACK TRAN OuterTran

    The above pseudo script is meant to preserve result of INSERT TABLE2”. In SQL Server 2008 or prior versions, "ROLLBACK TRAN" would always rollback all inner transactions to the outermost "BEGIN TRAN" statement (without specifiying savepoint). So the "InnerTran" transaction would be rolled back as well, which is not the desired behavior for the particular scenario.

    You could wonder why we need an autonomous transaction in the first place. Why can't we just implement two separate transactions so they don't interfere with each other? There are scenarios where applications do need logic structured like this. Logging errors in database is one of the most common scenarios. Below is a TSQL script demonstrating a nested transaction where the inner transaction attempts to save the runtime errors in a table.

    USE TEMPDB

    GO

    CREATE TABLE ErrorLogging (logTime DATETIME, msg VARCHAR(255))

    CREATE TABLE TestAT (id INT PRIMARY KEY)

    GO

    CREATE PROCEDURE usp_ErrorLogging

          @errNumber INT

    AS

          INSERT INTO ErrorLogging VALUES (GETDATE(), 'Error ' + CAST(@errNumber AS VARCHAR(8)) +' occurred.')

    GO

     

    DECLARE @ERROR AS INT

    INSERT INTO TestAT VALUES (1)

    BEGIN TRAN OuterTran

          INSERT INTO TestAT VALUES (1) -- This will raise primary key constraint violation error

         

          SELECT @ERROR = @@ERROR

          IF @ERROR <> 0

          BEGIN

                BEGIN TRAN InnerTran

                      EXEC usp_ErrorLogging @ERROR

                COMMIT TRAN InnerTran

         

                ROLLBACK TRAN OuterTran

          END

     

    IF @@TRANCOUNT > 0     

    COMMIT TRAN OuterTran

    GO

    SELECT * FROM TestAT

    SELECT * FROM ErrorLogging

    GO

    If you run above script against SQL Server, you would see no error message recorded in table "ErrorLogging" due to the "ROLLBACK TRAN OuterTran" statement. So, how can we make it work?

    In SQL Server 2008, you can implement a loopback linked server to achieve the same goal. For more information about loopback linked server, check Books Online for details (http://msdn.microsoft.com/en-us/library/ms188716.aspx).

    USE MASTER

    GO

    EXEC sp_addlinkedserver @server = N'loopback',@srvproduct = N' ',@provider = N'SQLNCLI', @datasrc = @@SERVERNAME

    GO

    EXEC sp_serveroption loopback,N'remote proc transaction promotion','FALSE'

    Go

    Note 'remote proc transaction promotion' is a new option on SQL Server 2008, which allows you to control whether or not you want to enlist remote stored procedure call in a distributed transaction. When this option is off  (FALSE) as we set in the above example, the local transaction will not be promoted to distributed transaction. This is how we are able to separate outer and inner transactions in a "autonomous transaction" fashion.

    The Inner transaction above can be replaced by:

          BEGIN TRAN InnerTran

                EXEC loopback.tempdb.dbo.usp_ErrorLogging @ERROR

          COMMIT TRAN InnerTran

    A complete working script is in the appendix below. I want to point out that this method of using a loopback linked server might not scale well if it's executed very frequently. And it only works in SQL Server 2008 due to new server option of 'remote proc transaction promotion' as discussed above. As always, test before you use it.

    If you are looking for alternative ways of creating autonomous transaction on SQL 2008 or 2005, you have these options:

    1. Loopback connection from SQLCLR procedure to start a new transaction. Compared to more rigid structure need of loopback linked server, SQLCLR is more flexible and gives you more control over how you want to handle interaction with database. If the logic of the autonomous transaction includes computational tasks, it's one of SQLCLR's strengths to provide performance gain as extra benefit.
    2. Using table variable to save data within transaction. Table variables are not affected by transaction rollback thus serve as temporary buffer for transaction data. Once transaction is done, you can dump data out of table variable to a permanent table. Table variables have limited scope and are less flexible. Usually they would also be slower due to lack of index/statistics. However, it does offer you a pure TSQL option with no need to create anything new.
    3. Loopback connection from Extended Stored Procedures. Note: Extendeded Stored Procedure functionality is on the SQL Server deprecation list and Microsoft strongly recommends NOT using it.

    In a future blog, I'll provide sample SQLCLR code and a script using a table variable to create autonomous transactions. I will also compare their performance differences with loopback linked server in a scalability test. Stay tuned.

    Appendix

    USE MASTER

    GO

    EXEC sp_addlinkedserver @server = N'loopback',@srvproduct = N' ',@provider = N'SQLNCLI', @datasrc = @@SERVERNAME

    GO

    EXEC sp_serveroption loopback,N'remote proc transaction promotion','FALSE'

    EXEC sp_serveroption loopback,N'RPC OUT','TRUE' -- Enable RPC to the given server.

    Go

    USE TEMPDB

    GO

    CREATE TABLE ErrorLogging (logTime DATETIME, msg VARCHAR(255))