Archive for May, 2008

Parsing 1D and 2D Delimited Strings (Arrays) in SQL Server 2005 and 2000

Friday, May 30th, 2008

I’m re-posting a post I put at SQLServerCentral.com

********************************************************************************

This 2D stuff is excellent for normalizing 1NF (first normal form) violations like ‘123^12|456^45′|789^12|945^34′

2D array parsing without table variables or temp tables! It is using Itzik Ben-Gan’s parsing algorithm that relies on a table of numbers (counter / tally / nums). My version of the 2D enhancement uses CROSS APPLY so it doesn’t work in SQL Server 2000.

2D ‘Table’ version - outputs vertical-ized data only; faster but not very useful on 2D data:

--Normal VarChar version
CREATE FUNCTION dbo.fn_DelimitToTable_2D
        (
                @String VarChar(8000),
                @Delimiter1 VarChar(1),
                @Delimiter2 VarChar(1)
        ) RETURNS TABLE
AS

RETURN
        (
                SELECT Counter2nd.Value AS Value
                FROM
                        (
                                SELECT
                                        SUBSTRING(@String+@Delimiter1, PK_CountID, CHARINDEX(@Delimiter1, @String+@Delimiter1, PK_CountID)-PK_CountID) AS Value
                                FROM dbo.counter

                                WHERE PK_CountID >0 AND PK_CountID<LEN(@String)+LEN(@Delimiter1) AND SubString(@Delimiter1 + @String + @Delimiter1, PK_CountID, 1)=@Delimiter1
                        ) AS Counter1st
                        CROSS APPLY (
                                SELECT
                                        SUBSTRING(Counter1st.Value+@Delimiter2, PK_CountID, CHARINDEX(@Delimiter2, Counter1st.Value+@Delimiter2, PK_CountID)-PK_CountID) AS Value
                                FROM dbo.counter
                                WHERE PK_CountID >0 AND PK_CountID<LEN(Counter1st.Value)+LEN(@Delimiter2) AND SubString(@Delimiter2 + Counter1st.Value + @Delimiter2, PK_CountID, 1)=@Delimiter2
                        ) AS Counter2nd
        )
GO

–Integer casting version when output is used to join to integer PK/FK columns.
CREATE FUNCTION dbo.fn_DelimitToIntTable_2D
        (
                @String VarChar(8000),
                @Delimiter1 VarChar(1),
                @Delimiter2 VarChar(1)

        ) RETURNS TABLE
AS

RETURN
        (
                SELECT CONVERT(int, Counter2nd.Value) AS PK_IntID
                FROM
                        (
                                SELECT
                                        SUBSTRING(@String+@Delimiter1, PK_CountID, CHARINDEX(@Delimiter1, @String+@Delimiter1, PK_CountID)-PK_CountID) AS Value
                                FROM dbo.counter
                                WHERE PK_CountID >0 AND PK_CountID<LEN(@String)+LEN(@Delimiter1) AND SubString(@Delimiter1 + @String + @Delimiter1, PK_CountID, 1)=@Delimiter1
                        ) AS Counter1st
                        CROSS APPLY (
                                SELECT
                                        SUBSTRING(Counter1st.Value+@Delimiter2, PK_CountID, CHARINDEX(@Delimiter2, Counter1st.Value+@Delimiter2, PK_CountID)-PK_CountID) AS Value
                                FROM dbo.counter
                                WHERE PK_CountID >0 AND PK_CountID<LEN(Counter1st.Value)+LEN(@Delimiter2) AND SubString(@Delimiter2 + Counter1st.Value + @Delimiter2, PK_CountID, 1)=@Delimiter2

                        ) AS Counter2nd
        )
GO


‘Array’ version - outputs indexer also (more overhead):
--Normal VarChar version
CREATE FUNCTION dbo.fn_DelimitToArray_2D
        (
                @String VarChar(8000),
                @Delimiter1 VarChar(1),
                @Delimiter2 VarChar(1)
        ) RETURNS TABLE
AS

RETURN
        (
                SELECT Counter1st.Pos AS RowPos, Counter2nd.Pos AS ColPos, Counter2nd.Value AS Value
                FROM
                        (
                                SELECT
                                        PK_CountID - LEN(REPLACE(LEFT(@String, PK_CountID-1), @Delimiter1, ‘’)) AS Pos,
                                        SUBSTRING(@String+@Delimiter1, PK_CountID, CHARINDEX(@Delimiter1, @String+@Delimiter1, PK_CountID)-PK_CountID) AS Value
                                FROM dbo.counter

                                WHERE PK_CountID >0 AND PK_CountID<LEN(@String)+LEN(@Delimiter1) AND SubString(@Delimiter1 + @String + @Delimiter1, PK_CountID, 1)=@Delimiter1
                        ) AS Counter1st
                        CROSS APPLY (
                                SELECT
                                        PK_CountID - LEN(REPLACE(LEFT(Counter1st.Value, PK_CountID-1), @Delimiter2, ‘’)) AS Pos,
                                        SUBSTRING(Counter1st.Value+@Delimiter2, PK_CountID, CHARINDEX(@Delimiter2, Counter1st.Value+@Delimiter2, PK_CountID)-PK_CountID) AS Value
                                FROM dbo.counter
                                WHERE PK_CountID >0 AND PK_CountID<LEN(Counter1st.Value)+LEN(@Delimiter2) AND SubString(@Delimiter2 + Counter1st.Value + @Delimiter2, PK_CountID, 1)=@Delimiter2
                        ) AS Counter2nd
                )
GO

–Integer casting version when output is used to join to integer PK/FK columns.
CREATE FUNCTION dbo.fn_DelimitToIntArray_2D
        (
                @String VarChar(8000),
                @Delimiter1 VarChar(1),

                @Delimiter2 VarChar(1)
        ) RETURNS TABLE
AS

RETURN
        (
                SELECT Counter1st.Pos AS RowPos, Counter2nd.Pos AS ColPos, CONVERT(int, Counter2nd.value) AS PK_IntID
                FROM
                        (
                                SELECT
                                        PK_CountID - LEN(REPLACE(LEFT(@String, PK_CountID-1), @Delimiter1, ‘’)) AS Pos,
                                        SUBSTRING(@String+@Delimiter1, PK_CountID, CHARINDEX(@Delimiter1, @String+@Delimiter1, PK_CountID)-PK_CountID) AS value
                                FROM dbo.counter
                                WHERE PK_CountID >0 AND PK_CountID<LEN(@String)+LEN(@Delimiter1) AND SubString(@Delimiter1 + @String + @Delimiter1, PK_CountID, 1)=@Delimiter1
                        ) AS Counter1st
                        CROSS APPLY (
                                SELECT
                                        PK_CountID - LEN(REPLACE(LEFT(Counter1st.value, PK_CountID-1), @Delimiter2, ‘’)) AS Pos,

                                        SUBSTRING(Counter1st.value+@Delimiter2, PK_CountID, CHARINDEX(@Delimiter2, Counter1st.Value+@Delimiter2, PK_CountID)-PK_CountID) AS value
                                FROM dbo.counter
                                WHERE PK_CountID >0 AND PK_CountID<LEN(Counter1st.value)+LEN(@Delimiter2) AND SubString(@Delimiter2 + Counter1st.value + @Delimiter2, PK_CountID, 1)=@Delimiter2
                        ) AS Counter2nd
                )
GO


For those of you who don’t have Itzik Ben-Gan’s Inside SQL Server 2005 T-SQL books or been to any of his conference sessions (the books are a lot cheaper), here are 1D versions:

‘Table’ version - ordinal postion stripped out for speed; Great for stored-procedure-izing IN() clauses - WHERE id IN (1,2,3,4):

--Normal VarChar version
CREATE FUNCTION dbo.fn_DelimitToTable
        (
                @String VarChar(8000),
                @Delimiter VarChar(1)
        ) RETURNS TABLE
AS

RETURN
        (
                SELECT SUBSTRING(@String+@Delimiter, PK_CountID, CHARINDEX(@Delimiter, @String+@Delimiter, PK_CountID)-PK_CountID) AS Value
                FROM dbo.counter

                WHERE PK_CountID >0 AND PK_CountID<LEN(@String)+LEN(@Delimiter) AND SubString(@Delimiter + @String + @Delimiter, PK_CountID, 1)=@Delimiter
        )
GO

–Integer casting version when output is used to join to integer PK/FK columns.
CREATE FUNCTION dbo.fn_DelimitToIntTable
        (
                @String VarChar(8000),
                @Delimiter VarChar(1)
        ) RETURNS TABLE
AS

RETURN
        (
                SELECT CONVERT(int, SUBSTRING(@String+@Delimiter, PK_CountID, CHARINDEX(@Delimiter, @String+@Delimiter, PK_CountID)-PK_CountID)) AS PK_IntID
                FROM dbo.counter
                WHERE PK_CountID >0 AND PK_CountID<LEN(@String)+LEN(@Delimiter) AND SubString(@Delimiter + @String + @Delimiter, PK_CountID, 1)=@Delimiter
        )
GO


‘Array’ version - with position indexer - good for index change scripts where column-order matters:
--Normal VarChar version
CREATE FUNCTION dbo.fn_DelimitToArray

        (
                @String VarChar(8000),
                @Delimiter VarChar(1)
        ) RETURNS TABLE
AS

RETURN
        (
                SELECT
                        PK_CountID - LEN(REPLACE(LEFT(@String, PK_CountID-1), @Delimiter, ‘’)) AS Pos,
                        SUBSTRING(@String+@Delimiter, PK_CountID, CHARINDEX(@Delimiter, @String+@Delimiter, PK_CountID)-PK_CountID) AS Value
                FROM dbo.counter
                WHERE PK_CountID >0 AND PK_CountID<LEN(@String)+LEN(@Delimiter) AND SubString(@Delimiter + @String + @Delimiter, PK_CountID, 1)=@Delimiter
        )
GO

–Integer casting version when output is used to join to integer PK/FK columns.
CREATE FUNCTION dbo.fn_DelimitToIntArray
        (
                @String VarChar(8000),
                @Delimiter VarChar(1)
        ) RETURNS TABLE
AS

RETURN

        (
                SELECT
                        PK_CountID - LEN(REPLACE(LEFT(@String, PK_CountID-1), @Delimiter, ‘’)) AS Pos,
                        CONVERT(int, SUBSTRING(@String+@Delimiter, PK_CountID, CHARINDEX(@Delimiter, @String+@Delimiter, PK_CountID)-PK_CountID)) AS PK_IntID
                FROM dbo.counter
                WHERE PK_CountID >0 AND PK_CountID<LEN(@String)+LEN(@Delimiter) AND SubString(@Delimiter + @String + @Delimiter, PK_CountID, 1)=@Delimiter
        )


As for logical reads on the nums / tally / counter table:

SQL server 2005 can fit 622 numbers per page if it is clustered. That drops to 299 if it is a heap.  SQL Server 2000 can fit 620 numbers per page clustered.

1 I/O per hit guaranteed: 299-number heap (seek or scan; only tested in 2005)
2 I/Os per hit guaranteed (seek or scan): 622 number clustered (620 for 2000)
Fully packed 2-level clustered index for a 2 I/O minimum per seek: 386,884 numbers (384,400 for 2000)

Make sure you use a 100% fill facter (the data shouldn’t ever change), and after populating the tables with data, you do a rebuild:
ALTER INDEX ALL ON Counter REBUILD WITH (FillFactor=100) for SQL Server 2005
DBCC DBREINDEX (Counter,’PK_C_IX__Counter__CountID’,100) for SQL Server 2000

I usually use both a ’small’ version and a ’standard’ version of the table of numbers (counter / nums / tally).  Never needed the ‘big’ version yet - a fully packed 3-level clustered index with 240,641,848 numbers (238,328,000 for SQL2000).
Here is my counter table building script for SQL Server 2005 and 2000; it runs in 4 seconds and allows or having a portion of your numbers being negative.  @MaxPositive and @ClusteredRowsPerPage are the hard-coded controlling parameters.

1-Level, 2-Level, and 3-Level (commented) Counter / Tally / Nums table builder SQL Server 2005:

--*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
--DDL
--*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

SET NOCOUNT ON

--*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

IF EXISTS (SELECT * FROM sys.tables WHERE name='CounterSmall' AND schema_id=1) DROP TABLE dbo.CounterSmall
IF EXISTS (SELECT * FROM sys.tables WHERE name='Counter' AND schema_id=1) DROP TABLE dbo.Counter
--IF EXISTS (SELECT * FROM sys.tables WHERE name='CounterBig' AND schema_id=1) DROP TABLE dbo.CounterBig
GO

--*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

CREATE TABLE dbo.CounterSmall
(

        PK_CountID int NOT NULL,
        CONSTRAINT PK_C_IX__CounterSmall__CountID PRIMARY KEY CLUSTERED (PK_CountID) WITH FILLFACTOR=100
)

CREATE TABLE dbo.Counter
(
        PK_CountID int NOT NULL,
        CONSTRAINT PK_C_IX__Counter__CountID PRIMARY KEY CLUSTERED (PK_CountID) WITH FILLFACTOR=100
)

/*
CREATE TABLE dbo.CounterBig
(
        PK_CountID int NOT NULL,
        CONSTRAINT PK_C_IX__CounterBig__CountID PRIMARY KEY CLUSTERED (PK_CountID) WITH FILLFACTOR=100
)
*/
GO

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
–Counter SQL 2005
–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

DECLARE @Power int
DECLARE @HeapRowsPerPage int
DECLARE @ClusteredRowsPerPage int
DECLARE @MaxRows int
DECLARE @MaxPositive int
DECLARE @MaxNegative int
DECLARE @OldMaxNegative int

SET @ClusteredRowsPerPage=622
SET @HeapRowsPerPage=299
SET @MaxPositive=621

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

SET @MaxRows=@ClusteredRowsPerPage
SET @MaxPositive=@MaxPositive-1
SET @OldMaxNegative=0
SET @MaxNegative=@MaxRows-@MaxPositive-@OldMaxNegative
SET @Power=1

PRINT ‘CounterSmall: ‘ + CONVERT(VarChar(10), @MaxNegative*-1+1) + ‘ to ‘ + CONVERT(VarChar(10), @MaxPositive) + ‘ - ‘ + CONVERT(VarChar(10), @MaxRows) + ‘ Rows - 1-Level Clustered Index’

–SELECT @MaxNegative AS MaxNegative, @MaxPositive AS MaxPositive, @OldMaxNegative AS OldMaxNegative, @Power AS Power, @MaxRows AS MaxRows

TRUNCATE TABLE CounterSmall

BEGIN TRANSACTION

/*
INSERT INTO CounterSmall WITH (TABLOCKX) (PK_CountID)
SELECT PK_CountID-@MaxNegative
FROM dbo.fn_Numbers(@MaxRows)
*/

INSERT INTO CounterSmall WITH (TABLOCKX) (PK_CountID) VALUES (1-@MaxNegative)

WHILE @Power<=@MaxRows
BEGIN
        INSERT INTO CounterSmall WITH (TABLOCKX) (PK_CountID)
        SELECT @Power+PK_CountID FROM CounterSmall
        WHERE @Power+PK_CountID<=@MaxPositive

        SET @Power=@Power*2
END

COMMIT

ALTER INDEX ALL ON CounterSmall REBUILD WITH (FillFactor=100)
UPDATE STATISTICS CounterSmall WITH FULLSCAN
–SELECT * FROM CounterSmall

–*=*=*=*=*=*=*=*=*=*=

SET @Power=@ClusteredRowsPerPage
SET @MaxRows=@Power*@ClusteredRowsPerPage
SET @OldMaxNegative=@MaxNegative+@OldMaxNegative
SET @MaxPositive=(@MaxPositive+1)*@ClusteredRowsPerPage
SET @MaxNegative=@MaxRows-@MaxPositive-@OldMaxNegative

PRINT ‘Counter: ‘ + CONVERT(VarChar(10), @MaxNegative*-1-@OldMaxNegative+1) + ‘ to ‘ + CONVERT(VarChar(10), @MaxPositive) + ‘ - ‘ + CONVERT(VarChar(10), @MaxRows) + ‘ Rows - 2-Level Clustered Index’

–SELECT @MaxNegative AS MaxNegative, @MaxPositive AS MaxPositive, @OldMaxNegative AS OldMaxNegative, @Power AS Power, @MaxRows AS MaxRows

TRUNCATE TABLE Counter

BEGIN TRANSACTION

INSERT INTO Counter WITH (TABLOCKX) (PK_CountID)
SELECT PK_CountID-@MaxNegative FROM CounterSmall

WHILE @Power<=@MaxRows
BEGIN
        INSERT INTO Counter WITH (TABLOCKX) (PK_CountID)
        SELECT @Power+PK_CountID FROM Counter
        WHERE @Power+PK_CountID<=@MaxPositive

        SET @Power=@Power*2
END
COMMIT

ALTER INDEX ALL ON Counter REBUILD WITH (FillFactor=100)
UPDATE STATISTICS Counter WITH FULLSCAN
–SELECT * FROM Counter ORDER BY PK_CountID

–*=*=*=*=*=*=*=*=*=*=
/*
SET @Power=@ClusteredRowsPerPage*@ClusteredRowsPerPage
SET @MaxRows=@Power*(@ClusteredRowsPerPage-2)
SET @OldMaxNegative=@MaxNegative+@OldMaxNegative
SET @MaxPositive=(@MaxPositive+1)*@ClusteredRowsPerPage
SET @MaxNegative=@MaxRows-@MaxPositive-@OldMaxNegative

PRINT ‘CounterBig: ‘ + CONVERT(VarChar(10), @MaxNegative*-1-@OldMaxNegative+1) + ‘ to ‘ + CONVERT(VarChar(10), @MaxPositive) + ‘ - ‘ + CONVERT(VarChar(10), @MaxRows) + ‘ Rows - 3-Level Clustered Index’

–SELECT @MaxNegative AS MaxNegative, @MaxPositive AS MaxPositive, @OldMaxNegative AS OldMaxNegative, @Power AS Power, @MaxRows AS MaxRows

TRUNCATE TABLE CounterBig
UPDATE STATISTICS CounterBig WITH FULLSCAN, NORECOMPUTE

BEGIN TRANSACTION

INSERT INTO CounterBig WITH (TABLOCKX) (PK_CountID)
SELECT PK_CountID-@MaxNegative FROM Counter

WHILE @Power<=@MaxRows
BEGIN
        INSERT INTO CounterBig WITH (TABLOCKX) (PK_CountID)
        SELECT @Power+PK_CountID FROM CounterBig
        WHERE @Power+PK_CountID<=@MaxPositive

        SET @Power=@Power*2
END
COMMIT

ALTER INDEX ALL ON CounterBig REBUILD WITH (FillFactor=100)
UPDATE STATISTICS CounterBig WITH FULLSCAN
–SELECT * FROM CounterBig ORDER BY PK_CountID
*/

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

SELECT * FROM sys.dm_db_index_physical_stats (DB_ID(), OBJECT_ID(’CounterSmall’), NULL, NULL, ‘DETAILED’)
SELECT * FROM sys.dm_db_index_physical_stats (DB_ID(), OBJECT_ID(’Counter’), NULL, NULL, ‘DETAILED’)
–SELECT * FROM sys.dm_db_index_physical_stats (DB_ID(), OBJECT_ID(’CounterBig’), NULL, NULL, ‘DETAILED’)

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
GO


1-Level, 2-Level, and 3-Level (commented) Counter / Tally / Nums table builder SQL Server 2000:
--*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
--DDL
--*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

SET NOCOUNT ON

--*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

IF EXISTS (SELECT * FROM sysobjects WHERE name='CounterSmall' AND uid=1 AND xtype='u') DROP TABLE dbo.CounterSmall
IF EXISTS (SELECT * FROM sysobjects WHERE name='Counter' AND uid=1 AND xtype='u') DROP TABLE dbo.Counter
--IF EXISTS (SELECT * FROM sysobjects WHERE name='CounterBig' AND uid=1 AND xtype='u') DROP TABLE dbo.CounterBig

--*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

CREATE TABLE dbo.CounterSmall
(
        PK_CountID int NOT NULL,
        CONSTRAINT PK_C_IX__CounterSmall__CountID PRIMARY KEY CLUSTERED (PK_CountID) WITH FILLFACTOR=100
)

CREATE TABLE dbo.Counter
(
        PK_CountID int NOT NULL,
        CONSTRAINT PK_C_IX__Counter__CountID PRIMARY KEY CLUSTERED (PK_CountID) WITH FILLFACTOR=100
)

/*
CREATE TABLE dbo.CounterBig
(
        PK_CountID int NOT NULL,
        CONSTRAINT PK_C_IX__CounterBig__CountID PRIMARY KEY CLUSTERED (PK_CountID) WITH FILLFACTOR=100
)
*/

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
–Counter SQL 2000
–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

DECLARE @Power int
DECLARE @HeapRowsPerPage int
DECLARE @ClusteredRowsPerPage int
DECLARE @MaxRows int
DECLARE @MaxPositive int
DECLARE @MaxNegative int
DECLARE @OldMaxNegative int

SET @ClusteredRowsPerPage=620
SET @HeapRowsPerPage=299
SET @MaxPositive=619

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

SET @MaxRows=@ClusteredRowsPerPage
SET @MaxPositive=@MaxPositive-1
SET @OldMaxNegative=0
SET @MaxNegative=@MaxRows-@MaxPositive-@OldMaxNegative
SET @Power=1

PRINT ‘CounterSmall: ‘ + CONVERT(VarChar(10), @MaxNegative*-1+1) + ‘ to ‘ + CONVERT(VarChar(10), @MaxPositive) + ‘ - ‘ + CONVERT(VarChar(10), @MaxRows) + ‘ Rows - 1-Level Clustered Index’

–SELECT @MaxNegative AS MaxNegative, @MaxPositive AS MaxPositive, @OldMaxNegative AS OldMaxNegative, @Power AS Power, @MaxRows AS MaxRows

TRUNCATE TABLE CounterSmall

BEGIN TRANSACTION

INSERT INTO CounterSmall WITH (TABLOCKX) (PK_CountID) VALUES (1-@MaxNegative)

WHILE @Power<=@MaxRows
BEGIN
        INSERT INTO CounterSmall WITH (TABLOCKX) (PK_CountID)

        SELECT @Power+PK_CountID FROM CounterSmall
        WHERE @Power+PK_CountID<=@MaxPositive

        SET @Power=@Power*2
END

COMMIT

DBCC DBREINDEX (CounterSmall,’PK_C_IX__CounterSmall__CountID’,100)
UPDATE STATISTICS CounterSmall WITH FULLSCAN
–SELECT * FROM CounterSmall

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

SET @Power=@ClusteredRowsPerPage
SET @MaxRows=@Power*@ClusteredRowsPerPage
SET @OldMaxNegative=@MaxNegative+@OldMaxNegative
SET @MaxPositive=(@MaxPositive+1)*@ClusteredRowsPerPage
SET @MaxNegative=@MaxRows-@MaxPositive-@OldMaxNegative

PRINT ‘Counter: ‘ + CONVERT(VarChar(10), @MaxNegative*-1-@OldMaxNegative+1) + ‘ to ‘ + CONVERT(VarChar(10), @MaxPositive) + ‘ - ‘ + CONVERT(VarChar(10), @MaxRows) + ‘ Rows - 2-Level Clustered Index’

–SELECT @MaxNegative AS MaxNegative, @MaxPositive AS MaxPositive, @OldMaxNegative AS OldMaxNegative, @Power AS Power, @MaxRows AS MaxRows

TRUNCATE TABLE Counter

BEGIN TRANSACTION

INSERT INTO Counter WITH (TABLOCKX) (PK_CountID)
SELECT PK_CountID-@MaxNegative FROM CounterSmall

WHILE @Power<=@MaxRows
BEGIN
        INSERT INTO Counter WITH (TABLOCKX) (PK_CountID)
        SELECT @Power+PK_CountID FROM Counter
        WHERE @Power+PK_CountID<=@MaxPositive

        SET @Power=@Power*2
END
COMMIT

DBCC DBREINDEX (Counter,’PK_C_IX__Counter__CountID’,100)
UPDATE STATISTICS Counter WITH FULLSCAN
–SELECT * FROM Counter ORDER BY PK_CountID

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

/*
SET @Power=@ClusteredRowsPerPage*@ClusteredRowsPerPage
SET @MaxRows=@Power*(@ClusteredRowsPerPage-2)
SET @OldMaxNegative=@MaxNegative+@OldMaxNegative
SET @MaxPositive=(@MaxPositive+1)*@ClusteredRowsPerPage
SET @MaxNegative=@MaxRows-@MaxPositive-@OldMaxNegative

PRINT ‘CounterBig: ‘ + CONVERT(VarChar(10), @MaxNegative*-1-@OldMaxNegative+1) + ‘ to ‘ + CONVERT(VarChar(10), @MaxPositive) + ‘ - ‘ + CONVERT(VarChar(10), @MaxRows) + ‘ Rows - 3-Level Clustered Index’

–SELECT @MaxNegative AS MaxNegative, @MaxPositive AS MaxPositive, @OldMaxNegative AS OldMaxNegative, @Power AS Power, @MaxRows AS MaxRows

TRUNCATE TABLE CounterBig
UPDATE STATISTICS CounterBig WITH FULLSCAN, NORECOMPUTE

BEGIN TRANSACTION

INSERT INTO CounterBig WITH (TABLOCKX) (PK_CountID)
SELECT PK_CountID-@MaxNegative FROM Counter

WHILE @Power<=@MaxRows
BEGIN
        INSERT INTO CounterBig WITH (TABLOCKX) (PK_CountID)
        SELECT @Power+PK_CountID FROM CounterBig
        WHERE @Power+PK_CountID<=@MaxPositive

        SET @Power=@Power*2
END
COMMIT

DBCC DBREINDEX (Counter,’PK_C_IX__CounterBig__CountID’,100)
UPDATE STATISTICS CounterBig WITH FULLSCAN
–SELECT * FROM CounterBig ORDER BY PK_CountID
*/

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

DBCC SHOWCONTIG (CounterSmall) WITH ALL_LEVELS, TABLERESULTS
DBCC SHOWCONTIG (Counter) WITH ALL_LEVELS, TABLERESULTS
–DBCC SHOWCONTIG (CounterBig) WITH ALL_LEVELS, TABLERESULTS

–*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=


I have big versions and two-column versions as well, but the post is already too big.  The big version gracefully can handle more than hundreds of thousands of characters because it splices into 8000 character blocks.  More code, no longer an inline table-valued function (inline table-valued functions are processed as derived tables / views behind the scenes and are much faster), but it is faster than VarChar(max) and works in SQL Server 2000 (if the string input is text instead of VarChar(max)) and never uses more than 8000 numbers.

I have had other uses for a table of numbers, particularly reporting involving date-ranges and you want to show a date-range-block even if there is no data with a date within that date-range block.

Google
 
Web www.greatinca.net