Are these SQL Server procedures concurrency safe? - sql-server

I have the following table which contains units of work:
create table [dbo].[Queue]
(
[QueueId] bigint not null identity(1,1),
[UserId] bigint default null, -- irrelevant foreign key here
primary key clustered ([QueueId] asc)
);
go
A pack of workers keeps circling this table and grabs one or more units of work by changing the UserId field from null to a positive value. No two workers should be able to update the same QueueId at the same time and they should not wait (readpast should help with that).
The following method makes it easy to add work to the table:
/**
* Push some work units.
* (rewritten from basic while insert to #Larnu's Tally suggestion)
*/
create procedure [dbo].[spPushWork]
#Count int
as
begin
if #Count < 1 or #Count > 1000000 throw 50001, N'#Count must be 1-1M.', 1;
with [num] as
(
select [num] from (values (null),(null),(null),(null),(null),(null),(null),(null),(null),(null)) [num]([num])
), [tally] as
(
select top (#Count)
row_number() over (order by (select null)) as [ind]
from [num] [num1]
cross join [num] [num2]
cross join [num] [num3]
cross join [num] [num4]
cross join [num] [num5]
cross join [num] [num6]
)
merge into [dbo].[queue]
using (select [ind] from [tally]) [t]
on 1 = 0
when not matched then insert default values;
end
go
And now we have 2 methods to grab work.
Method #1 is thread safe (I hope) as it's a select-update combo:
/**
* This grabs work units in a single operation (Select + Update).
*/
create procedure [dbo].[spGrabSafe]
#UserId bigint
,#Count int = 1
as
begin
if #UserId < 1 throw 50001, N'#UserId must be 1+.', 1;
if #Count < 1 throw 50001, N'#Count must be 1+.', 2;
declare #Ids table ([Id] bigint not null);
-- fetch and claim via single query
with [cte] as
(
select top(#Count) [QueueId]
from [dbo].[Queue] with (readpast) -- skip locked
where [UserId] is null
order by [QueueId] asc
)
update [q]
set [UserId] = #UserId
output [inserted].[QueueId] into #Ids
from [dbo].[Queue] [q]
join [cte] on [cte].[QueueId] = [q].[QueueId];
select [Id] from #Ids;
end;
go
Method #2 does the same thing as method one in 2 operations by first locking the rows and then claiming them by changing the UserId. It also has a delay argument that allows us to make it run longer for testing:
/**
* This grabs work units in multiple operations (Select&lock + Update).
*/
create procedure [dbo].[spGrabUnsafe]
#UserId bigint
,#Count int = 1
,#Delay time = null
as
begin
if #UserId < 1 throw 50001, N'#UserId must be 1+.', 1;
if #Count < 1 throw 50001, N'#Count must be 1+.', 1;
declare #Ids table ([Id] bigint not null);
begin transaction
-- fetch the QueueId's
insert into #Ids
select top(#Count) [QueueId]
from [dbo].[Queue]
with (xlock, rowlock, readpast) -- lock rows + skip locked
where [UserId] is null
order by [QueueId] asc;
-- claim via UserId
update [q]
set [UserId] = #UserId
from [dbo].[Queue] [q]
join #Ids [ids] on [ids].[Id] = [q].[QueueId];
-- this allows to wait a bit to test concurrency
if #Delay is not null
begin
declare #Time varchar(12) = convert(varchar, #Delay, 114);
waitfor delay #Time;
end;
commit transaction;
select [Id] from #Ids;
end
go
Is method #2 safe in a concurrent environment? There is a gap between selecting and updating UserId. But the rows should be locked...

Related

Why does a join on ROW_NUMBER() between common table expressions result in a Cartesian product?

I am joining two common table expressions (cte) based on an id created within each cte using ROW_NUMBER(). The resulting execution plan includes a Merge Join where the Estimated Rows are 13,530,000,000 in my case; and the actual number of rows ends up being about 13,000.
Below is an example of what I am trying to do, and this results in a similar execution plan with 1,200,000,000 estimated rows in the Merge Join, and 12,000 actual rows.
I am stuck understanding why what seems like a simple join is creating a Cartesian product.
IF OBJECT_ID('tempdb..#Product') IS NOT NULL DROP TABLE #Product;
IF OBJECT_ID('tempdb..#Product_Id') IS NOT NULL DROP TABLE #Product_Id;
CREATE TABLE #Product (
[rowId] int identity primary key,
[id] int
)
CREATE TABLE #Product_Id (
[id] int primary key
)
DECLARE #Id int
SET #Id = 500000
WHILE #Id <= 600000
BEGIN
INSERT INTO #Product_Id
VALUES (#Id)
SET #Id += 1
END
SET #Id = 1
WHILE #Id <= 12000
BEGIN
INSERT INTO #Product
VALUES (NULL)
SET #Id += 1
END
WITH
PRODUCT_ID_CTE AS (
SELECT [id],
ROW_NUMBER() OVER(ORDER BY [id]) AS [rn]
FROM #Product_Id
),
PRODUCT_CTE AS (
SELECT [id],
ROW_NUMBER() OVER(ORDER BY [rowId]) AS [rn]
FROM #Product
)
SELECT ISNULL(a.[id], b.[id]) AS [id]
FROM PRODUCT_CTE a
JOIN PRODUCT_ID_CTE b
ON a.[rn] = b.[rn]
Execution plan with CTE
Execution plan with sub queries

How to get rid of timeout error when using mssql hierarchy check constrint

i am creating sql hirercky table
Here is my code;
The Constraint Function Code
alter Function Accounts.Types_Sub_Check_fn (#ID uniqueidentifier, #Sub Uniqueidentifier) returns int
begin
--declare #id uniqueidentifier = '8c7d4151-246c-476c-adf6-964ca9afdd3c' declare #sub uniqueidentifier = '47c2b6da-25fc-4921-adfa-b1f635bddde6'
declare #a int
declare #b int =(iif(#ID=#SUB,2,0))
;with cte(id, lvl) as
(
select f.sub,
1
from Accounts.Types as f
where f.id = #id
union all
select f.sub,
lvl + 1
from Accounts.Types as f
inner join cte as c
on f.id = c.id
)
select #a = (select count (*)
from cte
where id =#sub) + #b
option (maxrecursion 0)
return #a
end
go
The Table code
create Table Accounts.Types
(
ID uniqueidentifier not null CONSTRAINT DF_Accounts_Types_ID DEFAULT newid() CONSTRAINT PK_Accounts_Types_ID PRIMARY KEY NONCLUSTERED (ID) ,
Name varchar(200) not null CONSTRAINT UQ_Accounts_Types_NAME UNIQUE (NAME),
Sub uniqueidentifier CONSTRAINT FK_Accounts_Types_Sub Foreign key references Accounts.Types ,
Ctype uniqueidentifier CONSTRAINT FK_Accounts_Types_Ctype Foreign key references Accounts.Types ,
insert_time datetime not null CONSTRAINT DF_Accounts_Types_Insert_Time DEFAULT getdate() ,
insert_user uniqueidentifier CONSTRAINT DF_Accounts_Types_Insert_User DEFAULT'9EC66F53-9233-4A6C-8933-F8417D2BB5A9' ,
ts timestamp,
INDEX IX_Accounts_Types_NAME#ASC CLUSTERED (Name ASC),
Constraint Check_Accounts_Types_Sub check (Accounts.Types_Sub_Check_fn(ID,Sub)<=1)
)
go
This function will give 2 as result if trying to insert itseft as parent (in sub column)
it will give 1 if its already a child, which trying to insert as its parent
The Check constraint is created to check if the the parent (sub column) for any id should not be its child or grand child,
and itself cannot be its parent
When i try to insert a data which does not match the check constraint, it stuck, and give a timeout error,
eg:
insert into Accounts.Types (ID, Name, Sub)
values ('607936b9-6f95-4989-8ebe-87a08807f43e','LLL','607936b9-6f95-4989-8ebe-87a08807f43e')
this will give timeout
can anyone help me out, i need to get rid of time out error; get the constraint error only
Easy question - when will your recursion end when your ID and Sub are the same values and you don't limit maxrecursion or lvl? Never. It'll never end.
values ('607936b9-6f95-4989-8ebe-87a08807f43e','LLL','607936b9-6f95-4989-8ebe-87a08807f43e')
You have to remove rows where ID = Sub or add maxrecursion or add level limit or normalize your table.
alter Function Accounts.Types_Sub_Check_fn (#ID uniqueidentifier, #Sub Uniqueidentifier) returns int
begin
--declare #id uniqueidentifier = '00279c6b-df00-4144-810d-571fdb1c5109' declare #sub uniqueidentifier = 'bc887e7b-36d2-4ece-8ec1-720dc81a9de4'
declare #a int = 0
declare #b int =(iif(#ID=#SUB,2,0))
if #ID <> #sub
begin
;with cte(id, lvl) as
(
select f.Sub ,
1
from Accounts.Types as f
where f.id = #sub
union all
select iif(f.Sub = #sub, Null, f.sub),
lvl + 1
from Accounts.Types as f
inner join cte as c
on f.id = c.id
)
select #a = (select count (*)
from cte
where id =#id)
option (maxrecursion 0);
end
-- select #a + #b
return #a + #b
end
go

How to make this the sample SQL Atomic?

I have an store procedure,
ALTER PROCEDURE [dbo].[InsertDealGallery]
(
#ImageID INT,
#DealID INT
)
AS
DECLARE #MaxOrder INT;
SELECT #MaxOrder = MAX([Order]) + 1 FROM DealGallery WHERE DealID = #DealID
IF (#MaxOrder IS NULL)
BEGIN
SET #MaxOrder = 1;
END
INSERT INTO [DealGallery]
([ImageID]
,[DealID]
,[Order])
VALUES
(#ImageID
,#DealID
,#MaxOrder)
But I am fearing that this is not atomic because in the same time MaxOrder might remain same in concurrent thread. SO how to make this atomic?
Transactions is the feature to obtain atomic behavior. You know that a from ACID is for Atomic.
But you have choiced a very bad design, the aggegration function can lock all rows, performance becomes very poor.
Edited
You should move to identitat data type. Also another techniques exists, like counter table, but easy way is identity.
To enclose code into a transaction you should to include 'Begin transaction' and 'commit' sentences.
Edited 2
This approach, with a counter for each Deal, will avoid lock all DealGallery's rows, only lock DealGalleryCounter row.
CREATE TABLE DealGalleryCounter
(DealID INT not null primary key,
order int default 0
); -- Or add column to an existing deal table.
ALTER PROCEDURE [dbo].[InsertDealGallery]
(
#ImageID INT,
#DealID INT
)
AS BEGIN
DECLARE #order int
begin transaction
set transaction isolation level serializable
-- repeatable read is enough --
select #order = order + 1
from DealGalleryCounter
where #DealID = DealID
if #order is null
insert into DealGalleryCounter (DealID) values (#DealID)
INSERT INTO dbo.DealGallery (ImageID, DealID, [Order])
VALUES ( #ImageID, #DealID, #order );
update DealGallery
set DealGalleryCounter = #order
where #DealID = DealID;
commit; --or check for errors and rollback
END
Try this one -
ALTER PROCEDURE [dbo].[InsertDealGallery]
(
#ImageID INT,
#DealID INT
)
AS BEGIN
INSERT INTO dbo.DealGallery (ImageID, DealID, [Order])
SELECT
#ImageID
, #DealID
, ISNULL(MAX(d.[Order]) + 1, 1)
FROM (SELECT a = 1) t
LEFT JOIN dbo.DealGallery d ON d.DealID = #DealID
END
Or try this one -
INSERT INTO dbo.DealGallery (ImageID, DealID, [Order])
SELECT
#ImageID
, #DealID
, ISNULL(m, a)
FROM (SELECT a = 1) t
LEFT JOIN (
SELECT m = MAX(d.[Order]) + 1
FROM dbo.DealGallery d
WHERE d.DealID = #DealID
) t2 ON 1 = 1
This should work: insert max order increased by one when deal id is found in DealGallery table otherwise insert max order as 0
BEGIN TRAN
INSERT INTO [DealGallery]
([ImageID]
,[DealID]
,[Order])
SELECT #ImageID, #DealID, MAX([Order]) + 1
FROM DealGallery WHERE DealID = #DealID
UNION
SELECT #ImageID, #DeadID, 1
FROM (SELECT 1 AS C) AS T
WHERE NOT EXISTS (SELECT * FROM DealGallery WHERE DealID = #DealID)
COMMIT TRAN

Need to speed up SQL Server SP that uses system metadata

Let me apologize in advance for the length of this question. I don't see how to ask it without giving all the definitions.
I've inherited a SQL Server 2005 database that includes a homegrown implementation of change tracking. Through triggers, changes to virtually every field in the database are stored in a set of three tables. In the application for this database, the user can request the history of various items, and what's returned is not just changes to the item itself, but also changes in related tables. The problem is that in some cases, it's painfully slow, and in some cases, the request eventually crashes the application. The client has also reported other users having problems when someone requests history.
The tables that store the change data are as follows:
CREATE TABLE [dbo].[tblSYSChangeHistory](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[date] [datetime] NULL,
[obj_id] [int] NULL,
[uid] [varchar](50) NULL
This table tracks the tables that have been changed. Obj_id is the value that Object_ID() returns.
CREATE TABLE [dbo].[tblSYSChangeHistory_Items](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[h_id] [bigint] NOT NULL,
[item_id] [int] NULL,
[action] [tinyint] NULL
This table tracks the items that have been changed. h_id is a foreign key to tblSYSChangeHistory. item_id is the PK of the changed item in the specified table. action indicates insert, delete or change.
CREATE TABLE [dbo].[tblSYSChangeHistory_Details](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[i_id] [bigint] NOT NULL,
[col_id] [int] NOT NULL,
[prev_val] [varchar](max) NULL,
[new_val] [varchar](max) NULL
This table tracks the individual changes. i_id is a foreign key to tblSYSChangeHistory_Items. col_id indicates which column was changed, and prev_val and new_val indicate the original and new values for that field.
There's actually a fourth table that supports this architecture. tblSYSChangeHistory_Objects maps plain English descriptions of operations to particular tables in the database.
The code to look up the history for an item is incredibly convoluted. It's one branch of a very long SP. Relevant parameters are as follows:
#action varchar(50),
#obj_id bigint = 0,
#uid varchar(50) = '',
#prev_val varchar(MAX) = '',
#new_val varchar(MAX) = '',
#start_date datetime = '',
#end_date datetime = ''
I'm storing them to local variables right away (because I was able to significantly speed up another SP by doing so):
declare #iObj_id bigint,
#cUID varchar(50),
#cPrev_val varchar(max),
#cNew_val varchar(max),
#tStart_date datetime,
#tEnd_date datetime
set #iObj_id = #obj_id
set #cUID = #uid
set #cPrev_val = #prev_val
set #cNew_val = #new_val
set #tStart_date = #start_date
set #tEnd_date = #end_date
And here's the code from that branch of the SP:
create table #r (obj_id int, item_id int, l tinyint)
create clustered index #ri on #r (obj_id, item_id)
insert into #r
select object_id(obj_name), #iObj_id, 0
from dbo.tblSYSChangeHistory_Objects
where obj_type = 'U' and descr = cast(#cPrev_val AS varchar(150))
declare #i tinyint, #cnt int
set #i = 1
while #i <= 4
begin
insert into #r
select obj_id, item_id, #i
from dbo.vSYSChangeHistoryFK a with (nolock)
where exists (select null from #r where obj_id = a.rel_obj_id and item_id = a.rel_item_id and l = #i - 1)
and not exists (select null from #r where obj_id = a.obj_id and item_id = a.item_id)
set #cnt = ##rowcount
insert into #r
select rel_obj_id, rel_item_id, #i
from dbo.vSYSChangeHistoryFK a with (nolock)
where object_name(obj_id) not in (<this is a list of particular tables in the database>)
and exists (select null from #r where obj_id = a.obj_id and item_id = a.item_id and l between #i - 1 and #i)
and not exists (select null from #r where obj_id = a.rel_obj_id and item_id = a.rel_item_id)
set #i = case #cnt + ##rowcount when 0 then 100 else #i + 1 end
end
select date, obj_name, item, [uid], [action],
pkey, item_id, id, key_obj_id into #tCH_R
from dbo.vSYSChangeHistory a with (nolock)
where exists (select null from #r where obj_id = a.obj_id and item_id = a.item_id)
and (#cUID = '' or uid = #cUID)
and (#cNew_val = '' or [action] = #cNew_val)
declare ch_item_cursor cursor for
select distinct pkey, key_obj_id, item_id
from #tCH_R
where item = '' and pkey <> ''
open ch_item_cursor
fetch next from ch_item_cursor
into #cPrev_val, #iObj_id, #iCol_id
while ##fetch_status = 0
begin
set #SQLStr = 'select #val = ' + #cPrev_val +
' from ' + object_name(#iObj_id) + ' with (nolock)' +
' where id = #id'
exec sp_executesql #SQLStr,
N'#val varchar(max) output, #id int',
#cNew_val output, #iCol_id
update #tCH_R
set item = #cNew_val
where key_obj_id = #iObj_id
and item_id = #iCol_id
fetch next from ch_item_cursor
into #cPrev_val, #iObj_id, #iCol_id
end
close ch_item_cursor
deallocate ch_item_cursor
select date, obj_name,
cast(item AS varchar(254)) AS item,
uid, [action],
cast(id AS int) AS id
from #tCH_R
order by id
return
As you can see, the code uses a view. Here's that definition:
ALTER VIEW [dbo].[vSYSChangeHistoryFK]
AS
SELECT i.obj_id, i.item_id, c1.parent_object_id AS rel_obj_id, i2.item_id AS rel_item_id
FROM dbo.vSYSChangeHistoryItemsD AS i INNER JOIN
sys.foreign_key_columns AS c1 ON c1.referenced_object_id = i.obj_id AND c1.constraint_column_id = 1 INNER JOIN
dbo.vSYSChangeHistoryItemsD AS i2 ON c1.parent_object_id = i2.obj_id INNER JOIN
dbo.tblSYSChangeHistory_Details AS d1 ON d1.i_id = i.min_id AND d1.col_id = c1.referenced_column_id INNER JOIN
dbo.tblSYSChangeHistory_Details AS d1k ON d1k.i_id = i2.min_id AND d1k.col_id = c1.parent_column_id AND ISNULL(d1.new_val,
ISNULL(d1.prev_val, '')) = ISNULL(d1k.new_val, ISNULL(d1k.prev_val, '')) --LEFT OUTER JOIN
UNION ALL
SELECT i0.obj_id, i0.item_id, c01.parent_object_id AS rel_obj_id, i02.item_id AS rel_item_id
FROM dbo.vSYSChangeHistoryItemsD AS i0 INNER JOIN
sys.foreign_key_columns AS c01 ON c01.referenced_object_id = i0.obj_id AND c01.constraint_column_id = 1 AND col_name(c01.referenced_object_id,
c01.referenced_column_id) = 'ID' INNER JOIN
dbo.vSYSChangeHistoryItemsD AS i02 ON c01.parent_object_id = i02.obj_id INNER JOIN
dbo.tblSYSChangeHistory_Details AS d01k ON i02.min_id = d01k.i_id AND d01k.col_id = c01.parent_column_id AND ISNULL(d01k.new_val,
d01k.prev_val) = CAST(i0.item_id AS varchar(max))
And finally, that view uses one more view:
ALTER VIEW [dbo].[vSYSChangeHistoryItemsD]
AS
SELECT h.obj_id, m.item_id, MIN(m.id) AS min_id
FROM dbo.tblSYSChangeHistory AS h INNER JOIN
dbo.tblSYSChangeHistory_Items AS m ON h.id = m.h_id
GROUP BY h.obj_id, m.item_id
Working with the Profiler, it appears that view vSYSChangeHistoryFK is the big culprit, and my testing suggests that the particular problem is in the join between the two copies of vSYSChangeHistoryItemsD and the foreign_key_columns table.
I'm looking for any ideas on how to give acceptable performance here. The client reports sometimes waiting as much as 15 minutes without getting results. I've tested up to nearly 10 minutes with no result in at least one case.
If there were new language elements in 2008 or later that would solve this, I think the client would be willing to upgrade.
Thanks.
Wow that's a mess. Your big gain should be in removing the cursor. I see 'where exists' - that's nice and efficient b/c as soon as it finds one match it aborts. And I see 'where not exists' - by definition that has to scan everything. Is it finding the top 4? You can do better with using ROW_NUMBER() OVER (PARTITON BY [whatever makes it unique] ORDER BY [whatever your id is]. It's hard to tell. select object_id(obj_name), #iObj_id, 0 makes it seem like only the #i=1 loop actually does anything (?)
If that is what it's doing, you could write it as
SELECT * from
(
select ROW_NUMBER() OVER (PARTITION BY obj_id ORDER BY item_id desc) as Row,
obj_id, item_id
FROM bo.vSYSChangeHistoryFK a with (nolock)
where obj_type = 'U' and descr = cast(#cPrev_val AS varchar(150))
) paged
where Row between 1 and 4
ORDER BY Row
A DBA level change that could help would be to set up a partitioning scheme based on date. Roll over to a new partition every so often. Put the old partitions on different disks. Most queries may only need to hit the recent partition, which will be say 1/5th the size that it used to be, making it much faster without changing anything else.
Not a full answer, sorry. That mess would take hours to parse

Is there a way to loop through a table variable in TSQL without using a cursor?

Let's say I have the following simple table variable:
declare #databases table
(
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into #databases
Is declaring and using a cursor my only option if I wanted to iterate through the rows? Is there another way?
First of all you should be absolutely sure you need to iterate through each row — set based operations will perform faster in every case I can think of and will normally use simpler code.
Depending on your data it may be possible to loop using just SELECT statements as shown below:
Declare #Id int
While (Select Count(*) From ATable Where Processed = 0) > 0
Begin
Select Top 1 #Id = Id From ATable Where Processed = 0
--Do some processing here
Update ATable Set Processed = 1 Where Id = #Id
End
Another alternative is to use a temporary table:
Select *
Into #Temp
From ATable
Declare #Id int
While (Select Count(*) From #Temp) > 0
Begin
Select Top 1 #Id = Id From #Temp
--Do some processing here
Delete #Temp Where Id = #Id
End
The option you should choose really depends on the structure and volume of your data.
Note: If you are using SQL Server you would be better served using:
WHILE EXISTS(SELECT * FROM #Temp)
Using COUNT will have to touch every single row in the table, the EXISTS only needs to touch the first one (see Josef's answer below).
Just a quick note, if you are using SQL Server (2008 and above), the examples that have:
While (Select Count(*) From #Temp) > 0
Would be better served with
While EXISTS(SELECT * From #Temp)
The Count will have to touch every single row in the table, the EXISTS only needs to touch the first one.
This is how I do it:
declare #RowNum int, #CustId nchar(5), #Name1 nchar(25)
select #CustId=MAX(USERID) FROM UserIDs --start with the highest ID
Select #RowNum = Count(*) From UserIDs --get total number of records
WHILE #RowNum > 0 --loop until no more records
BEGIN
select #Name1 = username1 from UserIDs where USERID= #CustID --get other info from that row
print cast(#RowNum as char(12)) + ' ' + #CustId + ' ' + #Name1 --do whatever
select top 1 #CustId=USERID from UserIDs where USERID < #CustID order by USERID desc--get the next one
set #RowNum = #RowNum - 1 --decrease count
END
No Cursors, no temporary tables, no extra columns.
The USERID column must be a unique integer, as most Primary Keys are.
Define your temp table like this -
declare #databases table
(
RowID int not null identity(1,1) primary key,
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into #databases
Then do this -
declare #i int
select #i = min(RowID) from #databases
declare #max int
select #max = max(RowID) from #databases
while #i <= #max begin
select DatabaseID, Name, Server from #database where RowID = #i --do some stuff
set #i = #i + 1
end
Here is how I would do it:
Select Identity(int, 1,1) AS PK, DatabaseID
Into #T
From #databases
Declare #maxPK int;Select #maxPK = MAX(PK) From #T
Declare #pk int;Set #pk = 1
While #pk <= #maxPK
Begin
-- Get one record
Select DatabaseID, Name, Server
From #databases
Where DatabaseID = (Select DatabaseID From #T Where PK = #pk)
--Do some processing here
--
Select #pk = #pk + 1
End
[Edit] Because I probably skipped the word "variable" when I first time read the question, here is an updated response...
declare #databases table
(
PK int IDENTITY(1,1),
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into #databases
--/*
INSERT INTO #databases (DatabaseID, Name, Server) SELECT 1,'MainDB', 'MyServer'
INSERT INTO #databases (DatabaseID, Name, Server) SELECT 1,'MyDB', 'MyServer2'
--*/
Declare #maxPK int;Select #maxPK = MAX(PK) From #databases
Declare #pk int;Set #pk = 1
While #pk <= #maxPK
Begin
/* Get one record (you can read the values into some variables) */
Select DatabaseID, Name, Server
From #databases
Where PK = #pk
/* Do some processing here */
/* ... */
Select #pk = #pk + 1
End
If you have no choice than to go row by row creating a FAST_FORWARD cursor. It will be as fast as building up a while loop and much easier to maintain over the long haul.
FAST_FORWARD
Specifies a FORWARD_ONLY, READ_ONLY cursor with performance optimizations enabled. FAST_FORWARD cannot be specified if SCROLL or FOR_UPDATE is also specified.
This will work in SQL SERVER 2012 version.
declare #Rowcount int
select #Rowcount=count(*) from AddressTable;
while( #Rowcount>0)
begin
select #Rowcount=#Rowcount-1;
SELECT * FROM AddressTable order by AddressId desc OFFSET #Rowcount ROWS FETCH NEXT 1 ROWS ONLY;
end
Another approach without having to change your schema or using temp tables:
DECLARE #rowCount int = 0
,#currentRow int = 1
,#databaseID int
,#name varchar(15)
,#server varchar(15);
SELECT #rowCount = COUNT(*)
FROM #databases;
WHILE (#currentRow <= #rowCount)
BEGIN
SELECT TOP 1
#databaseID = rt.[DatabaseID]
,#name = rt.[Name]
,#server = rt.[Server]
FROM (
SELECT ROW_NUMBER() OVER (
ORDER BY t.[DatabaseID], t.[Name], t.[Server]
) AS [RowNumber]
,t.[DatabaseID]
,t.[Name]
,t.[Server]
FROM #databases t
) rt
WHERE rt.[RowNumber] = #currentRow;
EXEC [your_stored_procedure] #databaseID, #name, #server;
SET #currentRow = #currentRow + 1;
END
You can use a while loop:
While (Select Count(*) From #TempTable) > 0
Begin
Insert Into #Databases...
Delete From #TempTable Where x = x
End
Lightweight, without having to make extra tables, if you have an integer ID on the table
Declare #id int = 0, #anything nvarchar(max)
WHILE(1=1) BEGIN
Select Top 1 #anything=[Anything],#id=#id+1 FROM Table WHERE ID>#id
if(##ROWCOUNT=0) break;
--Process #anything
END
I really do not see the point why you would need to resort to using dreaded cursor.
But here is another option if you are using SQL Server version 2005/2008
Use Recursion
declare #databases table
(
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
--; Insert records into #databases...
--; Recurse through #databases
;with DBs as (
select * from #databases where DatabaseID = 1
union all
select A.* from #databases A
inner join DBs B on A.DatabaseID = B.DatabaseID + 1
)
select * from DBs
-- [PO_RollBackOnReject] 'FININV10532'
alter procedure PO_RollBackOnReject
#CaseID nvarchar(100)
AS
Begin
SELECT *
INTO #tmpTable
FROM PO_InvoiceItems where CaseID = #CaseID
Declare #Id int
Declare #PO_No int
Declare #Current_Balance Money
While (Select ROW_NUMBER() OVER(ORDER BY PO_LineNo DESC) From #tmpTable) > 0
Begin
Select Top 1 #Id = PO_LineNo, #Current_Balance = Current_Balance,
#PO_No = PO_No
From #Temp
update PO_Details
Set Current_Balance = Current_Balance + #Current_Balance,
Previous_App_Amount= Previous_App_Amount + #Current_Balance,
Is_Processed = 0
Where PO_LineNumber = #Id
AND PO_No = #PO_No
update PO_InvoiceItems
Set IsVisible = 0,
Is_Processed= 0
,Is_InProgress = 0 ,
Is_Active = 0
Where PO_LineNo = #Id
AND PO_No = #PO_No
End
End
It's possible to use a cursor to do this:
create function [dbo].f_teste_loop
returns #tabela table
(
cod int,
nome varchar(10)
)
as
begin
insert into #tabela values (1, 'verde');
insert into #tabela values (2, 'amarelo');
insert into #tabela values (3, 'azul');
insert into #tabela values (4, 'branco');
return;
end
create procedure [dbo].[sp_teste_loop]
as
begin
DECLARE #cod int, #nome varchar(10);
DECLARE curLoop CURSOR STATIC LOCAL
FOR
SELECT
cod
,nome
FROM
dbo.f_teste_loop();
OPEN curLoop;
FETCH NEXT FROM curLoop
INTO #cod, #nome;
WHILE (##FETCH_STATUS = 0)
BEGIN
PRINT #nome;
FETCH NEXT FROM curLoop
INTO #cod, #nome;
END
CLOSE curLoop;
DEALLOCATE curLoop;
end
I'm going to provide the set-based solution.
insert #databases (DatabaseID, Name, Server)
select DatabaseID, Name, Server
From ... (Use whatever query you would have used in the loop or cursor)
This is far faster than any looping techique and is easier to write and maintain.
I prefer using the Offset Fetch if you have a unique ID you can sort your table by:
DECLARE #TableVariable (ID int, Name varchar(50));
DECLARE #RecordCount int;
SELECT #RecordCount = COUNT(*) FROM #TableVariable;
WHILE #RecordCount > 0
BEGIN
SELECT ID, Name FROM #TableVariable ORDER BY ID OFFSET #RecordCount - 1 FETCH NEXT 1 ROW;
SET #RecordCount = #RecordCount - 1;
END
This way I don't need to add fields to the table or use a window function.
I agree with the previous post that set-based operations will typically perform better, but if you do need to iterate over the rows here's the approach I would take:
Add a new field to your table variable (Data Type Bit, default 0)
Insert your data
Select the Top 1 Row where fUsed = 0 (Note: fUsed is the name of the field in step 1)
Perform whatever processing you need to do
Update the record in your table variable by setting fUsed = 1 for the record
Select the next unused record from the table and repeat the process
DECLARE #databases TABLE
(
DatabaseID int,
Name varchar(15),
Server varchar(15),
fUsed BIT DEFAULT 0
)
-- insert a bunch rows into #databases
DECLARE #DBID INT
SELECT TOP 1 #DBID = DatabaseID from #databases where fUsed = 0
WHILE ##ROWCOUNT <> 0 and #DBID IS NOT NULL
BEGIN
-- Perform your processing here
--Update the record to "used"
UPDATE #databases SET fUsed = 1 WHERE DatabaseID = #DBID
--Get the next record
SELECT TOP 1 #DBID = DatabaseID from #databases where fUsed = 0
END
Step1: Below select statement creates a temp table with unique row number for each record.
select eno,ename,eaddress,mobno int,row_number() over(order by eno desc) as rno into #tmp_sri from emp
Step2:Declare required variables
DECLARE #ROWNUMBER INT
DECLARE #ename varchar(100)
Step3: Take total rows count from temp table
SELECT #ROWNUMBER = COUNT(*) FROM #tmp_sri
declare #rno int
Step4: Loop temp table based on unique row number create in temp
while #rownumber>0
begin
set #rno=#rownumber
select #ename=ename from #tmp_sri where rno=#rno **// You can take columns data from here as many as you want**
set #rownumber=#rownumber-1
print #ename **// instead of printing, you can write insert, update, delete statements**
end
This approach only requires one variable and does not delete any rows from #databases. I know there are a lot of answers here, but I don't see one that uses MIN to get your next ID like this.
DECLARE #databases TABLE
(
DatabaseID int,
Name varchar(15),
Server varchar(15)
)
-- insert a bunch rows into #databases
DECLARE #CurrID INT
SELECT #CurrID = MIN(DatabaseID)
FROM #databases
WHILE #CurrID IS NOT NULL
BEGIN
-- Do stuff for #CurrID
SELECT #CurrID = MIN(DatabaseID)
FROM #databases
WHERE DatabaseID > #CurrID
END
Here's my solution, which makes use of an infinite loop, the BREAK statement, and the ##ROWCOUNT function. No cursors or temporary table are necessary, and I only need to write one query to get the next row in the #databases table:
declare #databases table
(
DatabaseID int,
[Name] varchar(15),
[Server] varchar(15)
);
-- Populate the [#databases] table with test data.
insert into #databases (DatabaseID, [Name], [Server])
select X.DatabaseID, X.[Name], X.[Server]
from (values
(1, 'Roger', 'ServerA'),
(5, 'Suzy', 'ServerB'),
(8675309, 'Jenny', 'TommyTutone')
) X (DatabaseID, [Name], [Server])
-- Create an infinite loop & ensure that a break condition is reached in the loop code.
declare #databaseId int;
while (1=1)
begin
-- Get the next database ID.
select top(1) #databaseId = DatabaseId
from #databases
where DatabaseId > isnull(#databaseId, 0);
-- If no rows were found by the preceding SQL query, you're done; exit the WHILE loop.
if (##ROWCOUNT = 0) break;
-- Otherwise, do whatever you need to do with the current [#databases] table row here.
print 'Processing #databaseId #' + cast(#databaseId as varchar(50));
end
This is the code that I am using 2008 R2. This code that I am using is to build indexes on key fields (SSNO & EMPR_NO) n all tales
if object_ID('tempdb..#a')is not NULL drop table #a
select 'IF EXISTS (SELECT name FROM sysindexes WHERE name ='+CHAR(39)+''+'IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+char(39)+')'
+' begin DROP INDEX [IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+'] ON '+table_schema+'.'+table_name+' END Create index IDX_'+COLUMN_NAME+'_'+SUBSTRING(table_name,5,len(table_name)-3)+ ' on '+ table_schema+'.'+table_name+' ('+COLUMN_NAME+') ' 'Field'
,ROW_NUMBER() over (order by table_NAMe) as 'ROWNMBR'
into #a
from INFORMATION_SCHEMA.COLUMNS
where (COLUMN_NAME like '%_SSNO_%' or COLUMN_NAME like'%_EMPR_NO_')
and TABLE_SCHEMA='dbo'
declare #loopcntr int
declare #ROW int
declare #String nvarchar(1000)
set #loopcntr=(select count(*) from #a)
set #ROW=1
while (#ROW <= #loopcntr)
begin
select top 1 #String=a.Field
from #A a
where a.ROWNMBR = #ROW
execute sp_executesql #String
set #ROW = #ROW + 1
end
SELECT #pk = #pk + 1
would be better:
SET #pk += #pk
Avoid using SELECT if you are not referencing tables are are just assigning values.

Resources