Av rating:
Total votes: 40
Total comments: 27


Robyn Page and Phil Factor
The Helper Table Workbench
16 March 2007


Sometimes, when writing TSQL code in functions or procedures, it
is tempting to do iterations, or even worse, a cursor, when it isn't
really necessary. Cursors and iterations are both renowned for slowing
down Transact SQL Code SQL Server just isn't designed for it.

However, there is usually a way to do such operations in a set-based
way. If you do so, then your routines will run a lot faster, with speed
at least doubling. There are a lot of tricks to turning a problem that
seems to require an iterative approach into a set-based operation, and
we wish we could claim we'd invented one of them. Probably the most
useful technique involves that apparently useless entity, the 'helper'
table. This workshop will concentrate on this, because it is probably
the most widely used.

The most common Helper table you'll see is a table with nothing but the
numbers in a sequence from 1 upwards. These tables have a surprising
number of uses. Once you've understood the principles behind helper
tables, then you'll think of many more. We'll be providing several
examples where a helper table suddenly makes life easier. The objective
is to show the principles so that you'll try out something similar the
next time you have to tackle a tricky operation in TSQL.

As always, you're encouraged to load the example script into Query
Analyser or Management Studio, and experiment!

Our examples include:

Splitting Strings into table-rows, based on a specified delimiter
Encoding and decoding a string
Substituting values into a string
Extracting individual words from a string into a table
Extracting all the numbers in a string into a table
Removing all text between delimiters
Scrabble score
Moving averages
Getting the 'Week beginning' date in a table
Calculating the number of working days between dates.

Note. These examples use VARCHAR(8000) just so they compile on both
SQL Server 2000 and 2005. If you are using SQL Server 2005, you'll
probably want to change them to VARCHAR(MAX)

Before we start, we'll need a helper table of numbers. Our examples
aren't going to require high numbers, but we've parameterised the size
of the table that the routine creates

Creating the helper table
-------------------------

Here is a routine that checks to see if such a 'helper' table called
'numbers' exists, and, if not, creates it*/

CREATE PROCEDURE spMaybeBuildNumberTable
@size INT=10000
AS
BEGIN
SET
NOCOUNT ON
IF
NOT EXISTS (SELECT * FROM dbo.sysobjects
 
WHERE id = OBJECT_ID(N'[dbo].[Numbers]')
   AND
OBJECTPROPERTY(id, N'IsUserTable') = 1)
   
BEGIN
    CREATE TABLE
[dbo].[Numbers](
     
[number] [int],
    
CONSTRAINT [Index_Numbers] PRIMARY KEY CLUSTERED
   
(
     
[number] ASC
   
) ON [PRIMARY]
   
) ON [PRIMARY]

   
DECLARE @ii INT
    SELECT
@ii=1
   
WHILE (@ii<=@size)
     
BEGIN
     INSERT INTO
NUMBERS(NUMBER) SELECT @II
     
SELECT @II=@II+1
     
END
    END
END
/*
Once you have one of these tables, which we've seen described as the
Transact SQL developers 'Swiss Army Knife', you will not want to be
without it.

Splitting Strings into table-rows, based on a specified delimiter
-----------------------------------------------------------------

Imagine you have a string which you want to break into words to make
into a table*/

DECLARE @ordinal VARCHAR(255)
SELECT @Ordinal=
'first second third fourth fifth sixth seventh eighth ninth tenth'

/*
This can be done very simply and quickly through the following single
SQL Select statement:
(make sure you have executed the spMaybeBuildNumberTable procedure
first!)*/

SELECT SUBSTRING(@Ordinal+' ', number,
   
CHARINDEX(' ', @Ordinal+' ', number) - number)
FROM Numbers
WHERE number <= LEN(@Ordinal)
AND
SUBSTRING(' ' + @Ordinal,
           
number1) = ' '
ORDER BY number RETURN

/*----with the result
first
second
third
fourth
fifth
sixth
seventh
eighth
nineth
tenth

(10 row(s) affected)


You can then enshrine this principle into a table function that will
take any delimiter to split a string. (we believe that the credit for
this clever routine should go to Anith Sen)*/

CREATE FUNCTION [dbo].[uftSplitString]
(
@String VARCHAR(8000),
@Delimiter VARCHAR(255)
)
RETURNS
@Results TABLE
(
SeqNo INT IDENTITY(1, 1),
Item VARCHAR(8000)
)
AS
BEGIN
INSERT INTO
@Results (Item)
SELECT SUBSTRING(@String+@Delimiter, number,
    
CHARINDEX(@Delimiter, @String+@Delimiter, number) - number)
FROM Numbers
WHERE number <= LEN(REPLACE(@String,' ','|'))
AND
SUBSTRING(@Delimiter + @String,
           
number,
           
LEN(REPLACE(@delimiter,' ','|'))) = @Delimiter
ORDER BY number RETURN
END

/*This is the fastest means I have come across in TSQL to split a
string into its components as rows.
Try this, which give you a table with the integer and the nominal name
*/
SELECT * FROM dbo.uftSplitString(
   
'one,two,three,four,five,six,seven,eight,nine,ten',',')
--or this, which
SELECT * FROM dbo.uftSplitString(
   
'Monday--Tuesday--Wednesday--thursday--friday--saturday--sunday','--')

/*
Encoding and decoding a string
------------------------------
You can use the same principle for a lot of string operations.

Here is one that will URLencode a string so it can be used in a POST
or GET HTTP operation. The string is converted into a table with one
character per row and, after being operated on, it is re-assembled.
*/
CREATE FUNCTION ufsURLEncoded
(
@String VARCHAR(MAX)
)
RETURNS VARCHAR(MAX)
BEGIN
DECLARE
@URLEncodedString VARCHAR(MAX)
SELECT @URLEncodedString=''

SELECT @URLEncodedString=@URLEncodedString+
   
CASE WHEN theChar LIKE '[A-Za-z0-9()''*-._!]'
   
THEN theChar
   
ELSE '%'
           
+ SUBSTRING ('0123456789ABCDEF',
            (
ASCII(theChar) / 16)+1,1)
            + 
SUBSTRING ('0123456789ABCDEF',
            (
ASCII(theChar) % 16)+1,1)
   
END
FROM
   
(
   
SELECT [theChar]=SUBSTRING(@string,number,1)
   
FROM numbers
    
WHERE number <= LEN(@String) ) Characterarray
   
-- Return the result of the function
   
RETURN @URLEncodedString

END
/*

This sort of routine is a lot less complex than the iterative methods
and is, as one would expect, a lot faster. Just to show that this is
no fluke, here is the reverse function to decode a URL Query string
*/
CREATE FUNCTION ufsURLDecoded
(
@String VARCHAR(MAX)
)
RETURNS VARCHAR(MAX)
BEGIN
SELECT
@string=
   
REPLACE(@string,escapeString, TheCharacter)
FROM
   
(SELECT
       
[escapeString]=SUBSTRING(@string,number,3),
       
[theCharacter]=CHAR(
                (
CHARINDEX(
                   
SUBSTRING(@string,number+1,
                   
1),
               
'0123456789ABCDEF')-1)*16
               
+CHARINDEX(
                   
SUBSTRING(@string,number+2,
                   
1),
               
'0123456789ABCDEF')-1)

   
FROM numbers
    
WHERE number <= LEN(@String)
        AND
SUBSTRING(@string,number,1) = '%'
   
)f
WHERE CHARINDEX(escapeString,@string)>0
RETURN @String
END

/*

--here is a simple function that acts as an int to hex converter
CREATE FUNCTION ufsIntToHex
(
   
@integer INT
)
RETURNS VARCHAR(20)
AS
BEGIN
DECLARE
@result VARCHAR(80)
SELECT @result=''

IF @integer>0
   
SELECT @result=CASE WHEN @integer>0 THEN
           
SUBSTRING('0123456789ABCDEF',(@integer%16)+1,1)
           
ELSE '' END+@result,
     
@integer=@integer/16
   
FROM numbers WHERE number<20
ELSE SELECT @result=0
RETURN '0x'+@result

END
--it works this way
SELECT dbo.ufsIntToHex(1024)

--and what about TSQL that converts hex to int?
CREATE FUNCTION ufiHexToInt
(
   
@HexString VARCHAR(50)
)
RETURNS INT
AS
BEGIN
DECLARE
@result INT
SELECT
@result=0
SELECT @Result=(@result*16)+
    (
CHARINDEX(SUBSTRING(@HexString,number,1),'0123456789ABCDEF')-1)
   
FROM numbers WHERE number<=LEN(@HexString)
    AND
SUBSTRING(@HexString,number,1) LIKE '[0-9ABCDEF]'
RETURN @Result

END
--and now we can test it out
SELECT COUNT(*)
   
FROM numbers
WHERE dbo.ufiHexToInt(dbo.ufsIntToHex(number))<>number

Substituting values into a string
---------------------------------

Next, we have a function that uses these principles to do macro
substitution of values into a string. It will work for replacing
XHTML placeholders with a value, or producing error messages in
a variety of languages. In fact, uses keep popping up for this
sort of function. In this version, one can specify what strings
are used for substitutions (the default is %1, %2, %3 etc,) and
what you use as the delimiter of your list of macros and values.
*/
CREATE FUNCTION [dbo].[ufsSubstitute]
(
@Template VARCHAR(8000),
@macroList VARCHAR(8000),
@valueList VARCHAR(8000),
@Delimiter VARCHAR(255)
)
RETURNS
VARCHAR(8000)
AS
BEGIN
DECLARE
@macros TABLE (MyID INT IDENTITY(1,1),variable VARCHAR(80))
DECLARE @values TABLE (MyID INT IDENTITY(1,1),[value] VARCHAR(8000))
--get all the variables
INSERT INTO @macros (variable)
   
SELECT SUBSTRING(@MacroList+@Delimiter, number,
       
CHARINDEX(@Delimiter, @MacroList+@Delimiter, number) - number)
FROM Numbers
WHERE number <= LEN(@MacroList)
    AND
SUBSTRING(@Delimiter + @MacroList, number, LEN(@delimiter))
       
= @Delimiter
ORDER BY number

INSERT INTO @values ([value]
    
SELECT SUBSTRING(@ValueList+@Delimiter, number,
       
CHARINDEX(@Delimiter, @ValueList+@Delimiter, number) - number)
FROM Numbers
WHERE number <= LEN(@ValueList)
AND
SUBSTRING(@Delimiter + @ValueList, number,
        
LEN(@delimiter)) = @Delimiter
ORDER BY number

SELECT @Template=
   
REPLACE(@Template,COALESCE(variable,
               
'%'+CAST(v.MyID AS VARCHAR)),
               
[value])
FROM @values v
LEFT OUTER JOIN @macros m ON v.MyID=m.MyID
    
WHERE CHARINDEX(COALESCE(variable,'%'+CAST(v.MyID AS VARCHAR))
        ,
@Template)>0

RETURN (@Template)

END

/*
there are several ways that we can use this routine in practical
applications. Try out these and see what happens!
*/
SELECT dbo.ufsSubstitute (NULL,NULL,'',',')
SELECT dbo.ufsSubstitute ('','','',',')
SELECT dbo.ufsSubstitute (
   
' views','','6',',')
SELECT dbo.ufsSubstitute ('
Dear $1 $2,
It has come to our attention that your $3 account is $4
to the extent of £$5.
Please phone our adviser, $6 $7 on $8 who will inform you of
the various actions that need to be taken'
,
   
'$1,$2,$3,$4,$5,$6,$7,$8',
   
'Mrs,Prism,current,overdrawn,5678,Mr,Grabbitas,04585 725938',
   
',')
SELECT dbo.ufsSubstitute ('To @Destination;
Report dated @Date
The @table table is now @rows long. please @action'
,'@Destination|@Date|@Table|@rows|@action',
'Phil Factor|12 Apr 2007|Log|1273980|truncate it at once','|')
SELECT dbo.ufsSubstitute (
'I thought that your present of a %1 was %2. Thank you very much.
The %1 will come in handy for %3'
   
,''
   
,'trowel|absolutely wonderful|gardening','|')
/*
Extracting individual words from a string into a table
------------------------------------------------------

One can do rather cleverer things than this. For example, one can
extract all the words from a string into a table, a row for each
word.
*/


CREATE FUNCTION [dbo].[uftWords]
(
@String VARCHAR(8000)
)
RETURNS
@Results TABLE
(
SeqNo INT IDENTITY(1, 1),
Word VARCHAR(8000)
)
AS
BEGIN
INSERT INTO
@Results(word)
   
SELECT [word]=LEFT(RIGHT(@string,number),
       
PATINDEX('%[^a-z]%',RIGHT(@string,number)+' ')-1)
FROM Numbers
WHERE number <= LEN(@String)
AND
PATINDEX('%[a-z]%',RIGHT(@string,number))=1
AND PATINDEX('%[^a-z]%',RIGHT(' '+@string,number+1))=1
ORDER BY number DESC
RETURN
END
--and you can get the words (we use it for inversion indexes)
SELECT * FROM dbo.uftWords ('One can do rather cleverer
things than this. <>!  For example, one can extract all the
words from a string into a table, a row for each word.'
)
--or a word count
SELECT COUNT(*) FROM dbo.uftWords ('It is extraordinary
how easy   it is to get a wordcount using this '
)


/*
Extracting all the numbers in a string into a table
----------------------------------------------------

Even more useful than this is a function that picks out all the
numbers from a string into a table. You can therefore easily pick
out the third or fourth string simply, because the table has the
order as well as the number itself. Were it not for the unary minus
operator, this would have been a delightfully simple function.

If you are using this routine, you'll want to cast these numbers into
the number type of your choice. We supply them as strings
*/

CREATE FUNCTION [dbo].[uftNumbers]
(
@String VARCHAR(8000)
)
RETURNS
@Results TABLE
(
SeqNo INT IDENTITY(1, 1),
number VARCHAR(100)
)
AS
BEGIN
INSERT INTO
@Results(number)
   
SELECT
   
CASE LEFT(RIGHT(' '+@String,number),1)
       
WHEN '-' THEN '-' ELSE '' END+
   
SUBSTRING( RIGHT(@String,number-1),1,
       
PATINDEX('%[^0-9.]%',
       
RIGHT(' '+@String,number-1)+' ')-1)
   
FROM Numbers
   
WHERE number <= LEN(REPLACE(@String,' ','!'))+1
       
AND PATINDEX('%[^0-9.][0-9]%',RIGHT(' '
       
+@String,number))=1
   
ORDER BY number DESC
RETURN
END
--So we try out a few examples just to see. It removes anything
--that doesn't look kile a number
SELECT * FROM dbo.uftNumbers('there are numbers like 34.56,
-56, 67.878, maybe34; possibly56, and a few others like <789023>'
)

SELECT * FROM dbo.uftNumbers('23,87986,56.78,67,09,23,30')
SELECT * FROM dbo.uftNumbers('')
SELECT * FROM dbo.uftNumbers('

')
SELECT * FROM dbo.uftNumbers('there are numbers like 34.56,
-56, 67.878, maybe34; possibly56, and a few others like <789023>'
)

/*
SeqNo    number
----------- ------------
1      34.56   
2      -56    
3      67.878   
4      34     
5      56     
6      789023   

(6 row(s) affected)
*/

/*
Removing all text between delimiters
------------------------------------

This is a handy little routine for looking at the strings in HTML code,
but seems to earn its keep in a lot of other ways. You specify the
opening and closing delimiter. At the moment, only single-character
delimiters are allowed. Can anyone re-write it to allow multi-character
delimiters?*/

CREATE FUNCTION [dbo].[ufsRemoveDelimited]
(
@String VARCHAR(8000),
@OpeningDelimiter CHAR(1),
@ClosingDelimiter CHAR(1)
)
RETURNS
VARCHAR(8000)
AS
BEGIN
DECLARE
@newString VARCHAR(8000)
IF @OpeningDelimiter = @ClosingDelimiter
   
BEGIN
    RETURN
NULL
   
END
IF
@OpeningDelimiter+@ClosingDelimiter+@String IS NULL
   
BEGIN
    RETURN
@String
   
END
SELECT
@NewString=''
SELECT @newString =@newString +SUBSTRING(@String,number,1)
   
FROM numbers
   
WHERE number<=LEN (REPLACE(@string,' ','|'))
    AND
    
CHARINDEX (@OpeningDelimiter,@string+@OpeningDelimiter,number)
        <
    
CHARINDEX (@ClosingDelimiter,@string+' '+@closingDelimiter,number)
AND
number <> CHARINDEX (@OpeningDelimiter,@string,number)
RETURN @NewString
END

--so we can try it out with brackets
SELECT dbo.ufsRemoveDelimited(
   
'this will appear(but not this),)will this?(','(',')')
--or if you want to take out tags
SELECT dbo.ufsRemoveDelimited(
   
'this will appear
and this
and this'
,'<','>')
--or this
SELECT dbo.ufsRemoveDelimited(
'<?xml version="1.0" encoding="us-ascii"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>A poem</title>
</head>
<body>
    <div style="float left: width:300px;">
        <h2>
           Weather forecast
        </h2>
        <p>
            The rain it raineth every day<br />
            upon the just and unjust fellah<br />
            but mainly on the just, because,<br />
            the unjust pinched the just''s umbrella<br />
        </p>
    </div>
</body>
</html>

','<','>')

/*
Scrabble Score
--------------

And as a slightly silly example of the sort of chore that crops up
occasionally when analysing strings, character by character,
here is a way of scoring a string for Scrabble, assuming it is all
on ordinary squares!*/

CREATE FUNCTION ufiScrabbleScore
(
   
@String VARCHAR(100)
)
RETURNS INT
AS
BEGIN
DECLARE
@Total INT
SELECT
@total=0

SELECT @Total=@total+
CASE WHEN SUBSTRING(@String,number,1) NOT LIKE '[a-z]' THEN NULL
   
ELSE
CAST(SUBSTRING('00000000001122223333347799',
   
CHARINDEX(SUBSTRING(@String,number,1),
   
'EAIONRTLSUDGBCMPFHVWYKJXQZ')
            ,
1) AS INT)+1 END
FROM
numbers WHERE number <=LEN(@String)
RETURN @Total
END

-- and now we try it out!
SELECT dbo.ufiScrabbleScore('Quiz'--22
SELECT dbo.ufiScrabbleScore('Robyn') --10

/* Now we find out which are the highest scorers, assuming an
illegal quantity of tiles. We use the WordList from Phil's Blog
on the Fireside Fun of Decapitations.

Don't try running this without the WordList!*/

SELECT dbo.ufiScrabbleScore(word),word
FROM wordlist ORDER BY dbo.ufiScrabbleScore(word) DESC /*
49     razzamatazz
48     razzmatazz
45     pizzazz
43     quizzically
39     squeezeboxes
38     quizzical
38     psychoanalyzing
37     psychoanalyzed
37     squeezebox
..etc....

As well as slicing and dicing strings, once one has one's helper table,
suddenly time-interval based reporting becomes much easier.

Moving averages
---------------
How about moving averages? Here is a simple Select statement that gives
you the moving average (over a week) of the number of log entries for
every day for a year. This can be adapted to give weighted and
exponential moving averages over arbitrary time periods. You use this
technique for ironing out 'noise' from a graph in order to accentuate
the inderlying trend

To execute this, you will need a table to try it on
*/
DROP TABLE #cb
CREATE TABLE #cb (insertionDate datetime)--quantity
--for once, we need to iterate to shuffle the pack
DECLARE @ii INT
SELECT
@ii=0
WHILE @ii<20000
   
BEGIN
    INSERT INTO
#cb(insertionDate)
           
SELECT DATEADD(Hour,RAND()*8760,'1 jan 2006')
   
SELECT @ii=@ii+1
   
END
--and put an index on it
CREATE CLUSTERED INDEX idxInsertionDate ON #cb(insertionDate)

/* now we can try out a moving average!
*/

SELECT start,[running average]=COUNT(*)/7
   
FROM
   
(
   
SELECT [order]=number,
           
[start]=DATEADD(DAY,number,'1 Jan 2006'),
           
[end]=DATEADD(DAY,number+7,'1 Jan 2006')
   
FROM numbers
   
WHERE DATEADD(DAY,number,'1 Jan 2006')
        BETWEEN
'1 Jan 2006' AND '1 Jan 2007')f
   
LEFT OUTER JOIN [#cb]
   
ON [#cb].insertionDate BETWEEN f.start AND f.[end]
   
GROUP BY start
   
ORDER BY start
/*

Getting the 'Week beginning' date in a table
---------------------------------------------

Here is a UDF that lists all the Mondays (or whatever you want)
between two dates*/

CREATE FUNCTION uftDatesOfWeekday
(
   
@Weekday VARCHAR(10),  
   
@StartDate datetime,
   
@EndDate DateTime
)
RETURNS TABLE
AS
RETURN
(
SELECT
           
[start]=DATEADD(DAY,number-1,@StartDate)
   
FROM numbers
   
WHERE DATEADD(DAY,number-1,@StartDate)< @EndDate
   
AND DATENAME(dw,DATEADD(DAY,number-1,@StartDate))=@Weekday
)
/*
And you can try it out by finding how many mondays there are between
the first of January and 1st june this year. */
SELECT * FROM dbo.uftDatesOfWeekday('monday','1 Jan 2007','1 Jun 2007')
/*


Number of Working Days between two dates
----------------------------------------

Or, how about a UDF that tells you the number of working days between
two dates? (you can alter it if Saturday and Sunday are not your days
off!)
*/

CREATE FUNCTION ufiWorkingDays
(
   
@StartDate datetime,
   
@EndDate DateTime
)
RETURNS INT
AS
BEGIN
RETURN
(SELECT COUNT(*)
   
FROM numbers
   
WHERE DATEADD(DAY,number-1,@StartDate)< @EndDate
   
AND DATENAME(dw,DATEADD(DAY,number-1,@StartDate))
    NOT
IN ('saturday','sunday'))
END

---So how many working days until christmas?
SELECT dbo.ufiWorkingDays(
       
GETDATE(),'25 Dec '+DATENAME(YEAR,GETDATE()))

/* We can go on for ever with example of using a numeric Helper table
but we won't because most of you will have wandered off even before you
reach this point. We hope that you'll now take over and create some
more examples, try them out against iterative solutions that do the same
thing. We guarantee you'll be pleased with the result!*/

See also other Workbenches at Simple-Talk

Robyn Page's SQL Server DATE/TIME Workbench

Robyn Page

Date calculation and formatting in SQL Server can be surprisingly tricky. Robyn Page's "hands-on" workbench will lead you through the minefield.

Robyn Page's SQL Server String Manipulation Workbench

Robyn Page

String searching and manipulation in SQL Server can be error-prone and tedious...unless you're armed with the techniques described in Robyn's string manipulation workbench...

SQL Server Error Handling Workbench

Grant Fritchey

Grant Fritchey steps into the workbench arena, with an example-fuelled examination of catching and gracefully handling errors in SQL 2000 and 2005, including worked examples of the new TRY..CATCH capabilities.

Robyn Page's SQL Server Cursor Workbench

Robyn Page

The topic of cursors is the ultimate "hot potato" in the world of SQL Server. Everyone has a view on when they should and mainly should not be used. By example and testing Robyn Page proves that, when handled with care, cursors are not necessarily a "bad thing".

Robyn Page's SQL Server Data Validation Workbench

Robyn Page

Robyn Page provides essential techniques for ensuring the validity of the data being entered into your SQL Server tables.

Robyn Page's Excel Workbench

Robyn Page and Phil Factor

The need to produce Excel reports from SQL Server is very common. Here, Robyn Page and Phil Factor present practical techniques for creating and manipulating Excel spreadsheets from SQL Server, using linked servers and T-SQL. The pièce de résistance is a stored procedure that uses OLE Automation...

Robyn Page's SQL Server Security Workbench

Robyn Page and Phil Factor

Robyn Page and Phil Factor present practical T-SQL techniques for controlling access to sensitive information within the database, and preventing malicious SQL injection attacks.



This article has been viewed 17700 times.
Robyn Page and Phil Factor

Author profile: Robyn Page and Phil Factor

Robyn Page is a consultant with Enformatica and USP Networks. She is also a well known actress, being most famous for her role as Katie Williams, barmaid in the Television Series Family Affairs.

Phil Factor (real name withheld to protect the guilty), aka Database Mole, has 20 years of experience with database-intensive applications. Despite having once been shouted at by a furious Bill Gates at an exhibition in the early 1980s, he has remained resolutely anonymous throughout his career.

Search for other articles by Robyn Page and Phil Factor

Rate this article:   Avg rating: from a total of 40 votes.


Poor

OK

Good

Great

Must read
 
Have Your Say
Do you have an opinion on this article? Then add your comment below:
You must be logged in to post to this forum

Click here to log in.


Subject: Perfect timing!
Posted by: Anonymous (not signed in)
Posted on: Monday, March 19, 2007 at 10:43 AM
Message: I'd used this technique for building calendars, but never thought of using it to split strings - I have to do this a lot from HTML-form posted data and this is going to make some of the SQL so much neater and faster/

Thank you.

Subject: Re: Perfect Timing
Posted by: Robyn Page (view profile)
Posted on: Monday, March 19, 2007 at 2:33 PM
Message: Yes, we got into this because all our existing string-parsing functions proved to be too slow dealing with the sort of XHTML Website work we were dealing with, and we did some timings......
Phil and I are pleased that you find it useful.

Subject: Perfect Timing
Posted by: Anonymous (not signed in)
Posted on: Tuesday, March 20, 2007 at 9:13 AM
Message: Now if I can just find the reverse without using cursors then I'll be even happier!

Subject: Re Perfect timing
Posted by: Phil Factor (view profile)
Posted on: Tuesday, March 20, 2007 at 5:21 PM
Message:
SELECT REVERSE('reippah eb won lliw eh yllufepoh')

Subject: Great Stuff
Posted by: Anonymous (not signed in)
Posted on: Tuesday, March 20, 2007 at 8:34 PM
Message: This is really interesting stuff that will help make some reporting tasks I'm about to delve into a lot easier/cleaner. Thanks so much!

FYI, found a typo here:

sellect SUBSTRING(@ValueList+@Delimiter, number,

(should be "select")

Thanks again!

Subject: Create numbers table without loop
Posted by: Anonymous (not signed in)
Posted on: Tuesday, March 20, 2007 at 10:25 PM
Message: I am a huge fan of helper tables. I have learned some new ways to use a numbers table so thank you for that.

However, what if you didn't use a loop to create the numbers table. This is the method I use. You need a source of records and if you don't have enough just cross join until you do.

Like this..

insert Numbers
select top 10000 identity(int, 0, 1)
into Numbers
from master.information_schema.columns
cross join master.information_schema.columns

Subject: Trivia / Humour
Posted by: Anonymous (not signed in)
Posted on: Wednesday, March 21, 2007 at 12:31 AM
Message: Interesting article, i would say very authentic also, especially coming from 'page' and 'Fill factor'


Subject: Re: Trivia/Humour
Posted by: Phil Factor (view profile)
Posted on: Wednesday, March 21, 2007 at 2:50 AM
Message: Like the old card game 'Happy Families', eh? It is absolutely true that there were two eminent Psychiatrists once, called Dr Batty and Dr Nutter. I admit that Phil Factor is a pseudonym, but 'Page' is coincidence.

Subject: Set-based Numbers Table
Posted by: Anonymous (not signed in)
Posted on: Wednesday, March 21, 2007 at 9:00 AM
Message: Use a set-based solution to create your Numbers table on SQL 2000 or SQL 2005:

SELECT TOP 10000 Num = IDENTITY(INT, 1, 1)
INTO Numbers
FROM INFORMATION_SCHEMA.COLUMNS a
CROSS JOIN INFORMATION_SCHEMA.COLUMNS b

ALTER TABLE Numbers
ADD CONSTRAINT PK_Numbers
PRIMARY KEY CLUSTERED (Num)

Subject: Working Days Range
Posted by: GSquared (view profile)
Posted on: Wednesday, March 21, 2007 at 9:29 AM
Message: On the working days range, I also have a table of holidays in my database. With a "Where not in ..." statement run against that table, you can get even more accuracy.

I originally tried having a function that would calculate holidays based on the rules for them (2nd Monday in month, etc.), but found the performance on it was far too poor. Would have been easier to maintain, and I only have 100 years worth of holidays in the table, but for performance reasons, I had to precalculate and store them in a table.

Of course, the other advantage to a table is non-rule-based dates can be used. Rare, but they have come up.

Subject: String Splitting
Posted by: aaa (view profile)
Posted on: Wednesday, March 21, 2007 at 9:30 AM
Message: A still faster way is to use an inline table-valued function:


alter function rep.fnStringSplit
(
@values varchar(max)
)
returns table
as
return
(
select
Value = substring(@values, N + 1, charindex(',', @values, N + 1) - N - 1)
from Numbers
where N < len(@values)
and substring(@values, N, 1) = ','
and len(substring(@values, N + 1, charindex(',', @values, N + 1) - N - 1)) > 0
)
/*
go
select * from rep.fnStringSplit('1,20,30a,,4000,')
*/
go
select * from rep.fnStringSplit('1,20,30a,,4000,')

Subject: String Splitting Alternative
Posted by: aaa (view profile)
Posted on: Wednesday, March 21, 2007 at 10:11 AM
Message: select *
from xxx
where charindex(',' + cast(id as varchar) + ',', ',1,20,30,,4000,') > 0

Subject: Questions About Routine For Parsing Numbers
Posted by: Anonymous (not signed in)
Posted on: Wednesday, March 21, 2007 at 2:58 PM
Message: First, let me say that the article was eye-opening and conceptually fascinating. I already have a stored procedure that populates a temporary table by parsing a string into integer values (primarily for use when the string is a delimited list of keys), but my code used a while loop to look at each character individually so I was anxious to test drive a new stored procedure based on you example code.

Question 1: Why, in the WHERE clause, do you do a REPLACE inside the LEN parameter (i.e. "LEN(REPLACE(@String,' ','!'))" )? Why not just LEN(@String) ?

Question 2: Why, in the SUBSTRING function in the SELECT clause, do you concatenate a space to the beginning of @String (i.e. "RIGHT(' '+@String,number-1)" ) when number - 1 will never be greater than the length of string and you will never reach that space? Am I missing something obvious?

Subject: re: Questions About Routine For Parsing Numbers
Posted by: Robyn Page (view profile)
Posted on: Wednesday, March 21, 2007 at 4:56 PM
Message: Answer 1: The LEN(REPLACE(@String,' ','!')) was to get around the problem of trailing spaces not being counted in the length of the string when you use the LEN() function. It is not always necessary, but we tend to do it out of habit. In this case I think it probably is necessary, but please by all means prove me wrong. Having been caught out in the past I like to be sure of getting the real length of any string.

Answer 2: This routine gave quite a struggle because of the problem of retaining the unary minus, (e.g. -243) and we may have left in a bit of code that is no longer necessary. Oops!

Subject: Great Article
Posted by: Anonymous (not signed in)
Posted on: Tuesday, March 27, 2007 at 1:18 PM
Message: I used one of the examples today!

Subject: Great Article
Posted by: Anonymous (not signed in)
Posted on: Wednesday, March 28, 2007 at 1:00 PM
Message: Thanks,
but there are some problems with running total
1) the very first day is out of scope
to include we need
[start]=DATEADD(DAY,number-1,'1 Jan 2006'),
the same with end date
2) it includes the next day with 00:00:00
to NOT include we have to do for example like this
[end]=DATEADD(ms, -3,DATEADD(DAY,number+7 - 1,'1 Jan 2006') )

Subject: multi char delimiter
Posted by: Anonymous (not signed in)
Posted on: Wednesday, March 28, 2007 at 1:04 PM
Message:
SET NOCOUNT ON
declare @String VARCHAR(8000)
declare @OpeningDelimiter VARCHAR(10)
declare @ClosingDelimiter VARCHAR(10)

SET @String='<<?xml version="1.0" encoding="us-ascii"?>>
<<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">>
<<html xmlns="http://www.w3.org/1999/xhtml">>
<<head>>
<<title>>A poem<</title>>
<</head>>
<<body>>
<<div style="float left: width:300px;">>
<<h2>>
Weather forecast
<</h2>>
<<p>>
The rain it raineth every day<<br />>
upon the just and unjust fellah<<br />>
but mainly on the just, because,<<br />>
the unjust pinched the just''s umbrella<<br />>
<</p>>
<</div>>
<</body>>
<</html>>
'
SELECT @OpeningDelimiter='<<', @ClosingDelimiter='>>'
DECLARE @numbers table (number int)
DECLARE @Flag bit, @Rez varchar(1000), @poz int, @ii INT
SELECT @Flag=1, @Rez='', @poz=1

SELECT @ii=1 WHILE (@ii<=1000) BEGIN INSERT INTO @NUMBERS(NUMBER) SELECT @II SELECT @II=@II+1 END
SELECT @Rez=@Rez+
CASE
WHEN substring(@String,Number,LEN(@OpeningDelimiter))=@OpeningDelimiter and @Flag=0 then SUBSTRING(@String,@poz,Number-@poz)
WHEN substring(@String,Number,LEN(@ClosingDelimiter))=@ClosingDelimiter and @Flag=1 then ''
WHEN @Flag=1 THEN SUBSTRING(@String,@poz,Number-@poz)
ELSE ''
END,
@poz=CASE
WHEN substring(@String,Number,LEN(@OpeningDelimiter))=@OpeningDelimiter and @Flag=0 then Number
WHEN substring(@String,Number,LEN(@ClosingDelimiter))=@ClosingDelimiter and @Flag=1 then Number+LEN(@ClosingDelimiter)
ELSE CASE @Flag WHEN 1 THEN Number ELSE Number+LEN(@ClosingDelimiter) END
END,
@Flag=CASE
WHEN substring(@String,Number,LEN(@OpeningDelimiter))=@OpeningDelimiter and @Flag=0 then @Flag
WHEN substring(@String,Number,LEN(@ClosingDelimiter))=@ClosingDelimiter and @Flag=1 then @Flag
ELSE CASE @Flag WHEN 1 THEN 0 ELSE 1 END
END
FROM @Numbers WHERE
substring(@String,Number,LEN(@OpeningDelimiter))=@OpeningDelimiter OR
substring(@String,Number,LEN(@ClosingDelimiter))=@ClosingDelimiter
SET @Rez=@Rez+SUBSTRING(@String,@poz,LEN (REPLACE(@string,' ','|')))
PRINT @Rez



DECLARE @newString VARCHAR(8000)
IF @OpeningDelimiter = @ClosingDelimiter
BEGIN PRINT 'NULL' END
IF @OpeningDelimiter+@ClosingDelimiter+@String IS NULL
BEGIN PRINT @String END
SELECT @NewString=''
SELECT @newString =@newString +SUBSTRING(@String,number,1)
FROM @numbers
WHERE number<=LEN (REPLACE(@string,' ','|'))
AND CHARINDEX (@OpeningDelimiter,@string+@OpeningDelimiter,number) <
CHARINDEX (@ClosingDelimiter,@string+' '+@closingDelimiter,number)
AND number <> CHARINDEX (@OpeningDelimiter,@string,number)
PRINT @NewString


Subject: Re: Multi-char delimiter
Posted by: Robyn Page (view profile)
Posted on: Thursday, March 29, 2007 at 4:17 AM
Message: That is really cool. I loved the flag trick! I've altered it slightly to make it a direct replacement for the single-character version. If you can please identify yourself to the editor, a Simple-Talk goodie bag will be yours as a prize! (worth it just for the beer-mat)


ALTER FUNCTION [dbo].[ufsRemoveDelimited] 

(
@String VARCHAR(8000
),
@OpeningDelimiter VARCHAR(20
),
@ClosingDelimiter VARCHAR(20
)
)
RETURNS
VARCHAR(8000
)
AS
BEGIN

DECLARE
@Flag bit, @Rez VARCHAR(1000), @poz INT, @ii INT
,
@lenOpeningD INT, @LenClosingD
INT

SELECT
@Flag=1, @Rez='', @poz=1,@LenOpeningD=LEN(@OpeningDelimiter
),
@LenClosingD=LEN(@ClosingDelimiter
)
IF @OpeningDelimiter=@ClosingDelimiter RETURN
''
SELECT
@Rez=@Rez
+
CASE
WHEN SUBSTRING(@String,Number,@lenOpeningD)=
@OpeningDelimiter
AND @Flag=0 THEN SUBSTRING(@String,@poz,Number-@poz
)
WHEN SUBSTRING(@String,Number,@lenClosingD)=
@ClosingDelimiter
AND @Flag=1 THEN
''
WHEN @Flag=
1
THEN SUBSTRING(@String,@poz,Number-@poz
)
ELSE
''
END
,
@poz=
CASE
WHEN SUBSTRING(@String,Number,@lenOpeningD)=
@OpeningDelimiter
AND @Flag=0 THEN
Number
WHEN SUBSTRING(@String,Number,@lenClosingD)=
@ClosingDelimiter
AND @Flag=1 THEN Number+
@lenClosingD
ELSE CASE @Flag WHEN 1 THEN Number ELSE Number+@lenClosingD
END
END
,
@Flag=
CASE
WHEN SUBSTRING(@String,Number,@lenOpeningD)=
@OpeningDelimiter
AND @Flag=0 THEN
@Flag
WHEN SUBSTRING(@String,Number,@lenClosingD)=
@ClosingDelimiter
AND @Flag=1 THEN
@Flag
ELSE CASE @Flag WHEN 1 THEN 0 ELSE 1
END
END
FROM
Numbers
WHERE
SUBSTRING(@String,Number,@lenOpeningD)=@OpeningDelimiter
OR
SUBSTRING(@String,Number,@lenClosingD)=
@ClosingDelimiter
SET @Rez=@Rez+SUBSTRING(@String,@poz,LEN (REPLACE(@string,' ','|'
)))
RETURN
@Rez
END

Subject: Re:Re: Multi-char delimiter
Posted by: Anonymous (not signed in)
Posted on: Thursday, March 29, 2007 at 7:41 AM
Message: "If you can please identify yourself to the editor, a Simple-Talk goodie bag will be yours as a prize! (worth it just for the beer-mat)"

vasc/sqlservercentral.com

if you can ship to canada :) I could identify myself to editor (real name /adress) a beer-mat would be really nice.




Subject: re: prize
Posted by: Tony Davis (view profile)
Posted on: Thursday, March 29, 2007 at 9:33 AM
Message: Hi there,

We can ship to Canada. If you drop me a mail at editor@simple-talk.com with a mailing address then I can get the swag bag out to you (polo shirt, pen, keyring, beer mat, USB key).

Cheers,

Tony (Simple-Talk Ed.)

Subject: Swag
Posted by: Phil Factor (view profile)
Posted on: Thursday, March 29, 2007 at 10:44 AM
Message: The Red-Gate USB key is really cute. It flashes red whenever it is accessed. You will be the envy of your friends. The keyring has a beer-bottle opener.

Subject: splitting strings
Posted by: user4301 (view profile)
Posted on: Tuesday, April 03, 2007 at 12:13 PM
Message: Looks really fast since I'm 'having'to use a cursor at the moment.
1 Does anyone have the neat addon of also select the 'patindex' of the first character of each of the words (so f of first is 1 and s of second is 7).
2 Does anyone have a resetting counter so that the returned result is "first,1,1","second,7,2"
the purpose for this is to STUFF a leading zero in single digit months and days of dates that can be anywhere in a text line.
3 "A line" is up to 1200 char long and typical days import is 0.75Mrows to 1.0Mrows
Very many thanks


Subject: Re: Splitting strings
Posted by: WebMister (view profile)
Posted on: Sunday, April 08, 2007 at 2:13 PM
Message: The problem of using PATINDEX is that you are not told the end of the match, as you would in a true regular expression. However, if you are clear about the format of the date you could do it cursorless with patindex. You'd have to do some timings to make sure that this would be quicker than using a WHILE loop.

Subject: Just a format test... please ignore...
Posted by: Jeff Moden (view profile)
Posted on: Tuesday, May 06, 2008 at 12:52 AM
Message: <code style="font-size: 12px;"><span style="color:green">--===== Create and populate the Tally table on the fly<br> </span><span style="color:blue">; SELECT TOP </span><span style="color:black">11000 </span><span style="color:green">--equates to more than 30 years of dates<br>        </span><span style="color:#434343">IDENTITY</span><span style="color:gray">(</span><span style="color:blue">INT</span><span style="color:gray">,</span><span style="color:black">1</span><span style="color:gray">,</span><span style="color:black">1</span><span style="color:gray">) </span><span style="color:blue">AS </span><span style="color:black">N<br>   </span><span style="color:blue">INTO </span><span style="color:black">dbo.Tally<br>   </span><span style="color:blue">FROM Master</span><span style="color:black">.dbo.SysColumns sc1</span><span style="color:gray">,<br>        </span><span style="color:blue">Master</span><span style="color:black">.dbo.SysColumns sc2<br><br></span><span style="color:green">--===== Add a Primary Key to maximize performance<br>  </span><span style="color:blue">ALTER TABLE </span><span style="color:black">dbo.Tally<br>    </span><span style="color:blue">ADD CONSTRAINT </span><span style="color:black">PK_Tally_N <br>        </span><span style="color:blue">PRIMARY KEY CLUSTERED </span><span style="color:gray">(</span><span style="color:black">N</span><span style="color:gray">) </span><span style="color:blue">WITH FILLFACTOR = </span><span style="color:black">100<br><br></span><span style="color:green">--===== Allow the general public to use it<br>  </span><span style="color:blue">GRANT SELECT ON </span><span style="color:black">dbo.Tally </span><span style="color:blue">TO PUBLIC<br><br><br><br></span></code>

Subject: How to format, please.
Posted by: Jeff Moden (view profile)
Posted on: Tuesday, May 06, 2008 at 12:55 AM
Message: Robyn and Phil,

What is the best way to post the nicely formatted code similar to what Robyn posted a little more than a half dozen posts up from this one?

Thanks folks.
--Jeff Moden

Subject: uftSplitString
Posted by: Kevin (view profile)
Posted on: Wednesday, May 07, 2008 at 12:32 PM
Message:
Cool stuff.

However... on the parameter split if parameter list happens to be exactually 8000 characters the substring in the select will fail. For example:

DECLARE @Parameter VARCHAR(8000)
SET @Parameter = REPLICATE('1,2,3,4,5,6,7,8,9,10',1000)

will produce @Parameter with a length of 8000 and cause the error. One can get around this buy doing something like checking the length and if it was 8000 use a select with a case statement looking for zero to be returned by charindex.

K

Subject: RE: Multi-char delimiter
Posted by: Anonymous (not signed in)
Posted on: Tuesday, July 22, 2008 at 2:11 AM
Message: Although your article has been written some time ago, i only have discovered it recently. It is indeed of great help and running through most of your examples i have been able to understand this technique and discover lots of situations where i could have used it. Many thanks!
Regarding the multi char delimiter. I have tried to implement my own function, a bit easier to follow and understand. It seems to me that it works, but perhaps I haven't tested all scenarios :)

ALTER FUNCTION [dbo].[ufsRemoveDelimited]
(
@String VARCHAR(8000)
,@OpeningDelimiter VARCHAR(20)
,@ClosingDelimiter VARCHAR(20)
)
RETURNS VARCHAR(8000)
AS
BEGIN
DECLARE
@NewString VARCHAR(MAX)
,@Poz INT;

SELECT
@NewString = ''
,@Poz = 0;

SELECT
@Poz = CASE WHEN SUBSTRING(@String, Number, LEN(@OpeningDelimiter)) = @OpeningDelimiter
THEN CHARINDEX(@ClosingDelimiter, @String, Number) + LEN(@ClosingDelimiter) - 1
ELSE @Poz
END
,@NewString = @NewString + CASE WHEN Number > @Poz
THEN SUBSTRING(@String, Number, 1)
ELSE ''
END
FROM
Numbers
WHERE
Number <= LEN(@String);

RETURN @NewString;
END

PRINT dbo.ufsRemoveDelimited('<-head-->
<-title-->A poem<-title-->
<-head-->
<-body-->
<-h2-->Weather forecast<-h2-->
<-p-->
The rain it raineth every day<-br-->
upon the just and unjust fellah<-Br-->
but mainly on the just, because,<-br-->
the unjust pinched the just''s umbrella<-br-->
<-p-->
<-body-->
<-html-->BLA'

,'<-'
,'-->')

 










Phil Factor
Finding Stuff in SQL Server Database DDL
 You'd have thought that nothing would be easier than using SQL Server Management Studio (SSMS) for searching... Read more...



 View the blog
Implementing User-Defined Hierarchies in SQL Server Analysis Services
 To be able to drill into multidimensional cube data at several levels, you must implement all of the... Read more...

Using the Filtering API with the SQL Comparison SDK
 Red Gate's SQL Comparison SDK provides a means to compare and synchronize database schemas and data... Read more...

SQL Toolbelt 2008: Predominantly an Engineering Task
 The conversion of the Red-Gate tools to be compatible with SQL Server 2008 might not seem, on first... Read more...

SQL Response: The dim sum interview
 Richard Morris met David and Nigel of the SQL Response team, in a dim sum Restaurant in Cambridge. They... Read more...

Writing Efficient SQL: Set-Based Speed Phreakery
 Phil Factor's SQL Speed Phreak challenge is an event where coders battle to produce the fastest code to... Read more...

Beginning SQL Server 2005 Reporting Services Part 1
 Steve Joubert begins an in-depth tour of SQL Server 2005 Reporting Services with a step-by-step guide... Read more...

Ten Common Database Design Mistakes
 Database design and implementation is the cornerstone of any data centric project (read 99.9% of... Read more...

Beginning SQL Server 2005 Reporting Services Part 2
 Continuing his in-depth tour of SQL Server 2005 Reporting Services, Steve Joubert demonstrates the most... Read more...

SQL Server Full Text Search Language Features
 SQL Full-text Search (SQL FTS) is an optional component of SQL Server 7 and later, which allows fast... Read more...

Creating CSV Files Using BCP and Stored Procedures
 Nigel Rivett demonstrates some core techniques for extracting SQL Server data into CSV files, focussing... Read more...

Over 150,000 Microsoft professionals subscribe to the Simple-Talk technical journal. Join today, it's fast, simple, free and secure.

Join Simple Talk