How do I NOT use a t-sql cursor when printing rows is necessary?
A colleague and I wrote this stored proc that documents a database table in wiki markup, for a ScrewTurn wiki system. Originally, I wrote it without a cursor, because until today I never even knew how to use one!
I started with what is essentially a combination of what you see below. I would select one column for each row, where that column was the entire wikimarkup of the row. This worked perfectly, but I wanted to print text before and after the results. I hacked that by using a few unions. I would union the header with the result set and then union all that with the footer. But THEN, I had to insert a row of text between each row, and THAT was the part I could not figure out without using a cursor. In short:
How do I select a bunch of records with a hardcoded row before each result row?
In my case, each row needs to be preceded with a |-
row.
set ansi_nulls on
go
set quoted_identifier on
go
alter procedure DocTable
@TableName varchar(256)
as
begin
set nocount on;
declare @WikiDocData table
(
Name nvarchar(256),
[Type] nvarchar(256),
Nullable nvarchar(256),
[Default] nvarchar(256),
[Identity] nvarchar(256),
[Description] nvarchar(max)
)
insert into @WikiDocData
select
c.name as Name,
tp.name +
' (' +
(case when c.max_length = -1 then 'MAX' else convert(nvarchar(256),c.max_length) end) +
', ' +
convert(nvarchar(256), c.scale) +
', ' +
convert(nvarchar(256), c.[precision]) + ')'
as [Type (L,S,P)],
(case when c.is_nullable = 1 then 'Yes' else '' end) as Nullable,
isnull(d.[definition], '') as [Default],
(case when c.is_identity = 1 then 'Yes' else '' end) as [Identity],
convert(nvarchar(max),isnull(p.value, '')) as [Description]
from
sys.tables t
inner join sys.columns c on t.object_id = c.object_id
left join sys.extended_properties p on c.object_id = p.major_id and c.column_id = p.minor_id
inner join sys.types tp on c.system_type_id = tp.system_type_id
left join sys.default_constraints d on c.default_object_id = d.object_id and c.column_id = d.parent_column_id
where
t.[name] = @TableName
and tp.name <> 'sysname'
order by
t.object_id,
c.column_id
/* Dear reader, if you know how to do this without a cursor, please let me know! */
-- Output header
print '{| cellpadding="4" cellspacing="0" border="1"'
print '! Name !! Type (L,S,P) !! Nullable !! Default !! Identity !! Description'
-- Output each row and row separator
declare @WikiRow nvarchar(max)
declare @GetWikiRow cursor
set @GetWikiRow = cursor for
select
'| ' +
Name + ' || ' +
[Type] + ' || ' +
Nullable + ' || ' +
[Default] + ' || ' +
[Identity] + ' || ' +
[Description]
from
@WikiDocData
open @GetWikiRow fetch next from @GetWikiRow into @WikiRow while @@fetch_status = 0
begin
print '|-'
print @WikiRow
开发者_JAVA百科 fetch next from @GetWikiRow into @WikiRow
end
close @GetWikiRow
deallocate @GetWikiRow
-- Output footer
print '|}'
end
go
This is currently working. It prints out exactly none other than the following when run on aspnet_Membership:
{| cellpadding="4" cellspacing="0" border="1"
! Name !! Type (L,S,P) !! Nullable !! Default !! Identity !! Description
|-
| ApplicationId || uniqueidentifier (16, 0, 0) || || || ||
|-
| UserId || uniqueidentifier (16, 0, 0) || || || ||
|-
| Password || nvarchar (256, 0, 0) || || || ||
|-
| PasswordFormat || int (4, 0, 10) || || ((0)) || ||
|-
| PasswordSalt || nvarchar (256, 0, 0) || || || ||
|-
| MobilePIN || nvarchar (32, 0, 0) || Yes || || ||
|-
| Email || nvarchar (512, 0, 0) || Yes || || ||
|-
| LoweredEmail || nvarchar (512, 0, 0) || Yes || || ||
|-
| PasswordQuestion || nvarchar (512, 0, 0) || Yes || || ||
|-
| PasswordAnswer || nvarchar (256, 0, 0) || Yes || || ||
|-
| IsApproved || bit (1, 0, 1) || || || ||
|-
| IsLockedOut || bit (1, 0, 1) || || || ||
|-
| CreateDate || datetime (8, 3, 23) || || || ||
|-
| LastLoginDate || datetime (8, 3, 23) || || || ||
|-
| LastPasswordChangedDate || datetime (8, 3, 23) || || || ||
|-
| LastLockoutDate || datetime (8, 3, 23) || || || ||
|-
| FailedPasswordAttemptCount || int (4, 0, 10) || || || ||
|-
| FailedPasswordAttemptWindowStart || datetime (8, 3, 23) || || || ||
|-
| FailedPasswordAnswerAttemptCount || int (4, 0, 10) || || || ||
|-
| FailedPasswordAnswerAttemptWindowStart || datetime (8, 3, 23) || || || ||
|-
| Comment || ntext (3000, 0, 0) || Yes || || ||
|}
New code with LittleBobbyTables' answer (it's shorter but involves a lot of string concatenation, and it fails to print when there are more than 8000 characters in the markup):
set ansi_nulls on
go
set quoted_identifier on
go
alter procedure DocTable
@TableName varchar(256)
as
begin
set nocount on;
-- Output header
print '{| cellpadding="4" cellspacing="0" border="1"'
-- Output each row and row separator
declare @WikiRow nvarchar(max)
set @WikiRow = '! Name !! Type (L,S,P) !! Nullable !! Default !! Identity !! Description'
select
@WikiRow = @WikiRow +
char(10) + '|- ' + char(10) + '| ' +
c.name + ' || ' +
tp.name +
' (' +
(case when c.max_length = -1 then 'MAX' else convert(nvarchar(256),c.max_length) end) +
', ' +
convert(nvarchar(256), c.scale) +
', ' +
convert(nvarchar(256), c.[precision]) + ')' + ' || ' +
(case when c.is_nullable = 1 then 'Yes' else '' end) + ' || ' +
isnull(d.[definition], '') + ' || ' +
(case when c.is_identity = 1 then 'Yes' else '' end) + ' || ' +
convert(nvarchar(max),isnull(p.value, ''))
from
sys.tables t
inner join sys.columns c on t.object_id = c.object_id
left join sys.extended_properties p on c.object_id = p.major_id and c.column_id = p.minor_id
inner join sys.types tp on c.system_type_id = tp.system_type_id
left join sys.default_constraints d on c.default_object_id = d.object_id and c.column_id = d.parent_column_id
where
t.[name] = @TableName
and tp.name <> 'sysname'
order by
t.object_id,
c.column_id
print @WikiRow
-- Output footer
print '|}'
end
go
Here's a routine to print long varchar(max) variables (it requires no distance between CRLF greater than the max threshold for PRINT to work, since it basically takes the string and moves it in a buffer in "lines", and then prints the buffer when it gets over 4000 characters):
CREATE PROCEDURE [usp_PrintLongSQL]
@sql varchar(max)
AS
BEGIN
DECLARE @CRLF AS varchar(2)
SET @CRLF = CHAR(13) + CHAR(10)
DECLARE @input AS varchar(max)
SET @input = @sql
DECLARE @output AS varchar(max)
SET @output = ''
WHILE (@input <> '')
BEGIN
DECLARE @line AS varchar(max)
IF CHARINDEX(@CRLF, @input) > 0
SET @line = LEFT(@input, CHARINDEX(@CRLF, @input) - 1) + @CRLF
ELSE
SET @line = @input
IF LEN(@input) - LEN(@line) > 0
SET @input = RIGHT(@input, LEN(@input) - LEN(@line))
ELSE
SET @input = ''
SET @output = @output + @line
IF LEN(@output) > 4000
BEGIN
PRINT @output
SET @output = ''
END
END
IF @output <> ''
PRINT @output
END
I personally prefer to use this, since it makes a lot of other code simpler and more versatile without a cursor (for instance code which can go into views or inline table-valued functions is far more reusable).
Update: Per Cade Roux and Chris, this doesn't work when printing over 8000 characters. I'm leaving this up as a warning.
You can use a variable to repeatedly add rows to. Try this:
-- Output header
print '{| cellpadding="4" cellspacing="0" border="1"'
print '! Name !! Type (L,S,P) !! Nullable !! Default !! Identity !! Description'
-- Output each row and row separator
declare @WikiRow nvarchar(max)
set @WikiRow = ''
select @WikiRow = @WikiRow +
'|- ' + char(10) + '| ' +
Name + ' || ' +
[Type] + ' || ' +
Nullable + ' || ' +
[Default] + ' || ' +
[Identity] + ' || ' +
[Description] + char(10)
from
@WikiDocData
print left(@WikiRow, len(@WikiRow) - 1)
-- Output footer
print '|}'
This can be done using the Offset Fetch clause in SQL Server 2012 or greater.
Using the AdventureWorks Production.Products table...
DECLARE @Output varchar(8000) = '';
-- 'Print' function only prints 8000 non-unicode chars max. Let's print 10 at a time. Use Fetch Next with Offset. (Sql Svr 2012+)
DECLARE @rowNum int = 0;
DECLARE @numRows int;
SELECT @numRows = count(ProductID) from Production.Products;
WHILE @rowNum < @numRows
BEGIN
SELECT @Output = @Output + '
IF (@someVariable = ''' + ProductNumber + ''') BEGIN; RETURN ''' + ProductName + '''; END;'
FROM Production.Products
ORDER BY ProductID
OFFSET @rowNum ROWS FETCH NEXT 10 ROWS ONLY; -- 10 rows at a time so can print without fear of truncation.
PRINT @Output;
SET @Output = ''; -- reset for next set of rows
SET @rowNum = @rowNum + 10;
END
How do I select a bunch of records with hardcoded string before each row?
select '|-I am a hardcoded string with a newline following'
+ char(10) + a.foo as foo
from bar a;
That is, just concat the hardcoded string onto the column you were already selecting. Separate them with a new line character (char(10)) or, for DOS/Windows, carriage-return linefeed (char(13) + char(10)).
Edit: thanks to everyone who pointed out that the catenation operator is "+", not "||" in T-SQL.
精彩评论