Why PRINT affects ##rowcount - sql

I recently noticed that while trying to loop through the rows of a table my loop wouldn't run based on if I had a PRINT statement before the the loop condition involving ##ROWCOUNT.
declare #s int;
select top 1 #s=sequence from myTable order by sequence;
while ##rowcount > 0
begin
print #s
select top 1 #s=sequence from myTable where sequence > #s order by sequence;
end
print #s
The above code prints what is expected; a sequence of numbers for each row in the table, but,
declare #s int;
select top 1 #s=sequence from myTable order by sequence;
print #s
while ##rowcount > 0
begin
print #s
select top 1 #s=sequence from myTable where sequence > #s order by sequence;
end
print #s
only prints the first value of sequence twice (one for each PRINT outside of the loop).
I tried reading up on the PRINT statement but found nothing on it affecting ##ROWCOUNT.
My question is, why does this PRINT affect ##ROWCOUNT and why isn't it more clearly documented because this can cause some bugs which are hard to debug?
UPDATE
After more research I did find
Statements such as USE, SET , DEALLOCATE CURSOR, CLOSE CURSOR, PRINT, RAISERROR, BEGIN TRANSACTION, or COMMIT TRANSACTION reset the ROWCOUNT value to 0.
from Microsoft's ##ROWCOUNT docs

##ROWCOUNT always refers to the previous executed statement, even print.
That is why code using ##ROWCOUNT almost always assigns the value to a variable:
declare #s int;
declare #rowcnt int;
select top 1 #s = sequence from myTable;
set #rowcnt = ##ROWCOUNT;
while #rowcnt> 0
. . .

Related

value of the variable ##rowcount

having the following sql script:
declare #table table(id int)
insert into #table values (1),(2)
--print ##rowcount
if 3 < 2
print 'false'
--print ##rowcount
if I'm uncommenting the first print, it will print he value 2, but if I am uncommenting the last print, it will print 0. So the instruction IF is affecting the value of the variable ##ROWCOUNT? Or what is the scope of this variable?
I am using sql server 2014.
The Global variable ##ROWCOUNT will return the number of rows affected by the last statement. Run after the INSERT statement, it will return 2 (rows). Run after the IF statement, it will return the number of rows affected by the IF statement, which is zero.
This also means in the SQL below the 2 ##rowcount statements return 2 then 0 (zero) as the first ##rowcount statement affects zero rows
declare #table table(id int)
insert into #table values (1),(2)
print ##rowcount
print ##rowcount
Yes. ##ROWCOUNT refers to the previous statement and it is constantly being reassigned. This is why it is usually used for assignment to a variable:
declare #rowcnt int;
<whatever>
set #rowcnt = ##ROWCOUNT;
The documentation for ##ROWCOUNT gives examples of how it works in some situations, but doesn't list IF specifically.
However, with a bit of testing, it appears that an IF statement that was not executed resets it to 0. This makes sense if you think of IF as a statement that wraps the statement/block inside it:
if the condition was true, the IF statement affected however many rows the statement inside affected
if the condition was not true, and there is an ELSE block, it affected however many rows the statement inside that affected
if the condition was not true, and there is no ELSE it affects zero rows
Compare:
DECLARE #TEST TABLE(id INT)
INSERT INTO #TEST VALUES (1),(2),(3)
IF 1 = 2
SELECT * FROM #TEST
PRINT ##ROWCOUNT
-- 0, because block didn't run
IF 1 = 1
SELECT * FROM #TEST
PRINT ##ROWCOUNT
-- 3, i.e. number of rows in SELECT
IF 1 = 2
SELECT TOP 1 * FROM #TEST
ELSE
SELECT TOP 2 * FROM #TEST
PRINT ##ROWCOUNT
-- 2, i.e. number of rows in the SELECT executed in the ELSE clause

Why does ##ROWCOUNT return 1 for a NULL statement using sp_executesql?

If I run this:
DECLARE #sql NVARCHAR(10) = NULL;
EXEC sp_executesql #sql;
SELECT ##ROWCOUNT;
I would expect to get 0, maybe even NULL would make sense. But I don't get either, I get 1. Why is 1 row affected by executing a NULL query? If I pass in a "proper" (non_NULL) query then it works fine.
Background (for those that care): this is from a process that is supposed to generate some dynamic SQL to update one row and ONLY one row. I need to check that 1 row has been affected, not 0 or 2 or more than 2. It worked fine until somehow a NULL SQL statement managed to be generated, and this was seen as a success - oops!
The actual fix will be to check the SQL is non-NULL before running it, and treat a NULL statement the same way as a result other than 1. But I was still curious why it behaved this way.
It's because you're assign NULL to your variable. Statements that make a simple assignment always set the ##ROWCOUNT value to 1.
See the example below. Because management studio can run its own queries on the connection and mess with the ##ROWCOUNT value it starts off selecting an empty result set to ensure the initial ##ROWCOUNT value is zero.
When there is no assignment the SELECT ##ROWCOUNT returns 0 (the initial value has not been modified). Otherwise it returns 1
/*Ensure ##ROWCOUNT starts off at 0*/
SELECT 1 WHERE 1 = 0;
DECLARE #sql NVARCHAR(10);
EXEC sp_executesql #sql;
SELECT ##ROWCOUNT;
GO
/*Ensure ##ROWCOUNT starts off at 0*/
SELECT 1 WHERE 1 = 0;
DECLARE #sql NVARCHAR(10) = NULL;
EXEC sp_executesql #sql;
SELECT ##ROWCOUNT;
You can also try similar with a non zero initial value:
/*Ensure ##ROWCOUNT starts off at 3*/
SELECT 1 UNION SELECT 2 UNION SELECT 3
DECLARE #sql NVARCHAR(10);
EXEC sp_executesql #sql;
SELECT ##ROWCOUNT; --Returns 3
GO
/*Ensure ##ROWCOUNT starts off at 3*/
SELECT 1 UNION SELECT 2 UNION SELECT 3
DECLARE #sql NVARCHAR(10) = NULL;
EXEC sp_executesql #sql;
SELECT ##ROWCOUNT; --Returns 1
Please note that my answer was written purely from my experience of SQL Server Management Studio and does not accurately explain this behaviour. Martin Smith has explained why this isn't true in a comment below.
It looks like sp_executesql doesn't run at all with a null parameter, perhaps as a failsafe.
Try running "SELECT ##ROWCOUNT" alone in a batch and you'll see that it returns 1, regardless of the fact there is no current rowcount. It seems likely that 1 is the default return value and that's why you're seeing this.

Why is the ##ROWCOUNT variable returning zero after the IF statement

This is a simple code referring to the pubs database. The SELECT statement returns 10 records so ##ROWCOUNT variable should be set to 10. But how come in the message window, it says '0 records found'. Is there a reason why after an IF statement ##ROWCOUNT is set to 0?
If I put SELECT ##ROWCOUNT right after the WHERE statement, the ##ROWCOUNT variable is at 10. But it changes after executing the IF STATEMENT.
SELECT *
FROM pubs.dbo.employee
WHERE pub_id ='0877'
IF ##ROWCOUNT > 0
PRINT CONVERT(CHAR(2), ##ROWCOUNT) + ' records found'
ELSE
PRINT 'No records found'
##ROWCOUNT returns the row count for the last statement. It is highly volatile. So, basically, anything can reset it.
If you care about it, assign it to a parameter immediately!
DECLARE #ROWCNT INT;
SELECT * FROM pubs.dbo.employee WHERE pub_id = '0877';
SET #ROWCNT = ##ROWCOUNT;
Then use the parameter value.

WAITFOR DELAY doesn't act separately within each WHILE loop

I've been teaching myself to use WHILE loops and decided to try making a fun Russian Roulette simulation. That is, a query that will randomly SELECT (or PRINT) up to 6 statements (one for each of the chambers in a revolver), the last of which reads "you die!" and any prior to this reading "you survive."
I did this by first creating a table #Nums which contains the numbers 1-6 in random order. I then have a WHILE loop as follows, with a BREAK if the chamber containing the "bullet" (1) is selected (I know there are simpler ways of selecting a random number, but this is adapted from something else I was playing with before and I had no interest in changing it):
SET NOCOUNT ON
CREATE TABLE #Nums ([Num] INT)
DECLARE #Count INT = 1
DECLARE #Limit INT = 6
DECLARE #Number INT
WHILE #Count <= #Limit
BEGIN
SET #Number = ROUND(RAND(CONVERT(varbinary,NEWID()))*#Limit,0,1)+1
IF NOT EXISTS (SELECT [Num] FROM #Nums WHERE [Num] = #Number)
BEGIN
INSERT INTO #Nums VALUES(#Number)
SET #Count += 1
END
END
DECLARE #Chamber INT
WHILE 1=1
BEGIN
SET #Chamber = (SELECT TOP 1 [Num] FROM #Nums)
IF #Chamber = 1
BEGIN
SELECT 'you die!' [Unlucky...]
BREAK
END
SELECT
'you survive.' [Phew...]
DELETE FROM #Nums WHERE [Num] = #Chamber
END
DROP TABLE #Nums
This works fine, but the results all appear instantaneously, and I want to add a delay between each one to add a bit of tension.
I tried using WAITFOR DELAY as follows:
WHILE 1=1
BEGIN
WAITFOR DELAY '00:00:03'
SET #Chamber = (SELECT TOP 1 [Num] FROM #Nums)
IF #Chamber = 1
BEGIN
SELECT 'you die!' [Unlucky...]
BREAK
END
SELECT
'you survive.' [Phew...]
DELETE FROM #Nums WHERE [Num] = #Chamber
END
I would expect the WAITFOR DELAY to initially cause a 3 second delay, then for the first SELECT statement to be executed and for the text to appear in the results grid, and then, assuming the live chamber was not selected, for there to be another 3 second delay and so on, until the live chamber is selected.
However, before anything appears in my results grid, there is a delay of 3 seconds per number of SELECT statements that are executed, after which all results appear at the same time.
I tried using PRINT instead of SELECT but encounter the same issue.
Clearly there's something I'm missing here - can anyone shed some light on this?
It's called buffering. The server doesn't want to return an only partially full response because most of the time, there's all of the networking overheads to account for. Lots of very small packets is more expensive than a few larger packets1.
If you use RAISERROR (don't worry about the name here where we're using 10) you can specify NOWAIT to say "send this immediately". There's no equivalent with PRINT or returning result sets:
SET NOCOUNT ON
CREATE TABLE #Nums ([Num] INT)
DECLARE #Count INT = 1
DECLARE #Limit INT = 6
DECLARE #Number INT
WHILE #Count <= #Limit
BEGIN
SET #Number = ROUND(RAND(CONVERT(varbinary,NEWID()))*#Limit,0,1)+1
IF NOT EXISTS (SELECT [Num] FROM #Nums WHERE [Num] = #Number)
BEGIN
INSERT INTO #Nums VALUES(#Number)
SET #Count += 1
END
END
DECLARE #Chamber INT
WHILE 1=1
BEGIN
WAITFOR DELAY '00:00:03'
SET #Chamber = (SELECT TOP 1 [Num] FROM #Nums)
IF #Chamber = 1
BEGIN
RAISERROR('you die!, Unlucky',10,1) WITH NOWAIT
BREAK
END
RAISERROR('you survive., Phew...',10,1) WITH NOWAIT
DELETE FROM #Nums WHERE [Num] = #Chamber
END
DROP TABLE #Nums
As Larnu already aluded to in comments, this isn't a good use of T-SQL.
SQL is a set-oriented language. We try not to write procedural code (do this, then do that, then run this block of code multiple times). We try to give the server as much as possible in a single query and let it work out how to process it. Whilst T-SQL does have language support for loops, we try to avoid them if possible.
1I'm using packets very loosely here. Note that it applies the same optimizations no matter what networking (or no-networking-local-memory) option is actually being used to carry the connection between client and server.

In T-SQL / SQL Server 2000, referencing a particular row of a result set

I want to reference the nth row of the #temptable (at the second SQL comment is below). What expression will allow me to do so?
DECLARE #counter INT
SET #counter = 0
WHILE (#counter<count(#temptable))
--#temptable has one column and 0 or more rows
BEGIN
DECLARE #variab INT
EXEC #variab = get_next_ticket 3906, 'n', 1
INSERT INTO Student_Course_List
SELECT #student_id,
-- nth result set row in #temptable, where n is #count+1
#variab
SET #counter = #counter +1
END
Cursor (will this work?):
for record in (select id from #temptable) loop
--For statements, use record.id
end loop;
Normally in a relational database like SQL Server, you prefer to do set operations. So it would be best to simply have INSERT INTO tbl SOMECOMPLEXQUERY even with very complex queries. This is far preferable to row processing. In a complex system, using a cursor should be relatively rare.
In your case, it would appear that the get_next_ticket procedure performs some significant logic which is not able to be done in a set-oriented fashion. If you cannot perform it's function in an alternative set-oriented way, then you would use a CURSOR.
You would declare a CURSOR on your set SELECT whatever FROM #temptable, OPEN it, FETCH from the cursor into variables for each column and then use them in the insert.
Instead of using a while loop (with a counter like you are doing) to iterate the table you should use a cursor
Syntax would be:
DECLARE #id int
DECLARE c cursor for select id from #temptable
begin
open c
fetch next from c into #id
WHILE (##FETCH_STATUS = 0)
BEGIN
--Do stuff here
fetch next from c into #id
END
close c
deallocate c
end