<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://www2.sqlblog.com/utility/FeedStylesheets/rss.xsl" media="screen"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/"><channel><title>Adam Machanic : T-SQL, Scripts</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/Scripts/default.aspx</link><description>Tags: T-SQL, Scripts</description><dc:language>en</dc:language><generator>CommunityServer 2.1 SP2 (Build: 61129.1)</generator><item><title>Looping over routines using sp_foreachroutine</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/looping-over-routines-using-sp-foreachroutine.aspx</link><pubDate>Thu, 13 Jul 2006 01:45:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:101</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>5</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/101.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=101</wfw:commentRss><description>Of all of the undocumented stored procedures shipped with SQL Server, there are two in particular that I constantly use: &lt;a href="http://www.databasejournal.com/features/mssql/article.php/3441031" target="#"&gt;sp_MSforeachtable and sp_MSforeachdb&lt;/a&gt;.
These procedures internally loop over each non-Microsoft shipped (i.e.
user-defined) table in the current database, or each database on the
current server, respectively. During this loop, the procedures perform
whatever action(s) are specified by the user (in the parameters). For
instance, what if you want to re-index every table in the database?
Sure, you could write your own cursor, but &lt;i&gt;why bother?&lt;/i&gt;  Use the following T-SQL instead:
&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;EXEC sp_MSforeachtable 'DBCC DBREINDEX(''?'')'&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Convenient, isn't it? But I won't get into any more detail on these.
Gregory Larsen does a good job of that in the article linked above.
&lt;/p&gt;&lt;p&gt;What I'd like to show instead is a very simple modification
I've made to sp_MSforeachtable. It's great to loop over tables and
databases, but sometimes we want to loop over &lt;i&gt;routines&lt;/i&gt;
(a collective term for procedures, functions, triggers, and views)
instead. Perhaps you want to grant pemissions to a user. Or perhaps you
want to roll out some &lt;a href="http://www.datamanipulation.net/TSQLMacro"&gt;TSQLMacro&lt;/a&gt;
updates to every routine in the database instead of just one, as is
supported by the current version of the framework... And now you know
how it will be done in the next version.
&lt;/p&gt;&lt;p&gt;
Presenting &lt;b&gt;sp_foreachroutine&lt;/b&gt;:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;CREATE PROCEDURE dbo.sp_foreachroutine&lt;br&gt;	@command1 nvarchar(2000), &lt;br&gt;	@replacechar nchar(1) = N'?', &lt;br&gt;	@command2 nvarchar(2000) = null,&lt;br&gt;	@command3 nvarchar(2000) = null, &lt;br&gt;	@whereand nvarchar(2000) = null,&lt;br&gt;	@precommand nvarchar(2000) = null, &lt;br&gt;	@postcommand nvarchar(2000) = null,&lt;br&gt;	@routinetype nvarchar(20) = null&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;	/* This proc returns one or more rows for each procedure (optionally, matching @where), &lt;br&gt;		with each procedure defaulting to its own result set */&lt;br&gt;	/* @precommand and @postcommand may be used to force a single result set via a temp table. */&lt;br&gt;&lt;br&gt;	/* Preprocessor won't replace within quotes so have to use str(). */&lt;br&gt;	declare @mscat nvarchar(12)&lt;br&gt;	select @mscat = ltrim(str(convert(int, 0x0002)))&lt;br&gt;&lt;br&gt;	if (@precommand is not null)&lt;br&gt;		exec(@precommand)&lt;br&gt;&lt;br&gt;	/* Create the select */&lt;br&gt;&lt;br&gt;	declare @sql nvarchar(4000)&lt;br&gt;	set @sql =&lt;br&gt;		N'declare hCForEach cursor global for ' &lt;br&gt;		 + N' select ''['' + REPLACE(user_name(uid), N'']'', N'']]'') + '']'' + ''.'' + ' &lt;br&gt;			+ N' ''['' + REPLACE(object_name(id), N'']'', N'']]'') + '']'' ' &lt;br&gt;		 + N' from dbo.sysobjects o '&lt;br&gt;	         + N' where OBJECTPROPERTY(o.id, N''IsMSShipped'') = 0 '&lt;br&gt;		 + 	CASE @routinetype&lt;br&gt;				WHEN 'procedure' THEN ' and OBJECTPROPERTY(o.id, N''IsProcedure'') = 1 '&lt;br&gt;				WHEN 'function' THEN ' and (OBJECTPROPERTY(o.id, N''IsScalarFunction'') = 1 '&lt;br&gt;					+ ' or OBJECTPROPERTY(o.id, N''IsTableFunction'') = 1) '&lt;br&gt;				WHEN 'view' THEN ' and OBJECTPROPERTY(o.id, N''IsView'') = 1 '&lt;br&gt;				WHEN 'trigger' THEN ' and OBJECTPROPERTY(o.id, N''IsTrigger'') = 1 '&lt;br&gt;				ELSE ' and ( ' &lt;br&gt;					+ ' OBJECTPROPERTY(o.id, N''IsProcedure'') = 1 '&lt;br&gt;					+ ' or OBJECTPROPERTY(o.id, N''IsScalarFunction'') = 1 '&lt;br&gt;					+ ' or OBJECTPROPERTY(o.id, N''IsTableFunction'') = 1 '&lt;br&gt;					+ ' or OBJECTPROPERTY(o.id, N''IsView'') = 1 '&lt;br&gt;					+ ' or OBJECTPROPERTY(o.id, N''IsTrigger'') = 1 '&lt;br&gt;					+ ' ) '&lt;br&gt;			END&lt;br&gt;	         + COALESCE(@whereand, '')&lt;br&gt;&lt;br&gt;	exec(@sql)&lt;br&gt;	declare @retval int&lt;br&gt;	select @retval = @@error&lt;br&gt;	if (@retval = 0)&lt;br&gt;		exec @retval = sp_MSforeach_worker @command1, @replacechar, @command2, @command3&lt;br&gt;&lt;br&gt;	if (@retval = 0 and @postcommand is not null)&lt;br&gt;		exec(@postcommand)&lt;br&gt;&lt;br&gt;	return @retval&lt;br&gt;END&lt;br&gt;GO&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;Regular readers of this blog will note that the formatting isn't
consistent with my usual standards. But since this was a port from an
MS-written proc, I decided to keep things fairly consistent with what
was already there. I've also added an additional parameter that wasn't
present in sp_MSforeachtable: @routinetype, which lets the user select
a specific type of routine to loop over. So, for instance, if you only
want views, pass in 'view'. Same for functions ('function'), triggers
('trigger') and procedures ('procedure'). Pass in any other value -- or
leave it NULL -- and you'll get all routines in the database.
&lt;/p&gt;&lt;p&gt;This procedure keeps the sp_ prefix on purpose; it's meant to
be created in the master database, and makes use of the MS-shipped
sp_MSforeach_worker stored procedure, which lets it do its work.
&lt;/p&gt;&lt;p&gt;Using it is simple. ? is the default substitution character
(this can be changed using the @replacechar parameter). So to print a
list of all routines in the current database, use:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;EXEC sp_foreachroutine 'print ''?'''&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
For just functions, use the optional @routinetype parameter:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;EXEC sp_foreachroutine 'print ''?''', @routinetype = 'function'&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Enjoy!&lt;/p&gt;&lt;br&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=101" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category></item><item><title>Tokenize UDF</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/tokenize-udf.aspx</link><pubDate>Thu, 13 Jul 2006 01:34:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:92</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>0</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/92.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=92</wfw:commentRss><description>Yes, another string splitting UDF from a guy who's obvioiusly become
obsessed with TSQL string splitting. This time we delve into a
mysterious world that I call, "Tokenization."
&lt;p&gt;
So what is Tokenization?  It's a word I made up for this problem.
&lt;/p&gt;&lt;p&gt;But what is it, really? It's splitting up a string based on a
delimiter -- in this case, a comma -- but being wary of substring
delimiters. In this case, that's a pair of apostrophes, because that's
what TSQL uses for strings.
&lt;/p&gt;&lt;p&gt;
I think this is best illustrated with an example string:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="CODE"&gt;DECLARE @Tokens VARCHAR(50)&lt;br&gt;&lt;br&gt;SET @Tokens = 'a, ''b'', ''''c'', ''d'', ''e'''', f, ''1,2,3,4'''&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
The basic &lt;a href="http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/splitting-a-string-of-unlimited-length.aspx"&gt;split string&lt;/a&gt; function that you can find will produce the following output:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT * &lt;br&gt;FROM dbo.SplitString(@Tokens, ',')&lt;br&gt;&lt;br&gt;OutParam&lt;br&gt;-------------&lt;br&gt;a&lt;br&gt;'b'&lt;br&gt;''c'&lt;br&gt;'d'&lt;br&gt;'e''&lt;br&gt;f&lt;br&gt;'1&lt;br&gt;2&lt;br&gt;3&lt;br&gt;4'&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Well, that's &lt;i&gt;wrong&lt;/i&gt;.  Because what I want to do is maintain the substrings (or, "tokens," as I like to call them -- thus, Tokenization!)
&lt;/p&gt;&lt;p&gt;
The output I desire is:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="CODE"&gt;Token&lt;br&gt;--------&lt;br&gt;a&lt;br&gt;'b'&lt;br&gt;''c', 'd', 'e''&lt;br&gt;f&lt;br&gt;'1,2,3,4'&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Notice that substrings -- delimited with apostrophes -- should be maintained.
&lt;/p&gt;&lt;p&gt;
And here's how I've solved this problem...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="CODE"&gt;CREATE FUNCTION dbo.Tokenize&lt;br&gt;(&lt;br&gt;	@Input NVARCHAR(2000)&lt;br&gt;)&lt;br&gt;RETURNS @Tokens TABLE &lt;br&gt;	(&lt;br&gt;		TokenNum INT IDENTITY(1,1),&lt;br&gt;		Token NVARCHAR(2000)&lt;br&gt;	)&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;	DECLARE @i INT SET @i = 0&lt;br&gt;	DECLARE @StartChar INT SET @StartChar = 1&lt;br&gt;	DECLARE @Quote INT SET @Quote = 0	&lt;br&gt;&lt;br&gt;	DECLARE @Chars TABLE &lt;br&gt;	(&lt;br&gt;		CharNum INT IDENTITY(1,1) NOT NULL PRIMARY KEY,&lt;br&gt;		TheChar CHAR(1), &lt;br&gt;		TheCount INT,&lt;br&gt;		StartChar INT&lt;br&gt;	)&lt;br&gt;&lt;br&gt;	SET @Input = ' , ' + @Input + ' , '&lt;br&gt;	&lt;br&gt;	INSERT @Chars (TheChar)&lt;br&gt;	SELECT SUBSTRING(@Input, n.Number, 1)&lt;br&gt;	FROM Numbers n&lt;br&gt;	WHERE n.Number &amp;gt; 0 &lt;br&gt;		AND n.Number &amp;lt;= LEN(@Input)&lt;br&gt;	ORDER BY n.Number&lt;br&gt;	&lt;br&gt;	UPDATE Chars SET &lt;br&gt;		@i = Chars.TheCount = &lt;br&gt;			CASE &lt;br&gt;				WHEN Chars1.TheChar = ',' &lt;br&gt;					AND @Quote % 2 = 0 THEN 0 &lt;br&gt;				ELSE @i + 1 &lt;br&gt;			END,&lt;br&gt;		@Quote = &lt;br&gt;			CASE  &lt;br&gt;				WHEN Chars1.TheChar = '''' THEN @Quote + 1 &lt;br&gt;				WHEN @i = 0 THEN 0 &lt;br&gt;				ELSE @Quote &lt;br&gt;			END,&lt;br&gt;		@StartChar = Chars.StartChar =&lt;br&gt;			CASE&lt;br&gt;				WHEN @i = 1 THEN Chars1.CharNum - 1&lt;br&gt;				WHEN @i = 0 THEN @StartChar + 1&lt;br&gt;				ELSE @StartChar&lt;br&gt;			END&lt;br&gt;	FROM @Chars Chars&lt;br&gt;	JOIN @Chars Chars1 ON Chars1.CharNum = Chars.CharNum + 1&lt;br&gt;&lt;br&gt;	INSERT @Tokens(Token)&lt;br&gt;	SELECT&lt;br&gt;		RTRIM(LTRIM(SUBSTRING(@Input, StartChar, CharNum - StartChar + 1)))&lt;br&gt;	FROM (&lt;br&gt;		SELECT StartChar, CharNum&lt;br&gt;		FROM @Chars&lt;br&gt;		WHERE TheCount = 0&lt;br&gt;&lt;br&gt;		UNION ALL&lt;br&gt;&lt;br&gt;		SELECT &lt;br&gt;			MAX&lt;br&gt;			(&lt;br&gt;				CASE TheCount &lt;br&gt;					WHEN 0 THEN CharNum &lt;br&gt;					ELSE 0 &lt;br&gt;				END&lt;br&gt;			) + 1, &lt;br&gt;			MAX(CharNum)&lt;br&gt;		FROM @Chars&lt;br&gt;	) x&lt;br&gt;	WHERE RTRIM(LTRIM(SUBSTRING(@Input, StartChar, CharNum - StartChar + 1))) NOT IN ('', ',')&lt;br&gt;	ORDER BY x.StartChar&lt;br&gt;	RETURN&lt;br&gt;END&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;A word of warning: This UDF uses the undocumented -- and unsupported
-- "aggregate update" functionality. I've tested thoroughly in this
case and believe it works perfectly (and it sure is handy!), but I
would advise you to not use it in your own projects without &lt;i&gt;extensive testing&lt;/i&gt;!  MS doesn't support this one, so handle with care.
&lt;/p&gt;&lt;p&gt;
And by the way, you need a &lt;a href="http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/you-require-a-numbers-table.aspx"&gt;numbers table&lt;/a&gt; to use this thing.  Of course.
&lt;/p&gt;&lt;p&gt;
As for using this thing, it's pretty easy:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="CODE"&gt;DECLARE @Tokens VARCHAR(50)&lt;br&gt;&lt;br&gt;SET @Tokens = 'a, ''b'', ''''c'', ''d'', ''e'''', f, ''1,2,3,4'''&lt;br&gt;&lt;br&gt;SELECT Token&lt;br&gt;FROM dbo.Tokenize(@Tokens)&lt;br&gt;&lt;br&gt;&lt;br&gt;Token&lt;br&gt;--------&lt;br&gt;a&lt;br&gt;'b'&lt;br&gt;''c', 'd', 'e''&lt;br&gt;f&lt;br&gt;'1,2,3,4'&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
... and it even appears to work properly!
&lt;/p&gt;&lt;p&gt;
Enjoy... and application for this and other strange things I've been posting recently coming very, very soon.&lt;/p&gt;&lt;br&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=92" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category></item><item><title>"Reflect" a TSQL routine</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/reflect-a-tsql-routine.aspx</link><pubDate>Thu, 13 Jul 2006 01:33:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:91</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>0</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/91.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=91</wfw:commentRss><description>Ever want to see the text of a stored procedure, function, or trigger -- or manipulate the text in some way?
&lt;p&gt;
sp_helptext works, sort of. But I really don't like the way it handles
large procedures (&amp;gt; 4000 characters). They seem to end up with some
strangely wrapped lines and other side-effects that aren't too nice. So
I decided to roll my own custom UDF to do the job. The UDF returns a
table containing two columns -- a line number (in increments of 100),
and a line of text, with a maximum of 4000 characters. Yes, that's a
limitation, but if you have stored procedures with line numbers longer
than 4000 characters, you probably have much, much bigger problems than
can be solved reading this blog.
&lt;/p&gt;&lt;p&gt;A friend of mine felt that a parallel of some sort could be
drawn between this UDF and .NET reflection, so I named it
"ReflectRoutine". I admit, the connection is somewhat tenuous, but it
sounds cool, so I'm leaving it. If you can think of a better name, feel
free to rename it on your end!
&lt;/p&gt;&lt;p&gt;I've commented this one quite heavily -- it uses a lot of
string manipulation and can get very confusing. At least, it was
incredibly confusing to write! So without further ado...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;CREATE FUNCTION dbo.ReflectRoutine&lt;br&gt;(&lt;br&gt;	@RoutineName VARCHAR(200)&lt;br&gt;)&lt;br&gt;RETURNS @ReturnTbl TABLE &lt;br&gt;	(&lt;br&gt;		LineNum INT IDENTITY(100, 100), &lt;br&gt;		OutParam NVARCHAR(4000)&lt;br&gt;	)&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;	/* n = current colid for the object */&lt;br&gt;	DECLARE @n TINYINT&lt;br&gt;	SET @n = 1&lt;br&gt;&lt;br&gt;	/* current is the current line of text we're working on */&lt;br&gt;	DECLARE @Current NVARCHAR(4000)&lt;br&gt;	SET @Current = ''&lt;br&gt;	DECLARE @CurrentLen INT&lt;br&gt;	SET @CurrentLen = 0&lt;br&gt;&lt;br&gt;	/* overflow and overflow2 will contain &lt;br&gt;	characters too big for the current working set */&lt;br&gt;	DECLARE @Overflow NVARCHAR(4000)&lt;br&gt;	SET @Overflow = ''&lt;br&gt;	DECLARE @OverflowLen INT&lt;br&gt;	SET @OverflowLen = 0&lt;br&gt;	DECLARE @Overflow2 NVARCHAR(4000)&lt;br&gt;	SET @Overflow2 = ''&lt;br&gt;&lt;br&gt;	/* delimiter is our delimiter for string splitting -- crlf */&lt;br&gt;	DECLARE @Delimiter NCHAR(2)&lt;br&gt;	SET @Delimiter = NCHAR(13)+NCHAR(10)&lt;br&gt;&lt;br&gt;	/* how many rows of 4000 characters are we dealing with? */&lt;br&gt;	DECLARE @maxColId TINYINT&lt;br&gt;	SELECT &lt;br&gt;		@maxColId = MAX(colid) + 1&lt;br&gt;	from dbo.syscomments&lt;br&gt;	WHERE id = OBJECT_ID(@RoutineName)&lt;br&gt;&lt;br&gt;	WHILE @n &amp;lt;= @maxColId&lt;br&gt;	BEGIN&lt;br&gt;		/* get the next row of data */&lt;br&gt;		IF @n &amp;lt; @maxColId&lt;br&gt;		BEGIN&lt;br&gt;			SELECT @Current = [text]&lt;br&gt;			FROM dbo.syscomments&lt;br&gt;			WHERE id = OBJECT_ID(@RoutineName)&lt;br&gt;				AND ColId = @n&lt;br&gt;		END&lt;br&gt;		ELSE&lt;br&gt;		BEGIN&lt;br&gt;			/* Work only with the overflow */&lt;br&gt;			SELECT @Current = ''&lt;br&gt;		END&lt;br&gt;&lt;br&gt;		SET @CurrentLen = DATALENGTH(@Current)/2&lt;br&gt;&lt;br&gt;		/* The current overflow value will be pushed into the front&lt;br&gt;		of the current row -- we need to pull off enough characters from the &lt;br&gt;		end to make room for the current overflow plus the current string&lt;br&gt;		and end up with at most 4000 characters.  Overflow2 is the characters&lt;br&gt;		that will be pulled from the end */&lt;br&gt;&lt;br&gt;		SET @Overflow2 = &lt;br&gt;			RIGHT(@Current,&lt;br&gt;				CASE&lt;br&gt;					WHEN (@CurrentLen + @OverflowLen) &amp;gt; 4000 THEN &lt;br&gt;						(@CurrentLen + @OverflowLen) - 4000&lt;br&gt;					ELSE 0&lt;br&gt;				END)&lt;br&gt;&lt;br&gt;		/* Now we push the previous overflow into the front of the value and&lt;br&gt;		get rid of the characters from the end that are now in overflow2 --&lt;br&gt;		leaving us with a current value that's at most 4000 characters (which &lt;br&gt;		is the limit for nvarchar) */&lt;br&gt;&lt;br&gt;		SET @Current = @Overflow + LEFT(@Current, @CurrentLen - DATALENGTH(@Overflow2)/2)&lt;br&gt;		SET @CurrentLen = DATALENGTH(@Current)/2&lt;br&gt;&lt;br&gt;		/* Re-calculate the overflow value.  Find the last occurrence of the &lt;br&gt;		delimiter (crlf) within the first 3996 characters of the string.  The&lt;br&gt;		rest of the string after that delimiter will be split on the next&lt;br&gt;		round (assume that there could be some concatenation needed with the&lt;br&gt;		next chunk, so we can't	split until that's done) &lt;br&gt;&lt;br&gt;		Also, surround the current value with two delimiters so that the split&lt;br&gt;		algorithm will work properly */&lt;br&gt;&lt;br&gt;		SELECT @Overflow = RIGHT(@Current, &lt;br&gt;			CASE &lt;br&gt;				WHEN @CurrentLen - MAX(Number) - 1 &amp;lt; 0 THEN 0&lt;br&gt;				ELSE @CurrentLen - MAX(Number) - 1&lt;br&gt;			END) + @Overflow2,&lt;br&gt;			@Current = @Delimiter + LEFT(@Current, MAX(Number) - 1) + @Delimiter&lt;br&gt;		FROM dbo.Numbers&lt;br&gt;		WHERE (SUBSTRING(@Current, Number, 2) = @Delimiter&lt;br&gt;				OR Number = @CurrentLen + 1)&lt;br&gt;			AND Number &amp;lt;= 3996&lt;br&gt;&lt;br&gt;		SET @CurrentLen = DATALENGTH(@Current)/2&lt;br&gt;		SET @OverflowLen = DATALENGTH(@Overflow)/2&lt;br&gt;&lt;br&gt;		/* finally, insert the lines for this round... */&lt;br&gt;		INSERT	@ReturnTbl (OutParam)&lt;br&gt;		SELECT	SUBSTRING(@Current, Number + 2,&lt;br&gt;				CHARINDEX(@Delimiter, @Current, Number + 2) - Number - 2)&lt;br&gt;		FROM	dbo.Numbers&lt;br&gt;		WHERE 	Number &amp;lt;= @CurrentLen - 2&lt;br&gt;			AND SUBSTRING(@Current, Number, 2) = @Delimiter&lt;br&gt;			AND @CurrentLen &amp;gt; 4&lt;br&gt;		ORDER BY Number&lt;br&gt;&lt;br&gt;		SET @n = @n + 1&lt;br&gt;	END&lt;br&gt;&lt;br&gt;	RETURN&lt;br&gt;END&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Note, it requires a &lt;a href="http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/you-require-a-numbers-table.aspx"&gt;numbers table&lt;/a&gt; -- but regular readers will probably ask, "what doesn't?"
&lt;/p&gt;&lt;p&gt;
Using it is quite easy... For instance, once it's created if you wish to reflect the UDF itself:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT *&lt;br&gt;FROM dbo.ReflectRoutine('ReflectRoutine')&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;I recommend Query Analyzer's "Results in Text" mode with "maximum
characters per column" set to a number greater than 4000 -- of course,
none of my stored procedures have lines greater than about 150
characters long, so that's probably not a huge deal for most people. As
I said, if your lines are that long, you have bigger problems!
&lt;/p&gt;&lt;p&gt;
So does this thing have any application?  Watch this space.  More on that coming soon...&lt;br&gt;
&lt;br&gt;
&lt;/p&gt;
&lt;hr&gt;
&lt;b&gt;Update, December 1, 2005&lt;/b&gt;: Don't use LEN when you mean DATALENGTH! I
used LEN in the original version of this function and it caused some
strange truncations to occur when an overflow happened right at some
white space (LEN does not count white space to the right). That bug is fixed
in the version now posted. By the way, in case you're reading this for
the first time, the application for this is &lt;a href="http://www.datamanipulation.net/tsqlmacro"&gt;TSQLMacro&lt;/a&gt;.&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=91" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category></item><item><title>Script out PKs/UNIQUE constraints and referencing FKs</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/script-out-pks-unique-constraints-and-referencing-fks.aspx</link><pubDate>Thu, 13 Jul 2006 01:32:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:90</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>17</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/90.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=90</wfw:commentRss><description>In the course of my work, I occasionally need to cluster a primary key
that's nonclustered, or go the other way, or make some other
modification to a primary key...
&lt;p&gt;But it's a hassle! All of the foreign keys need to be dropped,
the PK needs to be dropped, and then everything needs to be re-created.
Scripting all of that stuff out can be very annoying.
&lt;/p&gt;&lt;p&gt;With that annoyance in mind, I've written this stored
procedure. Put Query Analyzer into Results in Text mode and run
ScriptTableConstraints for a table. It will script out DROPs and
CREATEs for all primary keys, unique keys, and any foreign keys that
reference them -- in the right order. Foreign keys will be dropped
first, then any non-clustered PK or UNIQUE constraints, then clustered
constraints. Keys will be scripted for re-creation in the reverse
order.
&lt;/p&gt;&lt;p&gt;
... And that's it.  This should take some of the annoyance out of doing this kind of work.  Here is the stored procedure:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;CREATE PROCEDURE ScriptTableConstraints&lt;br&gt;	@TableName VARCHAR(100)&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;	SET NOCOUNT ON&lt;br&gt;&lt;br&gt;	SELECT 'ALTER TABLE [' + TC.TABLE_SCHEMA + '].[' + TC.TABLE_NAME + ']' + CHAR(13)+CHAR(10)+ &lt;br&gt;		'DROP CONSTRAINT [' + TC.CONSTRAINT_NAME + ']' + CHAR(13)+CHAR(10)+CHAR(13)+CHAR(10) &lt;br&gt;		AS [-- Drop Constraints]&lt;br&gt;	FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC&lt;br&gt;	LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC ON RC.CONSTRAINT_NAME = TC.CONSTRAINT_NAME&lt;br&gt;	LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FC ON FC.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME&lt;br&gt;	WHERE (TC.CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE') AND TC.TABLE_NAME = @TableName)&lt;br&gt;		OR (TC.CONSTRAINT_TYPE = 'FOREIGN KEY' AND FC.TABLE_NAME = @TableName)&lt;br&gt;	ORDER BY FC.TABLE_NAME DESC,&lt;br&gt;		INDEXPROPERTY(OBJECT_ID(TC.TABLE_NAME), TC.CONSTRAINT_NAME, 'IsClustered') ASC&lt;br&gt;	&lt;br&gt;	&lt;br&gt;	SELECT 'ALTER TABLE [' + TC.TABLE_SCHEMA + '].[' + TC.TABLE_NAME + ']' + CHAR(13)+CHAR(10)+ &lt;br&gt;		'ADD CONSTRAINT [' + TC.CONSTRAINT_NAME + ']' + CHAR(13)+CHAR(10)+&lt;br&gt;		'  ' + TC.CONSTRAINT_TYPE + &lt;br&gt;			CASE INDEXPROPERTY(OBJECT_ID(TC.TABLE_NAME), TC.CONSTRAINT_NAME, 'IsClustered') &lt;br&gt;				WHEN 1 THEN ' CLUSTERED' &lt;br&gt;				ELSE ' NONCLUSTERED' &lt;br&gt;			END + CHAR(13)+CHAR(10)+&lt;br&gt;		'  (' +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 1 THEN '[' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 2 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 3 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 4 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 5 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 6 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 7 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 8 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 9 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 10 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 11 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 12 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 13 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 14 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 15 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU.ORDINAL_POSITION WHEN 16 THEN ', [' + KCU.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;		')' + CHAR(13)+CHAR(10)+&lt;br&gt;		'WITH FILLFACTOR = ' + &lt;br&gt;			CONVERT(varchar(3), --Replace 0 FILLFACTOR with 100&lt;br&gt;				ISNULL(NULLIF(&lt;br&gt;					INDEXPROPERTY(OBJECT_ID(TC.TABLE_NAME), TC.CONSTRAINT_NAME, 'IndexFillFactor'), &lt;br&gt;				0), 100)&lt;br&gt;				) + CHAR(13)+CHAR(10)+&lt;br&gt;		'ON [' + sfg.groupname + ']' + CHAR(13)+CHAR(10)+CHAR(13)+CHAR(10)&lt;br&gt;		AS [-- Create PK/UNIQUE Constraints]&lt;br&gt;	FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC&lt;br&gt;	JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU ON TC.TABLE_NAME = KCU.TABLE_NAME&lt;br&gt;			AND TC.CONSTRAINT_NAME = KCU.CONSTRAINT_NAME&lt;br&gt;	LEFT JOIN sysindexes s ON s.name = TC.CONSTRAINT_NAME&lt;br&gt;		AND s.id = object_id(TC.TABLE_NAME)&lt;br&gt;	LEFT JOIN sysfilegroups sfg ON sfg.groupid = s.groupid&lt;br&gt;	WHERE TC.CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE')&lt;br&gt;		AND TC.TABLE_NAME = @TableName&lt;br&gt;	GROUP BY &lt;br&gt;		TC.TABLE_SCHEMA,&lt;br&gt;		TC.TABLE_NAME,&lt;br&gt;		TC.CONSTRAINT_NAME,&lt;br&gt;		TC.CONSTRAINT_TYPE,&lt;br&gt;		sfg.groupname&lt;br&gt;	ORDER BY INDEXPROPERTY(OBJECT_ID(TC.TABLE_NAME), TC.CONSTRAINT_NAME, 'IsClustered') DESC&lt;br&gt;	&lt;br&gt;	SELECT 'ALTER TABLE [' + FC.TABLE_SCHEMA + '].[' + FC.TABLE_NAME + ']' + CHAR(13)+CHAR(10)+ &lt;br&gt;		'ADD CONSTRAINT [' + FC.CONSTRAINT_NAME + '] ' + FC.CONSTRAINT_TYPE + CHAR(13)+CHAR(10)+&lt;br&gt;		'  (' +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 1 THEN '[' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 2 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 3 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 4 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 5 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 6 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 7 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 8 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 9 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 10 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 11 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 12 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 13 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 14 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 15 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_FK.ORDINAL_POSITION WHEN 16 THEN ', [' + KCU_FK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;		')' + CHAR(13)+CHAR(10)+&lt;br&gt;		'REFERENCES [' + TC.TABLE_SCHEMA + '].[' + TC.TABLE_NAME + ']' + CHAR(13)+CHAR(10)+&lt;br&gt;		'  (' +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 1 THEN '[' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 2 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 3 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 4 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 5 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 6 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 7 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 8 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 9 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 10 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 11 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 12 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 13 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 14 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 15 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;			MAX(CASE KCU_PK.ORDINAL_POSITION WHEN 16 THEN ', [' + KCU_PK.COLUMN_NAME + ']' ELSE '' END) +&lt;br&gt;		')' + CHAR(13)+CHAR(10)+CHAR(13)+CHAR(10)&lt;br&gt;		AS [-- Create FK Constraints]&lt;br&gt;	FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC&lt;br&gt;	JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU_PK ON TC.TABLE_NAME = KCU_PK.TABLE_NAME&lt;br&gt;			AND TC.CONSTRAINT_NAME = KCU_PK.CONSTRAINT_NAME&lt;br&gt;	JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC ON RC.UNIQUE_CONSTRAINT_NAME = TC.CONSTRAINT_NAME&lt;br&gt;	JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS FC ON FC.CONSTRAINT_NAME = RC.CONSTRAINT_NAME&lt;br&gt;	JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU_FK ON FC.TABLE_NAME = KCU_FK.TABLE_NAME&lt;br&gt;			AND FC.CONSTRAINT_NAME = KCU_FK.CONSTRAINT_NAME&lt;br&gt;	WHERE TC.CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE')&lt;br&gt;		AND TC.TABLE_NAME = @TableName&lt;br&gt;	GROUP BY &lt;br&gt;		FC.TABLE_SCHEMA,&lt;br&gt;		FC.TABLE_NAME,&lt;br&gt;		FC.CONSTRAINT_NAME,&lt;br&gt;		FC.CONSTRAINT_TYPE,&lt;br&gt;		TC.TABLE_SCHEMA,&lt;br&gt;		TC.TABLE_NAME&lt;br&gt;END&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Let me know if this helps you or if there's some modification that should be made... Enjoy!&lt;/p&gt;&lt;br&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=90" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category></item><item><title>Pattern-based split string</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/pattern-based-split-string.aspx</link><pubDate>Thu, 13 Jul 2006 01:31:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:89</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>9</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/89.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=89</wfw:commentRss><description>"hickymanz" asked in the &lt;a href="http://www.sqlservercentral.com/forums/shwmessage.aspx?forumid=8&amp;amp;messageid=161089" target="#"&gt;SQL Server Central forums&lt;/a&gt;
for a method of counting unique words in a text column. Wayne Lawton
recommended using a string split function, which was a good idea, but
not quite adequate for the job in my opinion. &lt;p&gt;
Typical string split functions, like &lt;a href="http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/splitting-a-string-of-unlimited-length.aspx"&gt;this one that I wrote&lt;/a&gt;
can handle only a single delimiter, e.g. a comma. But in the case of
splitting for unique words you want all sorts of possible delimiters --
punctuation marks, white space including spaces, possibly numerics,
etc.
&lt;/p&gt;&lt;p&gt;I believe I've seen other, similar requests for getting unique
words before, so I decided to solve the problem. I modified that split
string function to accept a pattern of &lt;i&gt;valid characters&lt;/i&gt;
for the second argument. Anything NOT in the pattern will be treated as
a delimiter. I figured that would be a bit more flexible than the other
way around, so that people don't have to type in (or figure out) every
single possible white space and/or punctuation character.
&lt;/p&gt;&lt;p&gt;Anyway, I think the modification is pretty straightforward if
you understand the previous function, so check it out and if you have
any questions feel free to ask. Here is the function:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;CREATE FUNCTION dbo.SplitStringPattern&lt;br&gt;(&lt;br&gt;	@List TEXT,&lt;br&gt;	@Pattern VARCHAR(50)&lt;br&gt;)&lt;br&gt;RETURNS @ReturnTbl TABLE (OutParam VARCHAR(20))&lt;br&gt;WITH SCHEMABINDING&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;	DECLARE @LeftSplit VARCHAR(7998)&lt;br&gt;	DECLARE @SplitStart INT SET @SplitStart = 0&lt;br&gt;	DECLARE @SplitEnd INT&lt;br&gt;	SET @SplitEnd = 7998&lt;br&gt;&lt;br&gt;	SET @Pattern = '%' + '[^' + RIGHT(@Pattern, LEN(@Pattern) - 1) + '%'&lt;br&gt;&lt;br&gt;	DECLARE @Delimiter CHAR(1)&lt;br&gt;	SELECT @Delimiter = CHAR(MAX(Number))&lt;br&gt;	FROM dbo.Numbers&lt;br&gt;	WHERE CHAR(Number) LIKE @Pattern&lt;br&gt;&lt;br&gt;	SELECT @SplitEnd = MAX(Number)&lt;br&gt;	FROM dbo.Numbers&lt;br&gt;	WHERE (SUBSTRING(@List, Number, 1) LIKE @Pattern&lt;br&gt;			OR Number = DATALENGTH(@List) + 1)&lt;br&gt;		AND Number BETWEEN @SplitStart AND @SplitEnd&lt;br&gt;&lt;br&gt;	WHILE @SplitStart &amp;lt; DATALENGTH(@List) - 1&lt;br&gt;	BEGIN&lt;br&gt;		SET @LeftSplit = @Delimiter + SUBSTRING(@List, @SplitStart, @SplitEnd - @SplitStart) + @Delimiter&lt;br&gt;&lt;br&gt;		INSERT @ReturnTbl (OutParam)&lt;br&gt;		SELECT LTRIM(RTRIM(SUBSTRING(@LeftSplit, Number + 1,&lt;br&gt;	                    PATINDEX(@Pattern, SUBSTRING(@LeftSplit, Number + 1, LEN(@LeftSplit))) - 1)))&lt;br&gt;	                AS Value&lt;br&gt;		FROM   dbo.Numbers&lt;br&gt;		WHERE  Number &amp;lt;= LEN(@LeftSplit) - 1&lt;br&gt;			AND SUBSTRING(@LeftSplit, Number, 1) LIKE @Pattern&lt;br&gt;			AND SUBSTRING(@LeftSplit, Number + 1,&lt;br&gt;	                    	PATINDEX(@Pattern, SUBSTRING(@LeftSplit, Number + 1, LEN(@LeftSplit))) - 1) &amp;lt;&amp;gt; ''&lt;br&gt;&lt;br&gt;		SET @SplitStart = @SplitEnd + 1&lt;br&gt;		SET @SplitEnd = @SplitEnd + 7998&lt;br&gt;&lt;br&gt;		SELECT @SplitEnd = MAX(Number) + @SplitStart&lt;br&gt;		FROM dbo.Numbers&lt;br&gt;		WHERE (SUBSTRING(@List, Number + @SplitStart, 1) LIKE @Pattern&lt;br&gt;				OR Number+@SplitStart = DATALENGTH(@List) + 1)&lt;br&gt;			AND Number BETWEEN 1 AND @SplitEnd - @SplitStart&lt;br&gt;	END&lt;br&gt;&lt;br&gt;	RETURN&lt;br&gt;END&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;A note on the pattern for input: The pattern should be
single-character based, and must be delimited with [ ]. Also, you must
type in the actual characters in the pattern. I do not recommend using
ranges; ranges cause strange side-effects because they tend to include
(depending on collation) characters that you wouldn't expect to show up
(e.g. characters with umlauts over them). So unless that's what you
really want, don't use ranges.
&lt;/p&gt;&lt;p&gt;
Here's a usage example:
&lt;/p&gt;&lt;pre class="code"&gt;SELECT OutParam&lt;br&gt;FROM dbo.SplitStringPattern('This is a test.', '[abcdefghijklmnopqrstuvwxyz]')&lt;br&gt;&lt;br&gt;&lt;br&gt;OutParam             &lt;br&gt;-------------&lt;br&gt;This&lt;br&gt;is&lt;br&gt;a&lt;br&gt;test&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Enjoy!&lt;/p&gt;&lt;br&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=89" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category></item><item><title>Validate a URL from SQL Server</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/validate-a-url-from-sql-server.aspx</link><pubDate>Thu, 13 Jul 2006 01:23:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:83</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>21</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/83.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=83</wfw:commentRss><description>File this one in your folder of things you should probably never use -- but maybe, some day, in an emergency, you'll need this.
&lt;p&gt;
I see posts requesting this functionality all the time. "How do I
validate a URL in SQL Server?" Not just the string, but the URL itself
-- how can we find out if it's valid?
&lt;/p&gt;&lt;p&gt;
Thanks to the Object Automation extended stored procedures Microsoft includes with SQL Server, it's quite easy...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;CREATE FUNCTION dbo.ValidateURL(@URL VARCHAR(300))&lt;br&gt;RETURNS BIT&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;	DECLARE @Object INT&lt;br&gt;	DECLARE @Return TINYINT&lt;br&gt;	DECLARE @Valid BIT SET @Valid = 0 --default to false&lt;br&gt;	&lt;br&gt;	--create the XMLHTTP object&lt;br&gt;	EXEC @Return = sp_oacreate 'MSXML2.ServerXMLHTTP.3.0', @Object OUTPUT&lt;br&gt;	IF @Return = 0&lt;br&gt;	BEGIN&lt;br&gt;		DECLARE @Method VARCHAR(350)&lt;br&gt;&lt;br&gt;		--define setTimeouts method&lt;br&gt;		--Resolve, Connect, Send, Receive&lt;br&gt;		SET @Method = 'setTimeouts(45000, 45000, 45000, 45000)'&lt;br&gt;&lt;br&gt;		--set the timeouts&lt;br&gt;		EXEC @Return = sp_oamethod @Object, @Method&lt;br&gt;&lt;br&gt;		IF @Return = 0&lt;br&gt;		BEGIN&lt;br&gt;			--define open method&lt;br&gt;			SET @Method = 'open("GET", "' + @URL + '", false)'&lt;br&gt;	&lt;br&gt;			--Open the connection&lt;br&gt;			EXEC @Return = sp_oamethod @Object, @Method&lt;br&gt;		END&lt;br&gt;	&lt;br&gt;		IF @Return = 0&lt;br&gt;		BEGIN&lt;br&gt;			--SEND the request&lt;br&gt;			EXEC @Return = sp_oamethod @Object, 'send()'&lt;br&gt;		END&lt;br&gt;	&lt;br&gt;		IF @Return = 0&lt;br&gt;		BEGIN&lt;br&gt;			DECLARE @Output INT&lt;br&gt;			EXEC @Return = sp_oamethod @Object, 'status', @Output OUTPUT&lt;br&gt;	&lt;br&gt;			IF @Output = 200&lt;br&gt;			BEGIN&lt;br&gt;				SET @Valid = 1&lt;br&gt;			END&lt;br&gt;		END&lt;br&gt;	END&lt;br&gt;	&lt;br&gt;	--destroy the object&lt;br&gt;	EXEC sp_oadestroy @Object&lt;br&gt;&lt;br&gt;	RETURN (@Valid)&lt;br&gt;END&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
... And that is it ...
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT dbo.ValidateURL('http://www.microsoft.com/sql')&lt;br&gt;&lt;br&gt;---&lt;br&gt;1&lt;br&gt;&lt;br&gt;&lt;br&gt;SELECT dbo.ValidateURL('http://www.XMLisNOTaMAGICbullet.com/')&lt;br&gt;&lt;br&gt;---&lt;br&gt;0&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;Note, you don't want to run this thing against a big table. It runs
synchronously and waits for the remote site to respond. That can
definitely hold locks open a lot longer than you might want.
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;hr&gt;
&lt;p&gt;
&lt;b&gt;UPDATE: &lt;/b&gt; Thanks to &lt;a href="http://blog.marcustucker.com/" target="#"&gt;Marcus Tucker&lt;/a&gt;
for pointing out that Microsoft.XMLHTTP (the XMLHTTP client object) was
not the right choice here. I've updated the UDF to use
MSXML2.ServerXMLHTTP.3.0, the XMLHTTP server object, instead.
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;hr&gt;
&lt;p&gt;
&lt;b&gt;ANOTHER UPDATE: &lt;/b&gt; Added a call to the setTimeouts method, as I
discovered that this wasn't behaving the same on differente servers --
apparently there is some default timeout set somewhere; I have no idea
where, though. Anyway, the four timeout types have all been set to 45
seconds (45000 ms). Tweak them if you need to.&lt;/p&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=83" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category></item><item><title>Counting occurrences of a substring within a string</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/counting-occurrences-of-a-substring-within-a-string.aspx</link><pubDate>Thu, 13 Jul 2006 01:22:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:82</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>15</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/82.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=82</wfw:commentRss><description>I have absolutely no idea why anyone wants to do this, but I keep
answering the same question in forums: "How do I count the occurrences
of a substring [note: usually comma] within a string?"
&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;p&gt;
In an effort to thwart carpal tunnel syndrome, I have created the Ultimate Substring Occurrence Counting UDF.
&lt;/p&gt;&lt;p&gt;
... And here it is:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;CREATE FUNCTION dbo.GetSubstringCount&lt;br&gt;(&lt;br&gt;	@InputString TEXT, &lt;br&gt;	@SubString VARCHAR(200),&lt;br&gt;	@NoisePattern VARCHAR(20)&lt;br&gt;)&lt;br&gt;RETURNS INT&lt;br&gt;WITH SCHEMABINDING&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;	RETURN &lt;br&gt;	(&lt;br&gt;		SELECT COUNT(*)&lt;br&gt;		FROM dbo.Numbers N&lt;br&gt;		WHERE&lt;br&gt;			SUBSTRING(@InputString, N.Number, LEN(@SubString)) = @SubString&lt;br&gt;			AND PATINDEX(@NoisePattern, SUBSTRING(@InputString, N.Number + LEN(@SubString), 1)) = 0&lt;br&gt;			AND 0 = &lt;br&gt;				CASE &lt;br&gt;					WHEN @NoisePattern = '' THEN 0&lt;br&gt;					ELSE PATINDEX(@NoisePattern, SUBSTRING(@InputString, N.Number - 1, 1))&lt;br&gt;				END&lt;br&gt;	)&lt;br&gt;END&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
First note:  You need (regular readers, you guessed it) a &lt;a href="http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/you-require-a-numbers-table.aspx"&gt;numbers table&lt;/a&gt;.
&lt;/p&gt;&lt;p&gt;Okay, so what's it do? Simply put, it returns the number of
times @SubString appears within @InputString. But wait! -- Act now and
you will receive an &lt;b&gt;additional bonus feature&lt;/b&gt; at no extra cost!  Can you feel the love?
&lt;/p&gt;&lt;p&gt;
The @NoisePattern parameter allows the user to put the UDF into "exact match" mode. 
&lt;/p&gt;&lt;p&gt;
For instance, let's say you have a big string containing some text
about automobile manufacturers, and for some reason (again, I have no
clue why people need this functionality -- fill me in if you do!) you
want to count the number of occurrences of the word "auto", but not the
number of occurrences of other forms of the word, e.g. "automobile" or
.... some word that ends in "auto" (if such a word exists).
&lt;/p&gt;&lt;p&gt;By specifying a pattern for @NoisePattern of characters that
shouldn't be adjacent to your word, you're telling the UDF that any
other characters are safe. Leaving the parameter empty means that all
occurrences of the substring will be counted. Examples:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT dbo.GetSubstringCount('We like the autos, the autos that go boom.', 'auto', '')&lt;br&gt;-- Returns 2&lt;br&gt;&lt;br&gt;SELECT dbo.GetSubstringCount('Autos are fun.  I like to drive my auto.', 'auto', '')&lt;br&gt;-- Also returns 2&lt;br&gt;&lt;br&gt;SELECT dbo.GetSubstringCount('Autos are fun.  I like to drive my auto.', 'auto', '%[a-z]%')&lt;br&gt;-- Only returns 1 -- The exact match must not have adjacent alphabetic characters&lt;br&gt;&lt;/pre&gt;&lt;br&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=82" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category></item><item><title>Splitting a string of unlimited length</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/splitting-a-string-of-unlimited-length.aspx</link><pubDate>Thu, 13 Jul 2006 01:20:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:81</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>14</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/81.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=81</wfw:commentRss><description>&lt;p&gt;There are many techniques for splitting a string in T-SQL (in other
words, taking a character-delimited string and producing a table of the
values), the best of which are encapsulated in Erland Sommarskog's &lt;a href="http://www.sommarskog.se/arrays-in-sql.html" target="#"&gt;famous article&lt;/a&gt;.  My favorite of his string splitting techniques is adapted from a previous example that was created by &lt;a href="http://www.bizdatasolutions.com/tsql/sqlarrays.asp" target="#"&gt;Anith Sen&lt;/a&gt;.
&lt;/p&gt;&lt;p&gt;
Both of these resources are excellent, but sometimes you just need a
little bit more. None of their solutions will split a string larger
than 8000 characters in length. So I've produced my own modified
version. If you're splitting smaller strings then &lt;i&gt;by all means, use theirs!&lt;/i&gt;
This is certainly slower, but I do believe it's the fastest way in SQL
Server 2000 to split a very long string. You will require a &lt;a href="http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/you-require-a-numbers-table.aspx"&gt;numbers table&lt;/a&gt;, so make sure you have it on hand...
&lt;/p&gt;&lt;p&gt;
Anyway, the code:
&lt;/p&gt;&lt;pre class="code"&gt;CREATE FUNCTION dbo.SplitString&lt;br&gt;(&lt;br&gt;	@List TEXT,&lt;br&gt;	@Delimiter CHAR(1)&lt;br&gt;)&lt;br&gt;RETURNS @ReturnTbl TABLE &lt;br&gt;(&lt;br&gt;	OutParam VARCHAR(20)&lt;br&gt;)&lt;br&gt;WITH SCHEMABINDING&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;	DECLARE @LeftSplit VARCHAR(7998)&lt;br&gt;	DECLARE @SplitStart INT SET @SplitStart = 0&lt;br&gt;	DECLARE @SplitEnd INT&lt;br&gt;	SET @SplitEnd = 7997&lt;br&gt;&lt;br&gt;	SELECT &lt;br&gt;		@SplitEnd = MAX(Number)&lt;br&gt;	FROM dbo.Numbers&lt;br&gt;	WHERE &lt;br&gt;		(&lt;br&gt;			REPLACE(SUBSTRING(@List, Number, 1), ' ', CHAR(255)) = &lt;br&gt;				REPLACE(@Delimiter, ' ', CHAR(255))&lt;br&gt;			OR Number = DATALENGTH(@List) + 1&lt;br&gt;		)&lt;br&gt;		AND Number BETWEEN @SplitStart AND @SplitEnd&lt;br&gt;&lt;br&gt;	WHILE @SplitStart &amp;lt; @SplitEnd&lt;br&gt;	BEGIN&lt;br&gt;		SET @LeftSplit = &lt;br&gt;			@Delimiter + &lt;br&gt;			SUBSTRING(@List, @SplitStart, @SplitEnd - @SplitStart) + &lt;br&gt;			@Delimiter&lt;br&gt;&lt;br&gt;		INSERT @ReturnTbl &lt;br&gt;		(&lt;br&gt;			OutParam&lt;br&gt;		)&lt;br&gt;		SELECT &lt;br&gt;			LTRIM&lt;br&gt;			(&lt;br&gt;				RTRIM&lt;br&gt;				(&lt;br&gt;					SUBSTRING&lt;br&gt;					(&lt;br&gt;						@LeftSplit, &lt;br&gt;						Number + 1,&lt;br&gt;	                    CHARINDEX(@Delimiter, @LeftSplit, Number + 1) - Number - 1&lt;br&gt;					)&lt;br&gt;				)&lt;br&gt;			) AS Value&lt;br&gt;		FROM dbo.Numbers&lt;br&gt;		WHERE  &lt;br&gt;			Number &amp;lt;= LEN(@LeftSplit) - 1&lt;br&gt;			AND REPLACE(SUBSTRING(@LeftSplit, Number, 1), ' ', CHAR(255)) = &lt;br&gt;				REPLACE(@Delimiter, ' ', CHAR(255))&lt;br&gt;			AND '' &amp;lt;&amp;gt;&lt;br&gt;				SUBSTRING&lt;br&gt;				(&lt;br&gt;					@LeftSplit, &lt;br&gt;					Number + 1, &lt;br&gt;					CHARINDEX(@Delimiter, @LeftSplit, Number + 1) - Number - 1&lt;br&gt;				)&lt;br&gt;&lt;br&gt;		SET @SplitStart = @SplitEnd + 1&lt;br&gt;		SET @SplitEnd = @SplitEnd + 7997&lt;br&gt;&lt;br&gt;		SELECT &lt;br&gt;			@SplitEnd = MAX(Number) + @SplitStart&lt;br&gt;		FROM dbo.Numbers&lt;br&gt;		WHERE &lt;br&gt;			(&lt;br&gt;				REPLACE(SUBSTRING(@List, Number + @SplitStart, 1), ' ', CHAR(255)) = &lt;br&gt;					REPLACE(@Delimiter, ' ', CHAR(255))&lt;br&gt;				OR Number + @SplitStart = DATALENGTH(@List) + 1&lt;br&gt;			)&lt;br&gt;			AND Number BETWEEN 1 AND @SplitEnd - @SplitStart&lt;br&gt;	END&lt;br&gt;&lt;br&gt;	RETURN&lt;br&gt;END&lt;br&gt;GO&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
This code does nothing more than chunk up the string, using the
delimiter as boundries, into small enough pieces (&amp;lt; 8000 characters)
that CHARINDEX can be used on them. Then the same algorithm from
Erland's split string is applied.
&lt;/p&gt;&lt;p&gt;Note that this function does not gracefully handle situations
such as passing in the wrong delimiter. So please carefully test your
code before deploying this!
&lt;/p&gt;&lt;p&gt;
Using the function is quite simple:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT * &lt;br&gt;FROM dbo.SplitString('something, something else, etc, etc, etc...', ',')&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;hr&gt;
&lt;p&gt;Update, February 15, 2005: Fixed so that only 8000 numbers are
needed in the Numbers table for this to work. Previously required as
many numbers as were present in the string to be split.&lt;/p&gt;&lt;p&gt;Update, February 23, 2009: Four years since the last update and a new bug is discovered by the insightful Craig Hathaway! Fixed the function so that it now handles space as a delimiter. Thanks, Craig!&lt;br&gt;&lt;/p&gt;&lt;br&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=81" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/splitting+strings/default.aspx">splitting strings</category></item><item><title>Pattern-based replacement UDF</title><link>http://www2.sqlblog.com/blogs/adam_machanic/archive/2006/07/12/pattern-based-replacement-udf.aspx</link><pubDate>Thu, 13 Jul 2006 01:07:00 GMT</pubDate><guid isPermaLink="false">21093a07-8b3d-42db-8cbf-3350fcbf5496:73</guid><dc:creator>Adam Machanic</dc:creator><slash:comments>14</slash:comments><comments>http://www2.sqlblog.com/blogs/adam_machanic/comments/73.aspx</comments><wfw:commentRss>http://www2.sqlblog.com/blogs/adam_machanic/commentrss.aspx?PostID=73</wfw:commentRss><description>&lt;p&gt;As a personal challenge, I decided to write a UDF that will work just
like T-SQL's REPLACE() function, but using patterns as input.
&lt;/p&gt;&lt;p&gt;
The first question:  How does REPLACE() handle overlapping patterns?
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT REPLACE('babab', 'bab', 'c')&lt;br&gt;&lt;br&gt;-------------------------------------------------- &lt;br&gt;cab&lt;br&gt;&lt;br&gt;(1 row(s) affected)&lt;br&gt;&lt;br&gt;&lt;br&gt;SELECT REPLACE('bababab', 'bab', 'c')&lt;br&gt;&lt;br&gt;-------------------------------------------------- &lt;br&gt;cac&lt;br&gt;&lt;br&gt;(1 row(s) affected)&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;It appears that SQL Server parses the input string from left to
right, replacing the first instance of the replacement string, and then
continues parsing to the right.
&lt;/p&gt;&lt;p&gt;Next question: How to do the replacement on a pattern? As it
turns out, this is somewhat trickier than I initially thought. A
replacement requires a starting point -- easy to find using PATINDEX --
and an end point. But there is no function for finding the last
character of a pattern. So you'll see that the UDF loops
character-by-character, testing PATINDEX, in order to find the end of
the match. This is useful for situations like:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT dbo.PatternReplace('baaa', 'ba%', 'c')&lt;br&gt;&lt;br&gt;-- We know that the match starts at character 1... but where does it end?&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Anyway, enough background, here's the code:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;CREATE FUNCTION dbo.PatternReplace&lt;br&gt;(&lt;br&gt;   @InputString VARCHAR(4000),&lt;br&gt;   @Pattern VARCHAR(100),&lt;br&gt;   @ReplaceText VARCHAR(4000)&lt;br&gt;)&lt;br&gt;RETURNS VARCHAR(4000)&lt;br&gt;AS&lt;br&gt;BEGIN&lt;br&gt;   DECLARE @Result VARCHAR(4000) SET @Result = ''&lt;br&gt;   -- First character in a match&lt;br&gt;   DECLARE @First INT&lt;br&gt;   -- Next character to start search on&lt;br&gt;   DECLARE @Next INT SET @Next = 1&lt;br&gt;   -- Length of the total string -- 8001 if @InputString is NULL&lt;br&gt;   DECLARE @Len INT SET @Len = COALESCE(LEN(@InputString), 8001)&lt;br&gt;   -- End of a pattern&lt;br&gt;   DECLARE @EndPattern INT&lt;br&gt; &lt;br&gt;   WHILE (@Next &amp;lt;= @Len) &lt;br&gt;   BEGIN&lt;br&gt;      SET @First = PATINDEX('%' + @Pattern + '%', SUBSTRING(@InputString, @Next, @Len))&lt;br&gt;      IF COALESCE(@First, 0) = 0 --no match - return&lt;br&gt;      BEGIN&lt;br&gt;         SET @Result = @Result + &lt;br&gt;            CASE --return NULL, just like REPLACE, if inputs are NULL&lt;br&gt;               WHEN  @InputString IS NULL&lt;br&gt;                     OR @Pattern IS NULL&lt;br&gt;                     OR @ReplaceText IS NULL THEN NULL&lt;br&gt;               ELSE SUBSTRING(@InputString, @Next, @Len)&lt;br&gt;            END&lt;br&gt;         BREAK&lt;br&gt;      END&lt;br&gt;      ELSE&lt;br&gt;      BEGIN&lt;br&gt;         -- Concatenate characters before the match to the result&lt;br&gt;         SET @Result = @Result + SUBSTRING(@InputString, @Next, @First - 1)&lt;br&gt;         SET @Next = @Next + @First - 1&lt;br&gt; &lt;br&gt;         SET @EndPattern = 1&lt;br&gt;         -- Find start of end pattern range&lt;br&gt;         WHILE PATINDEX(@Pattern, SUBSTRING(@InputString, @Next, @EndPattern)) = 0&lt;br&gt;            SET @EndPattern = @EndPattern + 1&lt;br&gt;         -- Find end of pattern range&lt;br&gt;         WHILE PATINDEX(@Pattern, SUBSTRING(@InputString, @Next, @EndPattern)) &amp;gt; 0&lt;br&gt;               AND @Len &amp;gt;= (@Next + @EndPattern - 1)&lt;br&gt;            SET @EndPattern = @EndPattern + 1&lt;br&gt;&lt;br&gt;         --Either at the end of the pattern or @Next + @EndPattern = @Len&lt;br&gt;         SET @Result = @Result + @ReplaceText&lt;br&gt;         SET @Next = @Next + @EndPattern - 1&lt;br&gt;      END&lt;br&gt;   END&lt;br&gt;   RETURN(@Result)&lt;br&gt;END&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
... And here's how you run it, with some sample outputs showing that it does, indeed, appear to work:
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT dbo.PatternReplace('babab', 'bab', 'c')&lt;br&gt;                                                   &lt;br&gt;-------------------------------------------------- &lt;br&gt;cab&lt;br&gt;&lt;br&gt;(1 row(s) affected)&lt;br&gt;&lt;br&gt;&lt;br&gt;SELECT dbo.PatternReplace('babab', 'b_b', 'c')&lt;br&gt;                                                   &lt;br&gt;-------------------------------------------------- &lt;br&gt;cab&lt;br&gt;&lt;br&gt;(1 row(s) affected)&lt;br&gt;&lt;br&gt;&lt;br&gt;SELECT dbo.PatternReplace('bababe', 'b%b', 'c')&lt;br&gt;&lt;br&gt;                                                   &lt;br&gt;-------------------------------------------------- &lt;br&gt;cabe&lt;br&gt;&lt;br&gt;(1 row(s) affected)&lt;br&gt;&lt;/pre&gt;
&lt;p&gt;
Hopefully this will help someone, somewhere.  I haven't found any use for it yet :)
&lt;/p&gt;&lt;p&gt;
Thanks to Steve Kass for posting some single-character replacement code which I based this UDF on.
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;hr&gt;
&lt;p&gt;
Update, January 10, 2005:  Thanks to &lt;a href="http://www.insidesql.de/" target="#"&gt;Frank Kalis&lt;/a&gt;,
I've tracked down some problems with the original UDF. The version
posted here has been fixed and now should respond identically to the
T-SQL REPLACE function when NULLs or non-pattern-based arguments are
passed in. The following example pairs should return the same values
(and do, at this point!)
&lt;/p&gt;&lt;p&gt;&amp;nbsp;&lt;/p&gt;&lt;pre class="code"&gt;SELECT dbo.PatternReplace(NULL, '', 'abc')&lt;br&gt;SELECT REPLACE(NULL, '', 'abc')&lt;br&gt;&lt;br&gt;SELECT dbo.PatternReplace('abc', '', NULL)&lt;br&gt;SELECT REPLACE('abc', '', NULL)&lt;br&gt;&lt;br&gt;SELECT dbo.PatternReplace('abc', NULL, '')&lt;br&gt;SELECT REPLACE('abc', NULL, '')&lt;br&gt;&lt;br&gt;SELECT dbo.PatternReplace('abc', 'b', '')&lt;br&gt;SELECT REPLACE('abc', 'b', '')&lt;br&gt;&lt;br&gt;SELECT dbo.PatternReplace('adc', 'b', '')&lt;br&gt;SELECT REPLACE('adc', 'b', '')&lt;br&gt;&lt;/pre&gt;&lt;img src="http://www2.sqlblog.com/aggbug.aspx?PostID=73" width="1" height="1"&gt;</description><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/T-SQL/default.aspx">T-SQL</category><category domain="http://www2.sqlblog.com/blogs/adam_machanic/archive/tags/Scripts/default.aspx">Scripts</category></item></channel></rss>