1 SET ANSI_NULLS ON; 2 SET ANSI_PADDING ON; 3 SET ANSI_WARNINGS ON; 4 SET CONCAT_NULL_YIELDS_NULL ON; 5 SET NUMERIC_ROUNDABORT OFF; 6 SET QUOTED_IDENTIFIER ON; 7 8 IF OBJECT_ID(‘[dbo].[FN_SPLIT_INT]‘) IS NOT NULL 9 DROP FUNCTION [dbo].[FN_SPLIT_INT] 10 GO 11 12 IF OBJECT_ID(‘[dbo].[FN_SPLIT]‘) IS NOT NULL 13 DROP FUNCTION [dbo].[FN_SPLIT] 14 GO 15 16 CREATE FUNCTION [dbo].[FN_SPLIT] ( 17 @Text nvarchar(max), -- Text to split 18 @Delimiter nvarchar(1000) -- Value to split on, try to only pass a single char. See notes for details. 19 ) 20 /* ------------------------------------------------------------------------------------------------ 21 Purpose: Split text on a common char. 22 Design Notes: 23 1) Will trim leading/trailing white space from items. 24 2) Will omit blank and null items. 25 3) Compatible from SQL 2005 and onward (see details about [item_int] in return table) 26 4) Return table item is nvarchar(max) (not bound by string length) 27 5) Reasonably durable with escape sequences, so if a delimiter has a [,%,_ in it, the patter should still match. 28 6) Uses a sliding search window, not saving the remaining text on each iteration. However, 29 saving each item in a temp variable (@item) was faster than using a CTE to temporarily 30 store the value, which surprised me. 31 7) Returns the value as an int as well, which is a common use for this function (splitting comma 32 separated lists of ints). Note that this should be low impact in that if you don‘t query 33 against that column since it is a non-persistent computed column (i.e. low overhead). 34 8) Supports @Delimiter > 1 char, but slower. Note in the unit tests, with text of approximately 35 10K, 1 char is about 30% faster, hence the big IF block in the code. Plus, the multi-char 36 delimiter does not scale terribly well. The test with 100,000 records, a 1 char delimiter takes 37 about 6 seconds while with a 5 char delimiter took 430 seconds (7 minutes!). As such, try to 38 replace your multi char delimiters with a single char value before calling this function. 39 Side Note: For what it‘s worth, I did try an alternative method of saving the remaining 40 "working text" as a sub string of text so the search would get faster near the end, but overall 41 it was slower at about 500 seconds. 42 43 NOTE: This version does not support case sensitivity. See "TODO" comments if you need a case insensitive version 44 45 Revision history: 46 --------------------------------------------------------------------------------------------------- 47 Date User Change Description 48 --------------------------------------------------------------------------------------------------- 49 10/10/2013 Brad Joss (bradjoss@hotmail.com) Initial Public Draft 50 ---------------------------------------------------------------------------------------------------*/ 51 RETURNS @retTable TABLE 52 ( 53 -- Output table definition 54 [item] nvarchar(max) COLLATE DATABASE_DEFAULT NOT NULL, 55 56 -- Since most of the times we split, we are splitting lists of ints, this makes that process easier. 57 -- Since the column is non persistent it should only be evaluated when requested (low overhead). 58 [item_int] as ( 59 -- SQL 2012 version, better, use if possible 60 -- TRY_CONVERT([int], NULLIF([item],‘‘)) -- SQL 2012 Format, faster and safer, but pre-2012 code provided as well... 61 62 -- Pre SQL 2012 syntax. Not as reliable, so use 2012 when possible by commenting out this CAST and using the TRY_CONVERT above 63 CAST( 64 CASE 65 WHEN LEN(item) > 11 THEN NULL -- LEN OF (-2147483648) is 11. Simple out of bounds checking. 66 WHEN ISNUMERIC([item]) = 1 AND [item] NOT LIKE ‘%.%‘ THEN [item] -- Ensure value conforms to int 67 ELSE null 68 END 69 as int) 70 ) 71 ) 72 WITH SCHEMABINDING 73 AS 74 BEGIN 75 -- Garbage in, Garbage out. If they did not pass input data, return nothing. 76 IF RTRIM(ISNULL(@Text,‘‘)) = ‘‘ OR RTRIM(ISNULL(@Delimiter,‘‘)) = ‘‘ 77 RETURN 78 79 DECLARE 80 @ix bigint -- Current index 81 , @pix bigint -- Previous index 82 , @del_len int -- Delimiter length 83 , @text_len bigint -- Input text length 84 , @item nvarchar(max) -- Temp item buffer. I tried w/o using CTEs, but this way was faster 85 86 SELECT @del_len = LEN(@Delimiter) 87 , @text_len = LEN(@Text) 88 89 IF @del_len = 1 90 BEGIN -- CHARINDEX MODE (Much faster than PATINDEX mode) 91 SELECT @ix = CHARINDEX(@Delimiter, @Text) -- TODO: If you want to implment Case Insensitivity here, wrap both in LOWER() 92 , @pix = 0 93 94 -- No delim found, just return the passed value, trimmed 95 IF @ix = 0 96 BEGIN 97 INSERT INTO @retTable(item) 98 SELECT LTRIM(RTRIM(@Text)) -- We know this is not null because of the first GIGO check above 99 END 100 ELSE 101 BEGIN 102 -- Find most of the matches 103 WHILE @ix > 0 104 BEGIN 105 SELECT 106 -- Get the current value 107 @item = LTRIM(RTRIM(SUBSTRING(@Text,@pix,(@ix - @pix)))) 108 -- Move previous pointer to end of last found delimiter 109 , @pix = @ix + @del_len 110 -- And update the values for next pass though the loop, finding the next match 111 , @ix = CHARINDEX(@Delimiter, @Text, (@ix + @del_len)) -- TODO: If you want to implment Case Insensitivity here, wrap both in LOWER() 112 113 IF @item <> ‘‘ AND @item IS NOT NULL -- Only save non empty values 114 INSERT INTO @retTable(item) VALUES (@item) 115 END 116 117 -- Get the trailing text 118 SET @item = LTRIM(RTRIM(SUBSTRING(@Text,@pix,@text_len))) 119 IF @item <> ‘‘ AND @item IS NOT NULL -- Only save non empty values 120 INSERT INTO @retTable(item) VALUES (@item) 121 END -- @ix = 0 122 END 123 ELSE -- @del_len = 1 124 BEGIN -- PATINDEX Mode (SLOW!) Try to pass in text that uses single char delimeters when possible 125 126 DECLARE @del_pat nvarchar(3002) -- Assume 3x @Delimiter + 2, for escaping every character plus wrapper % 127 128 -- Escape characters that will mess up the like clause, and wrap in wild cards % 129 SELECT @del_pat = ‘%‘ + REPLACE(REPLACE(REPLACE(@Delimiter 130 , ‘[‘,‘[[]‘) 131 , ‘%‘,‘[%]‘) 132 , ‘_‘, ‘[_]‘) 133 + ‘%‘ 134 135 SELECT @ix = PATINDEX(@del_pat, @Text) -- TODO: If you want to implment Case Insensitivity here, wrap both in LOWER() 136 , @pix = 0 137 138 -- No delim found, just return the passed value, trimmed 139 IF @ix = 0 140 BEGIN 141 INSERT INTO @retTable(item) 142 SELECT LTRIM(RTRIM(@Text)) -- We know this is not null because of the first GIGO check above 143 END 144 ELSE 145 BEGIN 146 -- Find most of the matches 147 WHILE @ix > 0 148 BEGIN 149 SELECT 150 -- Get the curent Item 151 @item = LTRIM(RTRIM(SUBSTRING(@Text,@pix,(@ix - @pix)))) 152 -- Move the previous index to the end of the previous delimiter 153 , @pix = @ix + @del_len 154 -- And set values for next itteration of the loop, finding the next match 155 , @ix = PATINDEX(@del_pat, SUBSTRING(@Text, (@ix + @del_len), @text_len)) -- TODO: If you want to implment Case Insensitivity here, wrap both in LOWER() 156 157 IF @item <> ‘‘ AND @item IS NOT NULL -- Only save non empty values 158 INSERT INTO @retTable(item) VALUES (@item) 159 160 IF @ix > 0 SET @ix = ((@ix + @pix) - 1) -- -1 since PatIndex is 1 based and Substring is 0 based 161 END 162 163 -- Get the trailing text 164 SET @item = LTRIM(RTRIM(SUBSTRING(@Text,@pix,@text_len))) 165 IF @item <> ‘‘ AND @item IS NOT NULL -- Only save non empty values 166 INSERT INTO @retTable(item) VALUES (@item) 167 END -- @ix = 0 168 END -- @del_len = 1 169 170 RETURN 171 END 172 173 GO 174 175 /* 176 Overloaded version to make splitting comma seperated lists of ints easier. 177 Note the delimiter is hard coded to comma and that non-int values will be removed. 178 */ 179 CREATE FUNCTION [dbo].[FN_SPLIT_INT] ( 180 @Text nvarchar(max) -- Text to split 181 ) 182 RETURNS TABLE 183 AS 184 RETURN SELECT [item_int] -- TODO: Optional add distinct? 185 FROM [dbo].[FN_SPLIT](@Text, ‘,‘) -- Hard coded to comma delimited 186 WHERE [item_int] IS NOT NULL -- Remove invalid values 187 GO 188 189 190 GRANT REFERENCES, SELECT ON [dbo].[FN_SPLIT] TO [public] AS [dbo] 191 GRANT REFERENCES, SELECT ON [dbo].[FN_SPLIT_INT] TO [public] AS [dbo] 192 GO 193 194 -- Precompile, so performance numbers below are not skewed. 195 SELECT * INTO #Garbage1 FROM [dbo].[FN_SPLIT](‘1,2,3‘,‘,‘) 196 SELECT * INTO #Garbage2 FROM [dbo].[FN_SPLIT_INT](‘1,2,3‘) 197 198 DROP TABLE #Garbage1 199 DROP TABLE #Garbage2 200 GO 201 202 -------------------------------------------------------------------------------------------------------------------------------------------- 203 -------------------------------------------------------------------------------------------------------------------------------------------- 204 -------------------------------------------------------------------------------------------------------------------------------------------- 205 -- Unit test -- 206 -- Split has been problematic enough for me I thought these tests important. 207 208 SET NOCOUNT ON 209 210 DECLARE @TESTS TABLE ( 211 [cnt] int, 212 [txt] nvarchar(max), 213 [delim] nvarchar(1000), 214 [sum_len] int 215 ) 216 DECLARE @RESULTS TABLE ( 217 [id] int identity(1,1), 218 [msg] varchar(255) null, 219 [pass_fail] AS ( 220 CASE 221 WHEN (ISNULL([expected_count],0) = ISNULL([actual_count],0) AND ISNULL([expected_sum_len],0) = ISNULL([actual_sum_len],0)) THEN ‘PASS‘ 222 ELSE ‘FAIL‘ 223 END 224 ), 225 [runtime] int null, 226 [expected_count] int null, 227 [actual_count] int null, 228 [expected_sum_len] int null, 229 [actual_sum_len] int null, 230 [delim] nvarchar(1000), 231 [txt] nvarchar(max) 232 ) 233 234 DECLARE @BigText nvarchar(max) 235 DECLARE @BigTextItemCount int 236 DECLARE @BigTextSumHash int 237 238 -- Alternative large volume tests, set to 10 for quick, set to 100K for a real workout 239 --SELECT @BigTextItemCount = 10, @BigTextSumHash = 11 240 SELECT @BigTextItemCount = 10000, @BigTextSumHash = 38894 241 --SELECT @BigTextItemCount = 100000, @BigTextSumHash = 488895 242 243 -- Create the hash of big text. I know this code is somewhat ugly, but it creates the large text in 244 -- about 1 second, as opposed to an itterative concat that took 14 minutes... :-) 245 ;with cte as ( 246 select 9 as [i] 247 union all 248 select [i] - 1 FROM cte where [i] > 0 249 ), 250 crs as ( 251 SELECT ROW_NUMBER() OVER(ORDER BY c1.[i]) as [rn] 252 FROM cte c1 -- 10 253 CROSS JOIN cte c2 -- 100 254 CROSS JOIN cte c3 -- 1000 255 CROSS JOIN cte c4 -- 10000 256 CROSS JOIN cte c5 -- 100000 257 ) 258 SELECT @BigText = 259 ( 260 ( 261 SELECT ‘#‘ + CAST([rn] as nvarchar(32)) 262 FROM crs 263 WHERE [rn] <= @BigTextItemCount 264 FOR XML PATH(‘‘) , TYPE 265 ).value(‘.‘, ‘nvarchar(max)‘) 266 ) 267 268 -- Most of the tests go here -- 269 INSERT INTO @TESTS (cnt, sum_len, txt, delim) 270 -- Basic 1-char Delim Tests 271 SELECT 0, 0, ‘‘, ‘,‘ 272 UNION ALL SELECT 0, 0, null, ‘,‘ 273 UNION ALL SELECT 0, 0, ‘a‘, null 274 UNION ALL SELECT 0, 0, ‘a‘, ‘‘ 275 UNION ALL SELECT 3, 3, ‘1,2,3‘, ‘,‘ 276 UNION ALL SELECT 3, 3, ‘,1,2,3‘, ‘,‘ 277 UNION ALL SELECT 3, 3, ‘1,2,3,‘, ‘,‘ 278 UNION ALL SELECT 3, 3, ‘,1,2,3,‘, ‘,‘ 279 UNION ALL SELECT 3, 3, ‘ , 1 , 2 , 3 , ‘, ‘,‘ 280 UNION ALL SELECT 3, 3, ‘,,, , 1 , 2 , 3 , ,,,‘, ‘,‘ 281 UNION ALL SELECT 3, 3, ‘a, b, c‘, ‘,‘ 282 UNION ALL SELECT 3, 3, ‘a,b,c‘, ‘,‘ 283 UNION ALL SELECT 2, 6, ‘Cat=Pub‘, ‘=‘ 284 UNION ALL SELECT 1, 1, ‘a‘, ‘,‘ 285 UNION ALL SELECT 1, 1, ‘ a ‘, ‘,‘ 286 -- 1 char Int Tests 287 UNION ALL SELECT 10, 18, ‘a,1,2,-1,-2,b,1.0,-1.0, 3 , -4 ,‘, ‘,‘ 288 -- Basic multi-char delim tests 289 UNION ALL SELECT 0, 0, ‘‘, ‘<tag>‘ 290 UNION ALL SELECT 0, 0, null, ‘<tag>‘ 291 UNION ALL SELECT 0, 0, ‘a‘, null 292 UNION ALL SELECT 0, 0, ‘a‘, ‘‘ 293 UNION ALL SELECT 3, 3, ‘1<TaG>2<tag>3‘, ‘<tag>‘ -- Case Insensitivity test 1 294 UNION ALL SELECT 3, 3, ‘<tag>1<tag>2<tag>3‘, ‘<TaG>‘ -- Case Insensitivity test 2 295 UNION ALL SELECT 3, 3, ‘1<tag>2<tag>3<tag>‘, ‘<tag>‘ 296 UNION ALL SELECT 3, 3, ‘<tag>1<tag>2<tag>3<tag>‘, ‘<tag>‘ 297 UNION ALL SELECT 3, 3, ‘ <tag> 1 <tag> 2 <tag> 3 <tag> ‘, ‘<tag>‘ 298 UNION ALL SELECT 3, 3, ‘<tag><tag><tag> <tag> 1 <tag> 2 <tag> 3 <tag> <tag><tag><tag>‘, ‘<tag>‘ 299 UNION ALL SELECT 3, 3, ‘a<tag> b<tag> c‘, ‘<tag>‘ 300 UNION ALL SELECT 3, 3, ‘a<tag>b<tag>c‘, ‘<tag>‘ 301 UNION ALL SELECT 2, 6, ‘Cat<tag>Pub‘, ‘<tag>‘ 302 UNION ALL SELECT 1, 1, ‘a‘, ‘<tag>‘ 303 UNION ALL SELECT 1, 1, ‘ a ‘, ‘<tag>‘ 304 -- multi char delim Int Tests 305 UNION ALL SELECT 10, 18, ‘a<tag>1<tag>2<tag>-1<tag>-2<tag>b<tag>1.0<tag>-1.0<tag> 3 <tag> -4 <tag>‘, ‘<tag>‘ 306 -- Delims with escape char % in it 307 UNION ALL SELECT 0, 0, ‘‘, ‘<t%a%g>‘ 308 UNION ALL SELECT 0, 0, null, ‘<t%a%g>‘ 309 UNION ALL SELECT 0, 0, ‘a‘, null 310 UNION ALL SELECT 0, 0, ‘a‘, ‘‘ 311 UNION ALL SELECT 3, 3, ‘1<t%a%g>2<t%a%g>3‘, ‘<t%a%g>‘ 312 UNION ALL SELECT 3, 3, ‘<t%a%g>1<t%a%g>2<t%a%g>3‘, ‘<t%a%g>‘ 313 UNION ALL SELECT 3, 3, ‘1<t%a%g>2<t%a%g>3<t%a%g>‘, ‘<t%a%g>‘ 314 UNION ALL SELECT 3, 3, ‘<t%a%g>1<t%a%g>2<t%a%g>3<t%a%g>‘, ‘<t%a%g>‘ 315 UNION ALL SELECT 3, 3, ‘ <t%a%g> 1 <t%a%g> 2 <t%a%g> 3 <t%a%g> ‘, ‘<t%a%g>‘ 316 UNION ALL SELECT 3, 3, ‘<t%a%g><t%a%g><t%a%g> <t%a%g> 1 <t%a%g> 2 <t%a%g> 3 <t%a%g> <t%a%g><t%a%g><t%a%g>‘, ‘<t%a%g>‘ 317 UNION ALL SELECT 3, 3, ‘a<t%a%g> b<t%a%g> c‘, ‘<t%a%g>‘ 318 UNION ALL SELECT 3, 3, ‘a<t%a%g>b<t%a%g>c‘, ‘<t%a%g>‘ 319 UNION ALL SELECT 2, 6, ‘Cat<t%a%g>Pub‘, ‘<t%a%g>‘ 320 UNION ALL SELECT 1, 1, ‘a‘, ‘<t%a%g>‘ 321 UNION ALL SELECT 1, 1, ‘ a ‘, ‘<t%a%g>‘ 322 UNION ALL SELECT 10, 18, ‘a<t%a%g>1<t%a%g>2<t%a%g>-1<t%a%g>-2<t%a%g>b<t%a%g>1.0<t%a%g>-1.0<t%a%g> 3 <t%a%g> -4 <t%a%g>‘, ‘<t%a%g>‘ 323 -- Delims with escape char _ in it 324 UNION ALL SELECT 0, 0, ‘‘, ‘<t_ag>‘ 325 UNION ALL SELECT 0, 0, null, ‘<t_ag>‘ 326 UNION ALL SELECT 0, 0, ‘a‘, null 327 UNION ALL SELECT 0, 0, ‘a‘, ‘‘ 328 UNION ALL SELECT 3, 3, ‘1<t_ag>2<t_ag>3‘, ‘<t_ag>‘ 329 UNION ALL SELECT 3, 3, ‘<t_ag>1<t_ag>2<t_ag>3‘, ‘<t_ag>‘ 330 UNION ALL SELECT 3, 3, ‘1<t_ag>2<t_ag>3<t_ag>‘, ‘<t_ag>‘ 331 UNION ALL SELECT 3, 3, ‘<t_ag>1<t_ag>2<t_ag>3<t_ag>‘, ‘<t_ag>‘ 332 UNION ALL SELECT 3, 3, ‘ <t_ag> 1 <t_ag> 2 <t_ag> 3 <t_ag> ‘, ‘<t_ag>‘ 333 UNION ALL SELECT 3, 3, ‘<t_ag><t_ag><t_ag> <t_ag> 1 <t_ag> 2 <t_ag> 3 <t_ag> <t_ag><t_ag><t_ag>‘, ‘<t_ag>‘ 334 UNION ALL SELECT 3, 3, ‘a<t_ag> b<t_ag> c‘, ‘<t_ag>‘ 335 UNION ALL SELECT 3, 3, ‘a<t_ag>b<t_ag>c‘, ‘<t_ag>‘ 336 UNION ALL SELECT 2, 6, ‘Cat<t_ag>Pub‘, ‘<t_ag>‘ 337 UNION ALL SELECT 1, 1, ‘a‘, ‘<t_ag>‘ 338 UNION ALL SELECT 1, 1, ‘ a ‘, ‘<t_ag>‘ 339 UNION ALL SELECT 10, 18, ‘a<t_ag>1<t_ag>2<t_ag>-1<t_ag>-2<t_ag>b<t_ag>1.0<t_ag>-1.0<t_ag> 3 <t_ag> -4 <t_ag>‘, ‘<t_ag>‘ 340 -- Semi Evil tests 341 UNION ALL SELECT 2, 2, ‘a~`!@#$%^&*()_+|-=\{}:;"‘‘<>,.?/ b‘, ‘~`!@#$%^&*()_+|-=\{}:;"‘‘<>,.?/ ‘ -- no [] 342 UNION ALL SELECT 2, 2, ‘a~`!@#$%^&*()_+|-=\{}[]:;"‘‘<>,.?/ b‘, ‘~`!@#$%^&*()_+|-=\{}[]:;"‘‘<>,.?/ ‘ -- with [] 343 UNION ALL SELECT 2, 2, ‘a‘ + CHAR(10) + CHAR(13) + ‘b‘, CHAR(10) + CHAR(13) -- White space chars 344 -- Big Text Tests 345 UNION ALL SELECT @BigTextItemCount,@BigTextSumHash,@BigText, ‘#‘ 346 UNION ALL SELECT @BigTextItemCount,@BigTextSumHash,REPLACE(@BigText,‘#‘, ‘<tag>‘), ‘<tag>‘ 347 348 -- Loop through each of the tests, logging results 349 DECLARE @txt nvarchar(max) -- Input text 350 DECLARE @delim nvarchar(1000) -- Input delimiter 351 DECLARE @cnt int -- Expected count 352 DECLARE @sum_len int -- Expected sum(len(item)) 353 DECLARE @t_cnt int -- Actual count 354 DECLARE @t_sum_len int -- Actual sum(len(item)) 355 DECLARE @start datetime -- Test Start time (for performance tracking) 356 357 DECLARE cur CURSOR FAST_FORWARD FOR 358 SELECT [cnt],[txt],[delim],[sum_len] FROM @TESTS 359 OPEN cur 360 FETCH cur INTO @cnt, @txt, @delim,@sum_len 361 WHILE @@FETCH_STATUS = 0 362 BEGIN 363 364 SELECT @start = GetDate(); 365 366 -- Execute test 367 SELECT @t_cnt = count(*), @t_sum_len = SUM(LEN(item)) 368 FROM [dbo].[FN_SPLIT](@txt, @delim) 369 370 -- Log results 371 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 372 SELECT ‘TEST‘, DATEDIFF(ms, @start, GetDate()), @cnt, @t_cnt, @sum_len, ISNULL(@t_sum_len,0), @delim, @txt 373 374 FETCH cur INTO @cnt, @txt, @delim,@sum_len 375 END 376 CLOSE cur 377 DEALLOCATE cur 378 379 ---------------------------------------------------------------------------------------------------------------------------------- 380 -- Extra tests that required additional coding 381 DECLARE @int_test nvarchar(max) 382 SELECT @int_test = N‘a,1,2,-1,-2,b,1.0,-1.0, 3 , -4 ,‘ 383 384 -- Basic int test, ensure int‘s are properly returned 385 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 386 SELECT ‘Tested Ints 1‘, null, 6, count(*), null, null, ‘,‘, @int_test 387 FROM [dbo].[FN_SPLIT](@int_test, ‘,‘) 388 WHERE [item_int] is not null 389 390 -- Ensure text value associated with int values maps 1:1 391 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 392 SELECT ‘Tested Ints 2‘, null, 6, count(*), null, null, ‘,‘, @int_test 393 FROM [dbo].[FN_SPLIT](@int_test, ‘,‘) 394 WHERE CAST([item_int] as nvarchar(max)) = [item] 395 and item_int is not null 396 397 398 -- Split int tests 399 SELECT @int_test = ‘1,-2,3‘ 400 SELECT @start = GetDate(); 401 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 402 SELECT ‘Split Int: ‘ + @int_test, DATEDIFF(ms, @start, GetDate()), 3, count(*), 2, SUM(item_int), ‘#‘, @int_test 403 FROM [dbo].[FN_SPLIT_INT](@int_test) 404 405 SELECT @int_test = ‘1,a,-2,b,3,c‘ 406 SELECT @start = GetDate(); 407 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 408 SELECT ‘Split Int: ‘ + @int_test, DATEDIFF(ms, @start, GetDate()), 3, count(*), 2, SUM(item_int), ‘#‘, @int_test 409 FROM [dbo].[FN_SPLIT_INT](@int_test) 410 411 SELECT @int_test = ‘1, -2, 3‘ -- Spaces between commas 412 SELECT @start = GetDate(); 413 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 414 SELECT ‘Split Int: ‘ + @int_test, DATEDIFF(ms, @start, GetDate()), 3, count(*), 2, SUM(item_int), ‘#‘, @int_test 415 FROM [dbo].[FN_SPLIT_INT](@int_test) 416 417 SELECT @int_test = ‘ 1, -2, 3 ‘ -- Leading/trailing 418 SELECT @start = GetDate(); 419 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 420 SELECT ‘Split Int: ‘ + @int_test, DATEDIFF(ms, @start, GetDate()), 3, count(*), 2, SUM(item_int), ‘#‘, @int_test 421 FROM [dbo].[FN_SPLIT_INT](@int_test) 422 423 SELECT @int_test = ‘999999999999999,1,-2,-3,-99999999999999999‘ -- Basic boundry testing 424 SELECT @start = GetDate(); 425 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 426 SELECT ‘Split Int: ‘ + @int_test, DATEDIFF(ms, @start, GetDate()), 3, count(*), -4, SUM(item_int), ‘#‘, @int_test 427 FROM [dbo].[FN_SPLIT_INT](@int_test) 428 429 SELECT @int_test = ‘ 1.0, -2.0, 3 ‘ -- Should only return ints 430 SELECT @start = GetDate(); 431 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 432 SELECT ‘Split Int: ‘ + @int_test, DATEDIFF(ms, @start, GetDate()), 1, count(*), 3, SUM(item_int), ‘#‘, @int_test 433 FROM [dbo].[FN_SPLIT_INT](@int_test) 434 435 ---------------------------------------------------------------------------------------------------------------------------------- 436 -- Runtime / Performance testing 437 438 IF OBJECT_ID(‘tempdb..#t1‘) IS NOT NULL DROP TABLE #t1 439 IF OBJECT_ID(‘tempdb..#t2‘) IS NOT NULL DROP TABLE #t2 440 IF OBJECT_ID(‘tempdb..#t3‘) IS NOT NULL DROP TABLE #t3 441 442 SELECT @start = GetDate(); 443 SELECT [item] INTO #t1 FROM [dbo].[FN_SPLIT](@BigText, ‘#‘) 444 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 445 SELECT ‘Speed 1: Item only‘, DATEDIFF(ms, @start, GetDate()), null, null, null, null, ‘#‘, @BigText 446 447 448 SELECT @start = GetDate(); 449 SELECT [item_int] INTO #t3 FROM [dbo].[FN_SPLIT](@BigText, ‘#‘) 450 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 451 SELECT ‘Speed 2: Item Int Only‘, DATEDIFF(ms, @start, GetDate()), null, null, null, null, ‘#‘, @BigText 452 453 SELECT @start = GetDate(); 454 SELECT [item] INTO #t2 FROM [dbo].[FN_SPLIT](@BigText, ‘#‘) WHERE [item_int] IS NOT NULL 455 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 456 SELECT ‘Speed 3: Item With Int Filter‘, DATEDIFF(ms, @start, GetDate()), null, null, null, null, ‘#‘, @BigText 457 458 IF OBJECT_ID(‘tempdb..#t1‘) IS NOT NULL DROP TABLE #t1 459 IF OBJECT_ID(‘tempdb..#t2‘) IS NOT NULL DROP TABLE #t2 460 IF OBJECT_ID(‘tempdb..#t3‘) IS NOT NULL DROP TABLE #t3 461 462 ---------------------------------------------------------------------------------------------------------------------------------- 463 /* 464 -- Ensure test failures work 465 INSERT INTO @RESULTS ([msg],[runtime],[expected_count],[actual_count],[expected_sum_len],[actual_sum_len],[delim],[txt]) 466 SELECT ‘INTENTIONAL FAILURE‘, null, 1, 2, 3, 4, ‘‘, ‘‘ 467 */ 468 469 -- Display results 470 SELECT * 471 FROM @RESULTS 472 ORDER BY CASE [pass_fail] WHEN ‘FAIL‘ THEN 0 ELSE 1 END ASC, [id] ASC 473 474 -- And Total runtime 475 SELECT SUM(ISNULL(runtime,0)) as [total_runtime] FROM @RESULTS 476 477 -- Raise errors as needed. 478 IF (SELECT count(*) FROM @RESULTS WHERE [pass_fail] = ‘FAIL‘) > 0 479 RAISERROR(‘Unexpected results. Review results table for details.‘,18,1) 480 GO
1 SET ANSI_NULLS ON; 2 SET ANSI_PADDING ON; 3 SET ANSI_WARNINGS ON; 4 SET CONCAT_NULL_YIELDS_NULL ON; 5 SET NUMERIC_ROUNDABORT OFF; 6 SET QUOTED_IDENTIFIER ON; 7 GO 8 9 IF OBJECT_ID(‘[dbo].[PR_FIND]‘) IS NOT NULL 10 DROP PROCEDURE [dbo].[PR_FIND] 11 GO 12 13 CREATE PROCEDURE [dbo].[PR_FIND] 14 /* ------------------------------------------------------------------------------------------------ 15 Purpose: Examples (finding self, since we know it exists) 16 17 EXEC [PR_FIND] ‘@DisplayLevel‘ 18 EXEC [PR_FIND] ‘PR_FIND‘, 0 19 EXEC [PR_FIND] ‘PR_FIND‘, 1, 50 20 21 22 Revision history: 23 --------------------------------------------------------------------------------------------------- 24 Date User Change Description 25 --------------------------------------------------------------------------------------------------- 26 10/11/2013 Brad Joss (bradjoss@hotmail.com) Initial Public Draft 27 01/20/2014 Brad Joss Fixed line number bug where line numbers were incorrect. 28 01/23/2014 Brad Joss When fixing the above bug, I saw some items that might 29 be nice to have, so I added a few features: 30 - Will now match on name, not just body text. Name can be fully qualified 31 since functionally it will do an OBJECT_ID() match. 32 - Display more details in run summary including total matches 33 and a better breakdown of where time was spent while searching. 34 - Display match_count and name_match in summary results. 35 ---------------------------------------------------------------------------------------------------*/ 36 ( 37 @TextToFind nvarchar(max), -- Pattern to search on 38 @DisplayLevel tinyint = 1, -- 0 = Object Summary Only, 1 = Windowed Results, 2 = Full text 39 @Lead int = 40, -- Amount of text to show the left and right of the result in Windowed mode 40 @SearchJobsToo bit = 1, -- When true, search SQL Agent Job Steps 41 @IncludeObjects nvarchar(max) = null, -- Comma separated list of objects scope search to 42 @ExcludeObjects nvarchar(max) = null -- Comma separated list of objects exempt from search 43 ) 44 AS 45 BEGIN 46 SET NOCOUNT ON 47 48 DECLARE @id int -- Object ID 49 , @name nvarchar(max) -- Full Object Name 50 , @Text nvarchar(max) -- Object Text 51 , @DisplayText nvarchar(max) -- Text to Output 52 , @index int -- index of start of display window (@match_index - @Lead) 53 , @match_index int -- Actual index where we found the match 54 , @matchCount int -- Matches found for the text 55 , @spanLen int -- Length of display window 56 , @lenText int -- Curent LEN(@Text) 57 , @lenTextToFind int -- LEN(@TextToFind) 58 , @StartTime DateTime -- Runtime Start 59 , @OriginalTextToFind nvarchar(max) -- Backup of what is being searched on before we adjust it 60 , @create_date datetime -- Object create date 61 , @modify_date datetime -- Object mod date 62 , @type_desc nvarchar(60) -- Object type 63 , @total_lines int -- Runing total of found new lines 64 , @current_line_number int -- Line number current match is found on (starting from CREATE statement) 65 , @total_start_time datetime -- Start time of the SP call, for kicks 66 , @skip_lines int -- When we skip a body of text, keep track of line count for skipped text 67 , @new_line nvarchar(max) -- Character(s) to seek when counting new lines 68 , @scan_time int -- Time it took to scan for matches 69 , @passed_object_id int -- If they passed in a full object name, let‘s know about it here... 70 71 72 -- Just to make sure we are keeping the code well optimzed, display runtimes 73 SELECT @total_start_time = GetDate() 74 75 -- Get this before we do any transformations on the text 76 SELECT @passed_object_id = OBJECT_ID(@TextToFind) 77 78 -- Backup what is being searched on so when we display it it doesn‘t look funny 79 SELECT @OriginalTextToFind = @TextToFind 80 81 -- Wrap the text in wild card wrappers and remove %%, setting to LOWER() so all matches will be case insenstive later on 82 SELECT @TextToFind = REPLACE(‘%‘ + LOWER(@TextToFind) + ‘%‘, ‘%%‘,‘%‘) 83 84 85 -- If they passed something that maps 1:1 to an object, escape out the bracets so we match on object name, 86 -- not character array patterns when searching on text. The actual name match will be based on object_id 87 IF @passed_object_id IS NOT NULL 88 SELECT @TextToFind = REPLACE(@TextToFind,‘[‘,‘[[]‘) 89 90 -- Even if we actually use 10/13, the way we are counting should still work. 91 -- If you move to more than 1 newline character, then the character count will have 92 -- to be factored into the delta counts (e.g. (LEN() - LEN(REPLACE()))/LEN(@new_line)) 93 SELECT @new_line = CHAR(10) 94 95 -- Parmeter checks 96 IF ISNULL(@DisplayLevel,-1) NOT IN (0,1,2) 97 SET @DisplayLevel = 1 -- Default to windowed mode 98 IF @Lead IS NULL OR @Lead < 0 99 SET @Lead = 40 -- Default lead lenght 100 IF @SearchJobsToo IS NULL 101 SET @SearchJobsToo = 1 102 IF RTRIM(@IncludeObjects) = ‘‘ 103 SET @IncludeObjects = NULL 104 IF RTRIM(@ExcludeObjects) = ‘‘ 105 SET @ExcludeObjects = NULL 106 107 108 -- Table to store the matched objects 109 DECLARE @oids TABLE ( 110 [id] int, -- [object_id] 111 [name] nvarchar(512), 112 [schema_name] nvarchar(512), 113 [full_name] as ( 114 ISNULL(‘[‘ + [schema_name] + ‘].‘, ‘‘) + ‘[‘ + [name] + ‘]‘ 115 ), 116 [create_date] datetime, 117 [modify_date] datetime, 118 [type_desc] nvarchar(60), 119 [txt] nvarchar(max), 120 [name_match] bit default(0), 121 [match_count] int null 122 ) 123 124 -- Table to store what objects to return 125 DECLARE @scope_objects TABLE ( 126 [object_id] int, 127 [include] bit not null default(0) 128 ) 129 130 -- If they have indicated they want to filter by object name, create a list of 131 -- valid objects to be searched. 132 IF @IncludeObjects IS NOT NULL OR @ExcludeObjects IS NOT NULL 133 BEGIN 134 -- Explicitly omit these items 135 ;with cte as ( 136 SELECT o.id 137 FROM sysobjects o 138 INNER JOIN [dbo].[FN_SPLIT](@ExcludeObjects,‘,‘) f 139 on (UPPER(o.name) LIKE LTRIM(RTRIM(UPPER([item])))) -- Case insensitive non-wildcard match, so we can actually match on a specific object name if need be 140 AND RTRIM([item]) <> ‘‘ 141 ) 142 INSERT INTO @scope_objects ([object_id],[include]) 143 SELECT DISTINCT [id], 0 144 FROM cte 145 WHERE [id] IS NOT NULL 146 147 IF @@ROWCOUNT = 0 148 SELECT @ExcludeObjects = NULL -- Later, we check to see if we should filter based on @ExcludeObjects and @IncludeObjects nullness 149 150 -- Scope search to only include these items 151 ;with cte as ( 152 SELECT o.id 153 FROM sysobjects o 154 INNER JOIN [dbo].[FN_SPLIT](@IncludeObjects,‘,‘) f 155 on (UPPER(o.name) LIKE LTRIM(RTRIM(UPPER([item])))) -- Case insensitive non-wildcard match, so we can actually match on a specific object name if need be 156 AND RTRIM([item]) <> ‘‘ 157 ) 158 INSERT INTO @scope_objects ([object_id],[include]) 159 SELECT DISTINCT [id], 1 160 FROM cte 161 WHERE [id] IS NOT NULL 162 163 IF @@ROWCOUNT = 0 164 SELECT @IncludeObjects = NULL -- Later, we check to see if we should filter based on @ExcludeObjects and @IncludeObjects nullness 165 ELSE 166 BEGIN 167 -- If they have indicated that the want to include and exclude at the same time 168 -- check for object overlap, and default to exclude the item so we can allow a 169 -- pattern like: 170 -- Show me all where "aaa%", except "aaa_111" 171 DELETE FROM @scope_objects 172 WHERE [include] = 1 173 AND [object_id] IN ( 174 SELECT [object_id] 175 FROM @scope_objects 176 WHERE [include] = 0 177 ) 178 179 -- If items were deleted, recheck to see if there are any includes left 180 IF @@ROWCOUNT > 0 181 IF NOT EXISTS (SELECT * FROM @scope_objects WHERE [include] = 1) 182 SELECT @IncludeObjects = NULL, @ExcludeObjects = NULL -- Later, we check to see if we should filter based on @ExcludeObjects and @IncludeObjects nullness 183 END 184 END -- @IncludeObjects IS NOT NULL OR @ExcludeObjects IS NOT NULL 185 186 -- Find matches 187 INSERT INTO @oids (id, name, [schema_name], [type_desc], txt, [create_date], [modify_date], [name_match]) 188 select distinct c.id, c.name, [schema_name], [type_desc], c.txt, [create_date], [modify_date], 189 CASE 190 -- Substring of a name is what was passed 191 WHEN LOWER(name) LIKE @TextToFind THEN 1 192 WHEN 193 ( 194 -- Fully qualified name was passed 195 @passed_object_id IS NOT NULL 196 AND 197 c.id = @passed_object_id 198 ) THEN 1 199 ELSE 0 END 200 from [dbo].[VW_OBJ_TXT] c 201 -- That case insensitive match our search text 202 where 203 ( 204 LOWER(c.[txt]) like @TextToFind -- Body match 205 OR 206 LOWER(name) LIKE @TextToFind -- Name match 207 OR 208 ( 209 -- In case they pass in "[dbo].[MyObject]" versus "dbo.MyObject" versus "MyObject" 210 -- Try to give them clues as to what they may be missing out on 211 @passed_object_id IS NOT NULL 212 AND 213 c.id = @passed_object_id 214 ) 215 ) 216 -- And are either in our include list, or no list was passed 217 and 218 ( 219 @IncludeObjects is null 220 OR 221 c.id IN (select [object_id] from @scope_objects where [include] = 1) 222 ) 223 -- And are not in our exclude list, or no list was passed 224 and 225 ( 226 @ExcludeObjects is null 227 OR 228 c.id NOT IN (select [object_id] from @scope_objects where [include] = 0) 229 ) 230 231 -- If they have indicated to search job text, do so here. 232 IF @SearchJobsToo = 1 233 BEGIN 234 INSERT INTO @oids (id, name, [schema_name], [type_desc], txt, [create_date], [modify_date], [name_match]) 235 SELECT DISTINCT 236 (-1 * ROW_NUMBER()OVER(ORDER BY j.name, js.step_name)), -- Since job_id‘s are not int, job_step_id might be confusing, get arbitrary negative number 237 ISNULL(‘JOB: ‘ + j.name, ‘Unknown Job‘) + ISNULL(‘, STEP: ‘ + js.step_name, ‘Unknown Step‘), 238 ‘job‘, 239 ‘SQL Agent Job Step‘, 240 js.command, -- Job Step Text 241 j.date_created, 242 j.date_modified, 243 CASE 244 WHEN LOWER(j.name) LIKE @TextToFind THEN 1 245 WHEN LOWER(js.step_name) LIKE @TextToFind THEN 1 246 ELSE 0 247 END 248 FROM msdb.dbo.sysjobs j 249 JOIN msdb.dbo.sysjobsteps js 250 ON js.job_id = j.job_id 251 WHERE 252 ( 253 LOWER(js.command) like @TextToFind -- Case insensitive 254 OR 255 LOWER(j.name) LIKE @TextToFind 256 OR 257 LOWER(js.step_name) LIKE @TextToFind 258 ) 259 END 260 261 SELECT @scan_time = DATEDIFF(ms, @total_start_time, GetDate()) 262 263 IF @DisplayLevel > 0 264 BEGIN 265 -- Horizontal rules to break up the results 266 DECLARE @C_OBJECT_HR nvarchar(max) = ‘/******************************************************************************/‘ 267 DECLARE @C_MATCH_HR nvarchar(max) = ‘--------------------------------------------------------------------------------‘ 268 269 -- Cache this value once before we enter into loop 270 SELECT @lenTextToFind = LEN(@TextToFind) - 2 -- -2 = Trimming the % 271 IF @lenTextToFind < 0 272 SELECT @lenTextToFind = 0 273 274 PRINT @C_OBJECT_HR 275 276 -- Loop though the results, getting the multiple matches within the body of the text 277 DECLARE DispCur CURSOR FAST_FORWARD FOR 278 SELECT 279 o.id, 280 [full_name], 281 o.txt, 282 create_date, 283 modify_date, 284 type_desc 285 from @oids o 286 ORDER BY LOWER([full_name]) ASC 287 288 OPEN DispCur 289 FETCH DispCur INTO @id, @name, @Text, @create_date, @modify_date, @type_desc 290 291 WHILE @@FETCH_STATUS = 0 292 BEGIN 293 -- Object match template, add details here to display information about the match per object 294 PRINT ‘ID: ‘ + CAST(@id as varchar(64)) 295 PRINT ‘NAME: ‘ + @name 296 PRINT ‘TYPE: ‘ + @type_desc 297 PRINT ‘CREATED: ‘ + CAST(@create_date as nvarchar(max)) + ‘, MODIFIED: ‘ + CAST(@modify_date as nvarchar(max)) 298 PRINT ‘SEARCH: "‘ + @OriginalTextToFind + ‘"‘ 299 PRINT @C_MATCH_HR 300 301 IF @DisplayLevel = 1 -- Windowed display mode 302 BEGIN 303 SELECT @StartTime = GetDate() -- For the search time of this one object (not the whole routine), for kicks 304 SELECT @index = PATINDEX(@TextToFind, LOWER(@Text)) -- Search for our matching pattern 305 SELECT @match_index = @index 306 SELECT @matchCount = 0 307 SELECT @DisplayText = ‘‘ 308 SELECT @total_lines = 0 309 310 -- Find all occurences of the pattern -- 311 WHILE @index > 0 312 BEGIN 313 -- Get the count of new line characters, then adding on matches from previous blocks of text 314 SELECT @current_line_number = (LEN(SUBSTRING(@Text, 1, @index)) - LEN(REPLACE(SUBSTRING(@Text, 1, @index), @new_line, ‘‘))) 315 316 -- Buffer the common search values in variables 317 SELECT @matchCount = @matchCount + 1 318 SELECT @lenText = LEN(@Text) 319 -- Set the start @index in bounds 320 SELECT @index = CASE 321 WHEN @index > @Lead THEN @index - @Lead 322 ELSE 0 323 END 324 -- Size of the display window 325 SELECT @spanLen = LEN(@TextToFind) + (2*@Lead) 326 327 -- If the search window is longer than the search text, narrow it 328 IF @spanLen + @index > @lenText 329 SELECT @spanLen = @lenText - @index 330 331 -- Display code snippet -- 332 SELECT @DisplayText = @DisplayText + ‘ 333 Match ‘ + CAST(@matchCount as varchar(32)) + ‘ on line ‘ + CAST((@current_line_number + @total_lines) as varchar(32)) + ‘ within ‘ + @name + ‘ 334 ‘ + @C_MATCH_HR + ‘ 335 ...‘ + SUBSTRING(@Text, @index, @spanLen) + ‘... 336 ‘ 337 338 -- If the search window covered to the end of the text, end searching 339 IF (@match_index + @Lead) >= @lenText 340 SELECT @index = 0 341 ELSE 342 BEGIN 343 -- Keep track of how many lines will be skipped by advancing the seek start by the window length 344 SELECT @skip_lines = LEN(SUBSTRING(@Text, @match_index, @Lead)) - LEN(REPLACE(SUBSTRING(@Text, @match_index, @Lead), @new_line, ‘‘)) 345 346 -- Else rescope the text to be searched to what remains and re-search 347 SELECT @Text = SUBSTRING 348 ( 349 @Text, 350 @match_index + @Lead, 351 @lenText - (@match_index + @Lead) 352 ) 353 SELECT @index = PATINDEX(@TextToFind, LOWER(@Text)) -- Find the next match 354 SELECT @match_index = @index 355 SELECT @total_lines = @total_lines + @current_line_number + @skip_lines -- Keep running total of line numbers 356 END 357 END -- While (finding all matches in object) 358 359 IF @matchCount = 0 360 SELECT @DisplayText = @DisplayText + ‘No body matches found, name match only.‘ 361 362 -- Footer template, displayed at the end of each object that matches 363 SELECT @DisplayText = @DisplayText + ‘ 364 ‘ + @C_MATCH_HR + ‘ 365 "‘ + @OriginalTextToFind + ‘" Found ‘ 366 + CAST(@matchCount as varchar(32)) + ‘ Time‘ + (CASE WHEN @matchCount = 1 THEN ‘‘ ELSE ‘s‘ END) + ‘ within ‘ + @name + ‘ in ‘ 367 + CAST(DATEDIFF(ms, @StartTime, GetDate()) as varchar(32)) + ‘ms 368 ‘ 369 370 EXEC PR_PRINT @DisplayText 371 372 UPDATE @oids SET [match_count] = @matchCount WHERE [id] = @id 373 374 END -- @DisplayLevel = 1 375 ELSE -- Mode 2 376 BEGIN 377 -- ELSE Display full code -- 378 EXEC PR_PRINT @Text 379 END 380 381 PRINT @C_OBJECT_HR 382 383 FETCH DispCur INTO @id, @name, @Text, @create_date, @modify_date, @type_desc 384 END 385 CLOSE DispCur 386 DEALLOCATE DispCur 387 END -- @DisplayLevel > 0 388 389 390 -- Display summary at the bottom so we have match counts -- 391 -- I would prefer this to be at the top, but I like the match count... 392 SELECT 393 [id], 394 CONVERT(varchar(19), [modify_date], 120) as [modify_date], 395 [type_desc], 396 [full_name], 397 [name_match], 398 [match_count] 399 from @oids t 400 ORDER BY LOWER(t.name) ASC 401 402 DECLARE @Message nvarchar(max) 403 DECLARE @TotalRuntime int 404 DECLARE @TotalMatches int 405 406 SELECT @TotalMatches = 407 (SELECT SUM(match_count) FROM @oids) 408 + 409 (SELECT COUNT(*) FROM @oids where [name_match] = 1); 410 411 SELECT @TotalRuntime = DATEDIFF(ms, @total_start_time, GetDate()) 412 SELECT @Message = ‘Search completed. ‘ 413 + ‘Found ‘ + CAST(@TotalMatches as nvarchar(max)) +‘ match‘ + CASE WHEN @TotalMatches = 1 THEN ‘‘ ELSE ‘es‘ END + ‘. ‘ 414 + CAST(@scan_time as nvarchar(max)) +‘ms Scan Time. ‘ 415 + CAST((@TotalRuntime - @scan_time) as nvarchar(max)) + ‘ms Format Time. ‘ 416 + ‘Total Runtime: ‘ + CAST((@TotalRuntime + 500)/1000 as nvarchar(max)) + ‘ seconds.‘ -- + 500 so we round up at 1/2 second 417 PRINT ‘‘ 418 PRINT @Message 419 END 420 GO 421 422 GRANT EXECUTE ON [dbo].[PR_FIND] TO [public] AS [dbo] 423 GO
1 IF OBJECT_ID(‘dbo.PR_PRINT‘) IS NOT NULL 2 DROP PROCEDURE dbo.PR_PRINT 3 GO 4 5 CREATE PROCEDURE [dbo].[PR_PRINT] 6 ( 7 @txt NVARCHAR(MAX) -- Text to print out 8 ) 9 AS 10 11 /* 12 This was originally posted on SQLServerCentral.com at 13 http://www.sqlservercentral.com/scripts/Print/63240/ 14 15 Modifed by Brad Joss 10/11/13 to break on space as well 16 17 This procedure is designed to overcome the limitation 18 in the SQL print command that causes it to truncate strings 19 longer than 8000 characters (4000 for nvarchar) in SQL Server 20 management studio. 21 22 It will print the text passed to it in substrings smaller than 4000 23 characters. If there are carriage returns (CRs) or new lines (NLs in the text), 24 it will break up the substrings at the carriage returns and the 25 printed version will exactly reflect the string passed. 26 27 If there are insufficient line breaks in the text, it will 28 print it out in blocks of 4000 characters with an extra carriage 29 return at that point. 30 31 If it is passed a null value, it will do virtually nothing. 32 33 NOTE: This is substantially slower than a simple print, so should only be used 34 when actually needed. 35 */ 36 37 SET NOCOUNT ON 38 39 DECLARE 40 @cur_end BIGINT, -- track the length of the next substring 41 @offset tinyint, -- tracks the amount of offSET needed 42 @pg_sz int, -- size of printing window 43 @char_idx bigint, -- index of the next newline character in the window 44 @temp_char_idx bigint, -- to avoid having to substring/reverse the window twice, save the reverse index of a found space here 45 @start_idx int, -- try to break on a known character combination 46 @txt_len bigint -- current lenght of the text, basically a cache since we potentially will need it twice in the loop 47 48 SET @pg_sz = 4000 -- Set window size to 4000 characters 49 50 -- If size is in bounds (ie, small text), just print it and exit 51 IF LEN(@txt) <= @pg_sz 52 BEGIN 53 PRINT @txt 54 RETURN 55 END 56 57 SET @txt = replace( replace(@txt, char(13) + char(10), char(10)) , char(13), char(10)) -- Standardize what a new line looks like to char(10) 58 SELECT @txt_len = LEN(@txt) 59 60 WHILE @txt_len > 1 61 BEGIN 62 63 -- Try to break on a new line 64 SET @char_idx = CHARINDEX(CHAR(10), @txt) 65 66 -- If we can‘t find a new line, try to break on a space where the space is near 67 -- the end of the current page of text 68 IF NOT (@char_idx between 1 AND @pg_sz) 69 BEGIN 70 -- Get the size of the page of text 71 SELECT @char_idx = CASE WHEN (@txt_len < @pg_sz) THEN @txt_len ELSE @pg_sz END 72 73 -- Look for the last space character in the page of text 74 SET @temp_char_idx = CHARINDEX(‘ ‘, REVERSE(SUBSTRING(@txt, 1, @char_idx))) 75 76 -- If found, set the position of the found character on the non-reversed string 77 IF @temp_char_idx > 0 78 SET @char_idx = (@char_idx - @temp_char_idx) + 1 -- +1 here since we -1 later on 79 ELSE -- Indicate character is still not found 80 SET @char_idx = 0 81 END 82 83 -- Try to break on a known char (newline or space) -- 84 IF @char_idx between 1 AND @pg_sz 85 BEGIN 86 -- Since we know the character we are breaking on is white space (new line or space) 87 -- don‘t print it (hence the -1) 88 SET @cur_end = @char_idx - 1 89 SET @offset = 2 90 END 91 ELSE 92 BEGIN 93 -- Else, just break at the window size. 94 SET @cur_end = @pg_sz 95 SET @offset = 1 96 END 97 98 -- Print the section 99 PRINT SUBSTRING(@txt, 1, @cur_end) 100 101 -- Remote the printed text from what remains to be printed. 102 SET @txt = SUBSTRING(@txt, @cur_end+@offset, 1073741822) 103 SELECT @txt_len = LEN(@txt) 104 105 END /*End While loop*/ 106 107 -- Print any leftovers 108 PRINT @txt 109 110 GO 111 112 GRANT EXECUTE ON [dbo].[PR_PRINT] TO [public] 113 GO 114 115 PRINT ‘Basic Test:‘ 116 EXEC dbo.PR_PRINT ‘aaa bbb ccc d‘ 117 118 119 PRINT ‘‘ 120 PRINT ‘More Complicated Test:‘ 121 122 DECLARE @BigText nvarchar(max) 123 DECLARE @WindowSize int 124 SET @WindowSize = 4000 125 SELECT @BigText = CAST(REPLICATE(‘a‘,@WindowSize-1) as nvarchar(max)) 126 + CAST(‘ ‘ as nvarchar(max)) 127 + CAST(REPLICATE(‘b‘,@WindowSize-1) as nvarchar(max)) 128 + CAST(CHAR(10) as nvarchar(max)) 129 + CAST(REPLICATE(‘c‘,@WindowSize-1) as nvarchar(max)) 130 + CAST(‘xxx‘ as nvarchar(max)) 131 + CAST(REPLICATE(‘d‘,@WindowSize-1)as nvarchar(max)) 132 133 134 EXEC dbo.PR_PRINT @BigText 135 GO
IF OBJECT_ID(‘VW_OBJ_TXT‘) IS NOT NULL DROP VIEW [dbo].[VW_OBJ_TXT] GO CREATE VIEW [dbo].[VW_OBJ_TXT] /******************************************************************************* * Name : dbo.VW_OBJ_TXT * Author : bradjoss@hotmail.com * Purpose : Helper view to make searching thorough code easier ****************************************************************************** * Change Date Change By Change DSC * ----------- ------------- -------------------------------------- * 10/05/2012 Brad Joss Initial Draft * 01/24/2014 Brad Joss Return records with null text bodies, like tables. Nixed cast on get from syscomments, it was unnecessary. *******************************************************************************/ AS with cte as ( select o.[object_id] as [id], o.[schema_id], SCHEMA_NAME(o.[schema_id]) as [schema_name], o.name, o.type, o.type_desc, o.create_date, o.modify_date, ( ( -- Concatenate together all the sections of code that make up a given object SELECT c2.[text] FROM syscomments c2 (nolock) WHERE c2.[id] = o.[object_id] ORDER BY colid FOR XML PATH(‘‘) , TYPE ).value(‘.‘, ‘nvarchar(max)‘) ) AS [txt] from sys.objects o ) SELECT cte.* /* -- These extend the potency of the view for doing code comparison. Since this view is only -- being posted as part of another suite of code, I have commmented them out. Uncomment if -- they would be useful to you in some way. , LEN([txt]) AS [txt_len] -- The name swap is to contend with cases where the object text may be the same but the name -- differs, as might be the case with an automatic constraint name. , CHECKSUM(REPLACE(txt, name, ‘##NAME##‘)) as [checksum] */ FROM cte -- WHERE [txt] IS NOT NULL -- Allow returing of objects w/o bodies GO -- Ensure it compiles select top 3 * from [VW_OBJ_TXT] GO
https://www.codeproject.com/Tips/667730/T-SQL-Search-SQL-Code
EXEC [dbo].[PR_FIND] ‘Sys_Role‘
原文:https://www.cnblogs.com/weifeng123/p/11114305.html