Update SQL variable when recordset is empty - tsql

We have a loop in SQL Server 2005 that loops around on a table getting each items parent until it gets to the top of the tree:
DECLARE #T Table
(
ItemID INT NOT NULL PRIMARY KEY,
AncestorID INT NULL
)
Which has data like this:
ItemID | AncestorID
1 2
2 3
3 4
4 NULL
We have a loop that basically does this:
DECLARE #AncestorID INT
SELECT #AncestorID = 1
WHILE (#AncestorID IS NOT NULL)
BEGIN
--Do some work
SELECT #AncestorID = T.AncestorID
FROM #T t
WHERE T.ItemID = #AncestorID
print #AncestorID
END
(Yes I know SQL is set based, and this is processing row by row, the "Do some work" needs to be done line by line for a reason).
This has always worked fine until today when we ended up in an endless loop. Turns out the cause was some wrong data:
ItemID | AncestorID
1 2
2 3
4 NULL
ItemID 3 was deleted. The loop now never ends because AncestorID is never NULL - it stays at 3.
Is there anyway to rewrite the select statement to make #AncestorID null if the SELECT query returns 0 rows, or do I need to have a separate SELECT statement to count the records and some IF ELSE type logic?

Is there anyway to rewrite the select statement to make #AncestorID
null if the SELECT query returns 0 rows,
You can use an aggregate on T.AncestorID.
SELECT #AncestorID = min(T.AncestorID)
FROM #T t
WHERE T.ItemID = #AncestorID

You could use another variable, e.g. #PreviousAncestorId, to hold the previous value and reset #AncestorId to NULL before the query.
You could check ##RowCount after the query to see if a row was found.
The code will still have issues dealing with cycles of arbitrary length within the data, e.g. a row where both values are the same. You would need to keep track of the visited rows in order to detect cycles. A simple reality check would be to count the number of iterations of the loop and check it against the number of rows.

Use a Break
e.g.
WHILE (#AncestorID IS NOT NULL)
BEGIN
SELECT T.AncestorID INTO #TEMP
FROM #T t WHERE T.ItemID = #AncestorID
IF((SELECT COUNT(*) FROM #TEMP) = 0) BREAK;
SELECT #AncestorID=T.AncestorID
FROM #TEMP t
print #AncestorID
DROP TABLE #TEMP
END

Related

Postgres 14 delete with count in where clause

I wanted to delete all records except the one with the highest value so I did
CREATE TABLE code (
id SERIAL,
name VARCHAR(255) NOT NULL ,
value int NOT NULL
);
INSERT INTO code (name,value) VALUES ('name',1);
INSERT INTO code (name,value) VALUES ('name',2);
INSERT INTO code (name,value) VALUES ('name',3);
INSERT INTO code (name,value) VALUES ('name1',3);
INSERT INTO code (name,value) VALUES ('name2',1);
INSERT INTO code (name,value) VALUES ('name2',3);
Example I want to delete all records except the one with the highest value on value column
I am expecting to get result as:
name 3
name1 3
name2 3
I tried doing
DELETE FROM code where value != (select MAX(value) value from code where count(code) > 1)
But I'm getting an error like:
ERROR: aggregate functions are not allowed in WHERE
LINE 1: ...value != (select MAX(value) value from code where count(code...
With everyone's idea and combine with this
SELECT dept, SUM(expense) FROM records
WHERE ROW(year, dept) IN (SELECT x, y FROM otherTable)
GROUP BY dept;
link
I was able to make the query I want
Demo
Your query makes no sense. Try this:
DELETE FROM code
where value <> (select value
FROM (SELECT count(*) AS count,
value
from code
GROUP BY value) AS q
ORDER BY count DESC
FETCH FIRST 1 ROWS ONLY);
The fast and easy solution would be:
BEGIN;
SELECT name,max(value) INTO temp t FROM code group by 1;
TRUNCATE code;
insert into code SELECT * FROM t;
END;
Or you can do like:
BEGIN;
DELETE FROM code USING (SELECT name,max(value) FROM code group by 1) a WHERE code.name=a.name AND code.value!=a.max;
END;

The difference between PRINT ##ROWCOUNT and OUTPUT $ACTION in sql server

Apologies if my question seems to be naive:
I cannot get my head around the 2 statements below, can someone please explain the difference:
OUTPUT $ACTION, INSERTED.BuildRequestID, ..... and
PRINT ##ROWCOUNT
Apparently, they both can be used to print something on the window, with output in the example above, the records that have been inserted will be displayed. And, PRINT ##ROWCOUNT returns the number of rows affected by the last executed statement in the batch, so, if the function was insert, then it will show the inserted records?
Thank you,
In its simplest terms, OUTPUT will give you the actual records affected by a DML statement (INSERT, UPDATE, DELETE, MERGE), ##ROWCOUNT will just tell you how many rows were affected by the previous Statement (not limited to DML).
This is probably easiest understood with a working example that you can run yourself and see both in action:
IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL
DROP TABLE #T;
-- CHECK ##ROWCOUNT
DECLARE #RowCountFromDropTable INT = ##ROWCOUNT;
-- CREATE A TABLE
CREATE TABLE #T (ID INT NOT NULL PRIMARY KEY, Col CHAR(1) NOT NULL);
-- INSERT SOME VALUES AND CHECK THE OUTPUT
INSERT #T (ID, Col)
OUTPUT inserted.*
VALUES (1, 'A'), (2, 'B'), (3, 'C');
-- CHECK ##ROWCOUNT
DECLARE #RowCountFromInsert INT = ##ROWCOUNT;
-- DELETE A VALUE AND INSPECT THE DELETED RECORD WITH OUTPUT
DELETE #T
OUTPUT deleted.*
WHERE ID = 3;
-- CHECK ##ROWCOUNT
DECLARE #RowCountFromDelete INT = ##ROWCOUNT;
-- UPDATE A RECORD AND VIEW BEFORE AND AFTER VALUES
UPDATE #T
SET Col = 'X'
OUTPUT inserted.ID AS ID,
inserted.Col AS UpdatedTo,
deleted.Col AS UpdatedFrom
WHERE ID = 2;
-- CHECK ##ROWCOUNT
DECLARE #RowCountFromUpdate INT = ##ROWCOUNT;
-- USE MERGE, AND CAPTURE ACTION:
MERGE #T AS t
USING (VALUES (2, 'B'), (3, 'C')) AS s (ID, Col)
ON s.ID = t.ID
WHEN NOT MATCHED THEN INSERT (ID, Col) VALUES (s.ID, s.Col)
WHEN MATCHED THEN UPDATE SET Col = s.Col
WHEN NOT MATCHED BY SOURCE THEN DELETE
OUTPUT $Action AS DMLAction,
inserted.ID AS InsertedID,
inserted.Col AS InsertedCol,
deleted.ID AS DeletedID,
deleted.Col AS DeletedCol;
-- CHECK ##ROWCOUNT
DECLARE #RowCountFromMerge INT = ##ROWCOUNT;
SELECT RowCountFromDropTable = #RowCountFromDropTable,
RowCountFromInsert = #RowCountFromInsert,
RowCountFromDelete = #RowCountFromDelete,
RowCountFromUpdate = #RowCountFromUpdate,
RowCountFromMerge = #RowCountFromMerge;
The recordsets output from each of the DML are:
INSERT
ID Col
-------
1 A
2 B
3 C
DELETE
ID Col
-------
3 C
UPDATE
ID UpdatedTo UpdatedFrom
---------------------------
2 X B
MERGE
DMLAction InsertedID InsertedCol DeletedID DeletedCol
------------------------------------------------------------
INSERT 3 C NULL NULL
DELETE NULL NULL 1 A
UPDATE 2 B 2 X
INSPECT ##ROWCOUNTS
RowCountFromDropTable RowCountFromInsert RowCountFromUpdate RowCountFromMerge
--------------------------------------------------------------------------------
0 3 1 3
A quick point on some wording in the qeustion too: You cannot use OUTPUT directly to print something to the window, it returns records much like a SELECT statement. ##ROWCOUNT can be used like any scalar function, so you could use this in consecutive statements. So you could do something like this:
SELECT TOP (1) *
FROM (VALUES (1), (2), (3)) AS t (ID);
SELECT TOP (##ROWCOUNT + 1) *
FROM (VALUES (1), (2), (3)) AS t (ID);
SELECT TOP (##ROWCOUNT + 1) *
FROM (VALUES (1), (2), (3)) AS t (ID);
Which returns 1, 1,2 and 1,2,3 respectively. I have no idea why you would want to do this, but it demonstrates the scope of ##ROWCOUNT a bit better than the above, and how it can be used elsewhere.

Check if the row is the last one when looping through result set in PL/pgSQL

Is something like this possible?
FOR row_var IN SELECT * FROM my_table LOOP
-- ...
IF is_last_row THEN
-- do something...
END IF;
END LOOP
The only thing that goes into my mind now is to select a count of the rows and compare it with row_number() in the loop.
There is a very cheap and simple way. Your row variable row_var still holds the last row after the loop ends. Just use it then:
FOR row_var IN
SELECT * FROM my_table ORDER BY ???
LOOP
-- do something for every row here
END LOOP;
-- do something with row_var for the last row here
Aside from that, there is often a more efficient solution with plain SQL, depending on the undisclosed details of your use case ...
If you selects rows ordered by a unique key, querying last_id should be cheaper than count() with row_number(), e.g.:
last_id := (select id from test order by id desc);
for rec in
select * from test order by id
loop
if rec.id = last_id then
...
You don't need new variable nor addition query for count().
If we assume you have primary key in table my_table in column id you can add one column with WINDOW FUNCTION:
FOR row_var IN SELECT lead(id) OVER() IS NULL AS is_last_row, * FROM my_table LOOP
-- ...
IF row_var.is_last_row THEN
-- do something...
END IF;
END LOOP
If you have composite PK or have not PK at all on that table you may use:
SELECT lead(r) OVER() IS NULL AS is_last_row, *
FROM (
SELECT 1 as r, * FROM ruch.s_icd10
) sub
as LOOP query. That's only idea I have.

Run insert statement x number of times

I have two tables. One table A has n rows of data and the other table B is empty. I want to insert n rows into table B, 1 row for each row in table A. Table B will have a couple of fields from table A in it, including a foreign key from table A.
In the end I want one row in B for each row in A. To do this I used:
INSERT INTO B(Col1
,Col2
,Col3
,Col4
,Col5
);
SELECT 100
,25
,'ABC'
,1
,A.ID
FROM Auctions A
Now, I've put this code in a stored procedure and this SP takes an int param called NumInserts.
I want to insert n * NumInserts rows. So, if n is 10 and NumInserts is 5 I want to run this code 5 * 10 (50) times.
In other words for each row in table A I want to insert 5 rows in table B. How would I do that?
create procedure insert_into_b
#numInserts int
as
begin
while #numInserts > 0
begin
insert into b (id)
select id from a
set #numInserts = #numInserts - 1
end
end
exec insert_into_b 2
This is a hack and I wouldn't recommend using it in production or big volumes of data. However, in development quick-and-dirty scenarios I found it often useful:
Use GO \[count\] to execute a batch of commands a specified number of times.
Concretely, if you had a stored procedure called InsertAIntoB, you could run this in Management Studio:
exec InsertAIntoB
GO 10
(replace 10 with whatever NumInserts is)
I prefer to avoid looping when I can, just so I don't have to maintain some easily breakable and somewhat ugly loop structure in my stored procedure.
You could easily do this with a Numbers table, the CROSS APPLY statement, and your existing INSERT statement.
Given that your numbers table would look like this:
Number
======
0
1
2
...
Your SQL statement simply becomes:
INSERT INTO B
(
[Col1]
,[Col2]
,[Col3]
,[Col4]
,[Col5]
)
SELECT
100
,25
,'ABC'
,1
,a.ID
FROM
Auctions a
CROSS APPLY
Numbers n
WHERE
n.Number BETWEEN 1 AND #NumInserts
Numbers tables can be useful if use appropriately. If you're unfamiliar with them, here are a few resources and some pros/cons:
http://dataeducation.com/you-require-a-numbers-table/ (the code to create a numbers table in this article is shown below)
http://archive.msdn.microsoft.com/SQLExamples/Wiki/View.aspx?title=NumbersTable
https://dba.stackexchange.com/questions/11506/why-are-numbers-tables-invaluable
Maybe this solution is overkill if #NumInserts is always going to be a reasonably small number, but if you already have a Numbers table sitting around, you might as well take advantage of it!
UPDATE:
Here's a quick and dirty method to populate a numbers table from 0 to 65,535:
CREATE TABLE Numbers
(
Number INT NOT NULL,
CONSTRAINT PK_Numbers
PRIMARY KEY CLUSTERED (Number)
WITH FILLFACTOR = 100
)
GO
INSERT INTO Numbers
SELECT
(a.Number * 256) + b.Number AS Number
FROM
(
SELECT number
FROM master..spt_values
WHERE
type = 'P'
AND number <= 255
) a (Number),
(
SELECT number
FROM master..spt_values
WHERE
type = 'P'
AND number <= 255
) b (Number)
GO
Credit: http://dataeducation.com/you-require-a-numbers-table/
Create procedure DoitNTimes
#N integer = 1
As
Set NoCount On
While #N > 0 Begin
Insert B (Col1, Col2, Col3, Col4, Col5)
Select 100, 25, 'ABC', 1, A.ID
From Auctions A
-- -----------------------------------
Set #N -= 1
End
If using SQL Server 2005 or earlier replace the Set #N -= 1' withSet #N = #N-1`
and if you really want to avoid loop using T-SQL variables, then use a CTE, not a disk-based table:
Create procedure DoitNTimes
#N integer = 1
As
Set NoCount On
With nums(num) As
(Select #N Union All
Select num - 1
From nums
Where num > 1)
Insert B (Col1, Col2, Col3, Col4, Col5)
Select 100, 25, 'ABC', 1, A.ID
From Auctions A Full Join nums
Option(MaxRecursion 10000)
but of course, this is also still looping, just like any solution to this issue.
Very late answer but there is no need to loop and it's a little simpler than Corey's good answer;
DECLARE #n int = 10;
INSERT INTO B(Col1,Col2,Col3,Col4,Col5);
SELECT 100,25,'ABC',1,A.ID
FROM Auctions A
JOIN (SELECT TOP(#n) 1 [junk] FROM sys.all_objects) as copies ON 1 = 1
You could use any table in the join as long as it has the number of rows you'll need. You could also change "1 [junk]" to "ROW_NUMBER() OVER(ORDER BY object_id) [copyno]" if you wanted a copy number somewhere in the insert table.
Hopefully this will save someone a little work down the road...
Try this (on SQL server databases):
DECLARE #NumInserts SMALLINT = 3
INSERT INTO B (Col1, Col2, Col3, Col4, Col5)
SELECT 100, 25, 'ABC', 1, A.ID
FROM Auctions A
JOIN master.dbo.spt_values numbers ON numbers.number < #NumInserts
WHERE numbers.[type] = 'P'
Note: This will only work if #NumInserts is less than or equal to 2048
master.dbo.spt_values WHERE type = 'P' is just a built-in SQL Server table of numbers from 0 to 2047

In SQL Server 2000, how to delete the specified rows in a table that does not have a primary key?

Let's say we have a table with some data in it.
IF OBJECT_ID('dbo.table1') IS NOT NULL
BEGIN
DROP TABLE dbo.table1;
END
CREATE TABLE table1 ( DATA INT );
---------------------------------------------------------------------
-- Generating testing data
---------------------------------------------------------------------
INSERT INTO dbo.table1(data)
SELECT 100
UNION ALL
SELECT 200
UNION ALL
SELECT NULL
UNION ALL
SELECT 400
UNION ALL
SELECT 400
UNION ALL
SELECT 500
UNION ALL
SELECT NULL;
How to delete the 2nd, 5th, 6th records in the table? The order is defined by the following query.
SELECT data
FROM dbo.table1
ORDER BY data DESC;
Note, this is in SQL Server 2000 environment.
Thanks.
In short, you need something in the table to indicate sequence. The "2nd row" is a non-sequitur when there is nothing that enforces sequence. However, a possible solution might be (toy example => toy solution):
If object_id('tempdb..#NumberedData') Is Not Null
Drop Table #NumberedData
Create Table #NumberedData
(
Id int not null identity(1,1) primary key clustered
, data int null
)
Insert #NumberedData( data )
SELECT 100
UNION ALL SELECT 200
UNION ALL SELECT NULL
UNION ALL SELECT 400
UNION ALL SELECT 400
UNION ALL SELECT 500
UNION ALL SELECT NULL
Begin Tran
Delete table1
Insert table1( data )
Select data
From #NumberedData
Where Id Not In(2,5,6)
If ##Error <> 0
Commit Tran
Else
Rollback Tran
Obviously, this type of solution is not guaranteed to work exactly as you want but the concept is the best you will get. In essence, you stuff your rows into a table with an identity column and use that to identify the rows to remove. Removing the rows entails emptying the original table and re-populating with only the rows you want. Without a unique key of some kind, there just is no clean way of handling this problem.
As you are probably aware you can do this in later versions using row_number very straightforwardly.
delete t from
(select ROW_NUMBER() over (order by data) r from table1) t
where r in (2,5,6)
Even without that it is possible to use the undocumented %%LOCKRES%% function to differentiate between 2 identical rows
SELECT data,%%LOCKRES%%
FROM dbo.table1`
I don't think that's available in SQL Server 2000 though.
In SQL Sets don't have order but cursors do so you could use something like the below. NB: I was expecting to be able to use DELETE ... WHERE CURRENT OF but that relies on a PK so the code to delete a row is not as simple as I was hoping for.
In the event that the data to be deleted is a duplicate then there is no guarantee that it will delete the same row as CURRENT OF would have. However in this eventuality the ordering of the tied rows is arbitrary anyway so whichever row is deleted could equally well have been given that row number in the cursor ordering.
DECLARE #RowsToDelete TABLE
(
rowidx INT PRIMARY KEY
)
INSERT INTO #RowsToDelete SELECT 2 UNION SELECT 5 UNION SELECT 6
DECLARE #PrevRowIdx int
DECLARE #CurrentRowIdx int
DECLARE #Offset int
SET #CurrentRowIdx = 1
DECLARE #data int
DECLARE ordered_cursor SCROLL CURSOR FOR
SELECT data
FROM dbo.table1
ORDER BY data
OPEN ordered_cursor
FETCH NEXT FROM ordered_cursor INTO #data
WHILE EXISTS(SELECT * FROM #RowsToDelete)
BEGIN
SET #PrevRowIdx = #CurrentRowIdx
SET #CurrentRowIdx = (SELECT TOP 1 rowidx FROM #RowsToDelete ORDER BY rowidx)
SET #Offset = #CurrentRowIdx - #PrevRowIdx
DELETE FROM #RowsToDelete WHERE rowidx = #CurrentRowIdx
FETCH RELATIVE #Offset FROM ordered_cursor INTO #data
/*Can't use DELETE ... WHERE CURRENT OF as here that requires a PK*/
SET ROWCOUNT 1
DELETE FROM dbo.table1 WHERE (data=#data OR data IS NULL OR #data IS NULL)
SET ROWCOUNT 0
END
CLOSE ordered_cursor
DEALLOCATE ordered_cursor
To perform any action on a set of rows (such as deleting them), you need to know what identifies those rows.
So, you have to come up with criteria that identifies the rows you want to delete.
Providing a toy example, like the one above, is not particularly useful.
You plan ahead and if you anticipate this is possible you add a surrogate key column or some such.
In general you make sure you don't create tables without PK's.
It's like asking "Say I don't look both directions before crossing the road and I step in front of a bus."