Cursor poor performance issue - sql-server

I have a query which runs very slow. After some observations, I feel that it is because of using cursor in the query.
CREATE PROCEDURE [dbo].[spTest] (#INwww varchar(6)) AS
declare #curwwwbegdate datetime
declare #prevwwwbegdate datetime
declare #PrevwwwNum varchar(6)
set NOCOUNT ON
set #CurwwwBegDate = (select Begindate from wwwNUM where wwwnumber = #Inwww)
set #PrevwwwNum = (SELECT TOP 1 wwwNUMBER from wwwNUM WHERE BEGINDATE < #curwwwbegdate
order by begindate desc)
CREATE TABLE #ContRevExp (Inum int, lwww varchar(6),BookNum varchar(13), ldate datetime, lllLoc varchar(8), currentrev varchar(20), currentexp varchar(20), oldrev varchar(20), oldexp varchar(20), ContFileLoc varchar(100), bookloc varchar(3))
create table #wwwdates (wdLoc varchar(3),wdwww varchar(6), wdBegDate datetime, wdEndDate datetime)
create table #LocTable (loc varchar(3))
insert into #LocTable
SELECT LocCode from Location WHERE STATUS = 1 AND CCC = 1
DECLARE LocCursor INSENSITIVE CURSOR FOR
select * from #LocTable
OPEN LocCursor
declare #Loc varchar(3)
FETCH NEXT FROM LocCursor into #Loc
WHILE (##FETCH_STATUS = 0)
BEGIN
insert into #wwwdates (wdLoc, wdwww, wdBegDate, wdEndDate)
select #Loc,#Inwww,
(select top 1 wwwllldate from cccswwwllled where lllwww = #PrevwwwNum and branch = #Loc),
(select top 1 wwwllldate from cccswwwllled where lllwww = #Inwww and branch = #Loc)
FETCH NEXT FROM LocCursor into #Loc
END
deallocate LocCursor
Drop table #LocTable
DECLARE LocwwwCursor INSENSITIVE CURSOR FOR
select wdLoc, wdBegDate, wdEndDate from #wwwdates
declare #INFILELOC as varchar(3)
declare #INBEGDATE datetime
declare #INENDDATE datetime
OPEN LocwwwCursor
FETCH NEXT FROM LocwwwCursor into #INFILELOC, #INBEGDATE, #INENDDATE
WHILE (##FETCH_STATUS = 0)
BEGIN
declare #INVNUM int
declare #INDATE datetime
create table #cccs (cInvNum int)
insert into #cccs
Select distinct cccInv_Num from cccswwwllled
left join cccsexport on cccsExport.inv_num = cccinv_num
where cccswwwllled.Branch = #INFILELOC
and (cccswwwllled.impexp <> 1 or cccswwwllled.impexp is null)
and lllwww < #INwww and ContBookIndicator = 'C'
and cccsexport.lastupdate >= #INBEGDATE and cccsexport.lastupdate < #INENDDATE
DECLARE cccCursor INSENSITIVE CURSOR FOR
select * from #cccs
OPEN cccCursor
declare #cnum int
FETCH NEXT FROM cccCursor into #cnum
WHILE (##FETCH_STATUS = 0)
BEGIN
insert into #ContRevExp (Inum , lwww ,BookNum, ldate , lllloc , currentrev , currentexp, oldrev, oldexp, contfileloc, bookloc )
select top 1 cccInv_Num, lllwww,
(Select top 1 cccNumber from cccsExport where inv_num = #cnum and lastupdate <= #INENDDATE order by lastupdate desc ),
wwwlllDate,CE.FileNum,
0,
--case when CE.STATUS = 0 then '0' else convert(varchar(20),CE.TotalCost + CE.AllinRpoCost) end,
case when CE.STATUS = 0 then '0' else convert(varchar(20),CE.TotalExpense) end,
0,
--(Select top 1 convert(varchar(20),TotalCost + Allinrpocost) from cccsExport where inv_num = #cnum and lastupdate <= #INBEGDATE order by lastupdate desc ),
(Select top 1 convert(varchar(20),TotalExpense) from cccsExport where inv_num = #cnum and lastupdate <= #INBEGDATE order by lastupdate desc ),
'Cont/File/Loc',
BookLocation from cccswwwllled as CWL
left join cccsexport as CE on CE.inv_num = CWL.cccInv_Num
where CWL.cccInv_Num = #cnum
and CWL.lllwww < #INwww and CWL.ContBookIndicator = 'C'
and CE.lastupdate >= #INBEGDATE and CE.lastupdate <= #INENDDATE
order by CE.lastupdate desc
FETCH NEXT FROM cccCursor into #cnum
END
deallocate cccCursor
drop table #cccs
FETCH NEXT FROM LocwwwCursor into #INFILELOC, #INBEGDATE, #INENDDATE
END
deallocate LocwwwCursor
drop table #wwwdates
select bookloc,lwww,booknum,Inum,oldrev,oldexp,currentrev,currentexp,lllloc,
case when contfileloc is null then 'NOT' ELSE contfileloc end as 'Cont' from #ContRevExp
where (currentrev <> oldrev) or (currentexp <> oldexp)
order by bookloc, booknum
drop table #ContRevExp
I am not strong on cursor. My question is how to modify the code to improve the performance?
EDIT
The real problem is that I use ado.net to retrieve the data in a web application, the entire time to download data from server to the client is about 10 seconds, By using telerik justtrace tool, I found the stored procedure spent 99% time.
In the query, I found there is a nest cursors usage. cccCursor and LocwwwCursor. It means that there are two while loops, one is inside the other one. I guess that is the main reason, so I hope to replace the cursors with other skills.
SAMPLE DATA:
Because there are a lot of tables. I try to make the logic clear. We get the data from table Location:
The temp table #LocTable only has one column. The sample data is
AAA
BBB
CCC
DDD
EEE
FFF
GGG
I believe LocCursor is dealing each value of above data.
The input of the stored procedure is Inwww, let say it is 200218.
We have the query set #CurwwwBegDate = (select Begindate from wwwNUM where wwwnumber = #Inwww) will return a datetime 2002-04-28 00:00:00.000.
Another query set #PrevwwwNum = (SELECT TOP 1 wwwNUMBER from wwwNUM WHERE BEGINDATE < #curwwwbegdate
order by begindate desc) will return 200217. The temp table #wwwdates has the sample data as below.
wdLoc wdWeek wdBegDate wdEndDate
AAA 200218 NULL NULL
BBB 200218 NULL NULL
CCC 200218 NULL NULL
DDD 200218 2002-05-06 13:39:31.000 2002-05-07 16:52:44.000
EEE 200218 NULL NULL
FFF 200218 2002-05-06 13:39:40.000 2002-05-07 16:53:42.000
GGG 200218 NULL NULL
I think that the cursor LocwwwCursor takes the value from the temp table #wwwdatae then insert #cccs. I haven't got the sample data yet.
The final result likes(I just demonstrate it with one row)
ARL ARL 200510 IETARL0510087 150547816 $0.00 155.21 $0.00 155.00 ARL SOMETHING

There are unnecessary loops for aggerates in the SP above. Most of your logic could be condensed and made more performant with grouping and aggregations. For example, The whole block below could be drastically simplified.
REPLACE THIS ....
declare #prevwwwbegdate datetime
declare #PrevwwwNum varchar(6)
set NOCOUNT ON
set #CurwwwBegDate = (select Begindate from wwwNUM where wwwnumber = #Inwww)
set #PrevwwwNum = (SELECT TOP 1 wwwNUMBER from wwwNUM WHERE BEGINDATE < #curwwwbegdate
order by begindate desc)
CREATE TABLE #ContRevExp (Inum int, lwww varchar(6),BookNum varchar(13), ldate datetime, lllLoc varchar(8), currentrev varchar(20), currentexp varchar(20), oldrev varchar(20), oldexp varchar(20), ContFileLoc varchar(100), bookloc varchar(3))
create table #wwwdates (wdLoc varchar(3),wdwww varchar(6), wdBegDate datetime, wdEndDate datetime)
create table #LocTable (loc varchar(3))
insert into #LocTable
SELECT LocCode from Location WHERE STATUS = 1 AND CCC = 1
DECLARE LocCursor INSENSITIVE CURSOR FOR
select * from #LocTable
OPEN LocCursor
declare #Loc varchar(3)
FETCH NEXT FROM LocCursor into #Loc
WHILE (##FETCH_STATUS = 0)
BEGIN
insert into #wwwdates (wdLoc, wdwww, wdBegDate, wdEndDate)
select #Loc,#Inwww,
(select top 1 wwwllldate from cccswwwllled where lllwww = #PrevwwwNum and branch = #Loc),
(select top 1 wwwllldate from cccswwwllled where lllwww = #Inwww and branch = #Loc)
FETCH NEXT FROM LocCursor into #Loc
END
deallocate LocCursor
Drop table #LocTable
WITH THIS ....
create table #wwwdates (wdLoc varchar(3),wdwww varchar(6), wdBegDate datetime, wdEndDate datetime)
insert into #wwwdates
SELECT
LocCode,#Inwww,MAX(Prev_MaxDate),MAX(In_MaxDate)
FROM
(
SELECT LocCode,
Prev_MaxDate=CASE WHEN cccswwwllled.lllwww=#PrevwwwNum THEN wwwllldate ELSE NULL END,
In_MaxDate=CASE WHEN cccswwwllled.lllwww=#Inwww THEN wwwllldate ELSE NULL END
from
Location
INNER JOIN cccswwwllled ON cccswwwllled.lllwww IN(#PrevwwwNum,#Inwww) AND cccswwwllled.Branch=Location.LocCode
WHERE Location.STATUS = 1 AND Location.CCC = 1
)AS X
GROUP BY
LocCode

You should remove cursors and create set-based solution. You and mssql engine are not strong in cursor solutions :).
Rething what you would like to do. Join wwwNUM + lllwww + Location into view something like + check indexes on wwwnumber, branch, LocCode + STATUS + CCC (depends on data types)
create view MyViewWithNameWhichDescriptContain
as
select
l.LocCode,
n.wwwNUMBER Inwww,
n.Begindate InwwwDate,
c.wwwllldate Inwww
from Location l, wwwNUM n, cccswwwllled c
where l.[STATUS] = 1 AND l.CCC = 1
and l.LocCode = c.branch
and c.lllwww = n.wwwnumber
select from this view:
if object_id('tempdb..#wwwMyDatesPerLocCode') is not null drop table #wwwMyDatesPerLocCode
select *
into #wwwMyDatesPerLocCode
from (
select *, row_number() over(partition by Inwww order by InwwwDate desc OrbyCol
from MyViewWithNameWhichDescriptContain) t
where OrbyCol in (1,2)
create view cccswwwllled + cccsexport lets say llledExported
make selfjoin to access descendant (wwwMyDatesPerLocCode where OrbyCol = 1 + wwwMyDatesPerLocCode where OrbyCol = 2) and connect with view llledExported to get your info with your filters
check indexes on all tables again
Finnaly you don't need variables and cursors. Try think in joins. If you will add some somple data from all tables and add description of your indexes I can make it more detailed.

Related

Trigger did not run?

I have a trigger "after insert/update/delete/". It is supposed to count Balance on Account table based on transactions in Transaction table. It is on Transaction table. I am getting Balance discrepancies rarely, so have decided to add some logging into it. It dumps inserted+deleted tables (they are combined into a table var) and tsql statement which fired it. Judging from my log, it looks like the trigger did not fire for some inserts into Transaction table. Can this happen ? Are there any TSQL statement which change table data without firing trigger (except truncate table etc)?
Here is the trigger :
CREATE TRIGGER [dbo].[trg_AccountBalance]
ON [dbo].[tbl_GLTransaction]
AFTER INSERT, UPDATE, DELETE
AS
set nocount on
begin try
declare #OldOptions int = ##OPTIONS
set xact_abort off
declare #IsDebug bit = 1
declare #CurrentDateTime datetime = getutcdate()
declare #TriggerMessage varchar(max), #TriggerId int
if #IsDebug = 1
begin
select #TriggerId = isnull(max(TriggerId), 0) + 1
from uManageDBLogs.dbo.tbl_TriggerLog
declare #dbcc_INPUTBUFFER table(EventType nvarchar(30), Parameters Int, EventInfo nvarchar(4000) )
declare #my_spid varchar(20) = CAST(##SPID as varchar(20))
insert #dbcc_INPUTBUFFER
exec('DBCC INPUTBUFFER ('+#my_spid+')')
select #TriggerMessage = replace(EventInfo, '''', '''''') from #dbcc_INPUTBUFFER
insert into uManageDBLogs.dbo.tbl_TriggerLog (TriggerId, "Message", CreateDate)
values (#TriggerId, #TriggerMessage, #CurrentDateTime)
end
declare #Oper int
select #Oper = 0
-- determine type of sql statement
if exists (select * from inserted) select #Oper = #Oper + 1
if exists (select * from deleted) select #Oper = #Oper + 2
if #IsDebug = 1
begin
select #TriggerMessage = '#Oper = ' + convert(varchar, #Oper)
insert into uManageDBLogs.dbo.tbl_TriggerLog (TriggerId, "Message", CreateDate)
values (#TriggerId, #TriggerMessage, #CurrentDateTime)
end
if #Oper = 0 return -- No data changed
declare #TomorrowDate date = dateadd(day, 1, convert(date, getdate()))
declare #CurrentDate date = convert(date, getdate())
-- transactions from both inserted and deleted tables
declare #tbl_Trans table (FirmId int, GLAccountId int,
AmountDebit money, AmountCredit money, "Status" char(1), TableType char(1))
declare #tbl_AccountCounters table (FirmId int, GLAccountId int, Balance money)
declare #IsChange bit = null
insert into #tbl_Trans (FirmId, GLAccountId, AmountDebit, AmountCredit, "Status", TableType)
select FirmId, GLAccountId, AmountDebit, AmountCredit, "Status", 'I'
from inserted
union
select FirmId, GLAccountId, AmountDebit, AmountCredit, "Status", 'D'
from deleted
if #IsDebug = 1
begin
select #TriggerMessage = (select * from #tbl_Trans for xml path ('tbl_Trans'))
insert into uManageDBLogs.dbo.tbl_TriggerLog (TriggerId, "Message", CreateDate)
values (#TriggerId, #TriggerMessage, #CurrentDateTime)
end
insert into #tbl_AccountCounters (FirmId, GLAccountId, Balance)
select FirmId, GLAccountId, 0
from #tbl_Trans
group by FirmId, GLAccountId
if #Oper = 1 or #Oper = 2 -- insert/delete
begin
update #tbl_AccountCounters
set Balance = cnt.TransSum
from #tbl_AccountCounters as ac join
(
select trans.FirmId, trans.GLAccountId,
isnull(sum((trans.AmountDebit - trans.AmountCredit) * iif(trans.TableType = 'I', 1, -1)), 0) as TransSum
from #tbl_Trans as trans
where trans.Status = 'A'
group by trans.FirmId, trans.GLAccountId
) as cnt on ac.FirmId = cnt.FirmId and ac.GLAccountId = cnt.GLAccountId
select #IsChange = 1
end
else
begin
if update(AmountDebit) or update(AmountCredit) or update(Status) or update(GLAccountId)
begin
update #tbl_AccountCounters
set Balance = cnt.TransBalance
from #tbl_AccountCounters as ac join
(select trans.FirmId, trans.GLAccountId, isnull(sum(trans.AmountDebit - trans.AmountCredit), 0) as TransBalance
from dbo.tbl_GLTransaction as trans
where trans."Status" = 'A' and exists (select 1 from #tbl_AccountCounters as ac
where ac.GLAccountId = trans.GLAccountId and ac.FirmId = trans.FirmId)
group by trans.FirmId, trans.GLAccountId) as cnt on
ac.FirmId = cnt.FirmId and ac.GLAccountId = cnt.GLAccountId
select #IsChange = 0
end
end
if #IsDebug = 1
begin
select #TriggerMessage = '#IsChange = ' + isnull(convert(varchar, #IsChange), 'null')
insert into uManageDBLogs.dbo.tbl_TriggerLog (TriggerId, "Message", CreateDate)
values (#TriggerId, #TriggerMessage, #CurrentDateTime)
select #TriggerMessage = (select * from #tbl_AccountCounters for xml path ('tbl_AccountCounters'))
insert into uManageDBLogs.dbo.tbl_TriggerLog (TriggerId, "Message", CreateDate)
values (#TriggerId, #TriggerMessage, #CurrentDateTime)
end
if #IsChange is not null
begin
update tbl_GLAccount
set tbl_GLAccount.Balance = iif(#IsChange = 1, cnt.Balance + acc.Balance, cnt.Balance),
tbl_GLAccount.LastUpdate = getutcdate(),
tbl_GLAccount.LastUpdatedBy = 1
from #tbl_AccountCounters as cnt join dbo.tbl_GLAccount as acc on
cnt.FirmId = acc.FirmId and cnt.GLAccountId = acc.GLAccountId
end
if (16384 & #OldOptions) = 16384 set xact_abort on
end try
begin catch
declare #ErrorLine varchar(max)
select #ErrorLine = uManageDb.dbo.udf_GetErrorInfo()
insert into uManageDb.dbo.tbl_TriggerError ("Name", "Message", CreateDate)
values ('AccountingDB..trg_AccountBalance', #ErrorLine, GETUTCDATE())
end catch
I think I've found it. I have this line:
select .. from inserted
union
select .. from deleted
and they inserted 5 trans for $300 and 4 trans $100. I've got 2 records (300 and 100) in my #tbl_Trans (it was in the log). That's probably was the bug. So log hellps and trigger run as it had to.
I'll replace union with union all.

Updating Next_ID column

I have the following table:
VehicleID Reg_ID Next_RegID EntryDate
330034 9111 NULL 2010-12-06 00:00:00
330034 9113 NULL 2010-12-09 00:00:00
On the first row I need to update the Next_RegId column with the Reg_ID of the second row where VehicleId or (VIN/ChassisNumber) is the same. The Next_RegID column on the last entry should remain Null.
I've created a while loop procedure which works perfectly, but with millions of records in the table it takes ages to complete. Therefore, I was wondering if any of you dealt with this kind of a problem and have a solution for it.
Here's the procedure I wrote, and thanks in advance for all your help:
Declare #i as integer;
Declare #x as integer;
Declare #y as integer
Set #i= (Select Max(RID) from TempRegistration)
Set #x= 0
Set #y= 1
Declare #curChassis as nvarchar(100)
Declare #nextChassis as nvarchar(100)
While (#x <= #i)
Begin
set #curChassis = (Select ChassisNumber from TempRegistration where RID = #x)
set #nextChassis = (Select ChassisNumber from TempRegistration where RID = #y)
If (#curChassis = #nextChassis)
Begin
Update Registration set NextRegistrationId = (Select RegistrationId from TempRegistration where RID = #y)
Where RegistrationId = (Select RegistrationId from TempRegistration where RID = #x)
End
Set #x = #x + 1
Set #y = #y + 1
Print(#x)
End
TempRegistration is a temporary table I've created to assign a row_id which guides the while loop to assign the Reg_ID to the Next_RegId on the previous row.
This can be done with one UPDATE query. You haven't mentioned your RDBMS so...
For MSSQL:
Update Registration as t1
set NextRegistrationId = (Select TOP 1 RegistrationId
from Registration
where RID = t1.RID
and EntryDate>t1.EntryDate
order by EntryDate DESC)
For MySQL
Update Registration as t1
set NextRegistrationId = (Select RegistrationId
from Registration
where RID = t1.RID
and EntryDate>t1.EntryDate
order by EntryDate DESC
LIMIT 1)
If RID's are increasing with EntryDate then
Update Registration as t1
set NextRegistrationId = (Select MIN(RegistrationId)
from Registration
where RID = t1.RID
and EntryDate>t1.EntryDate
)
Tested and it seems to be working but this version uses a CTE (SQL Server)
with RegDetails as
(
select VehicleID, Reg_ID, ROW_NUMBER() OVER(PARTITION BY VehicleID ORDER BY EntryDate) AS ROWNUMBER
FROM dbo.Vehicle)
UPDATE a SET a.Next_RegID = b.Reg_ID
FROM RegDetails b
INNER JOIN dbo.Vehicle a ON (a.VehicleID = b.VehicleID)
WHERE b.ROWNUMBER = 2 and a.Next_RegID IS NULL and a.Reg_ID != b.Reg_ID

How to make this sql query

I have 2 SQL Server tables with the following structure
Turns-time
cod_turn (PrimaryKey)
time (datetime)
Taken turns
cod_taken_turn (Primary Key)
cod_turn
...
and several other fields which are irrelevant to the problem. I cant alter the table structures because the app was made by someone else.
given a numeric variable parameter, which we will assume to be "3" for this example, and a given time, I need to create a query which looking from that time on, it looks the first 3 consecutive records by time which are not marked as "taken". For example:
For example, for these turns, starting by the time of "8:00" chosen by the user
8:00 (not taken)
9:00 (not taken)
10:00 (taken)
11:00 (not taken)
12:00 (not taken)
13:00 (not taken)
14:00 (taken)
The query it would have to list
11:00
12:00
13:00
I cant figure out how to make the query in pure sql, if possible.
with a cursor
declare #GivenTime datetime,
#GivenSequence int;
select #GivenTime = cast('08:00' as datetime),
#GivenSequence = 3;
declare #sequence int,
#code_turn int,
#time datetime,
#taked int,
#firstTimeInSequence datetime;
set #sequence = 0;
declare turnCursor cursor FAST_FORWARD for
select turn.cod_turn, turn.[time], taken.cod_taken_turn
from [Turns-time] as turn
left join [Taken turns] as taken on turn.cod_turn = taken.cod_turn
where turn.[time] >= #GivenTime
order by turn.[time] asc;
open turnCursor;
fetch next from turnCursor into #code_turn, #time, #taked;
while ##fetch_status = 0 AND #sequence < #GivenSequence
begin
if #taked IS NULL
select #firstTimeInSequence = coalesce(#firstTimeInSequence, #time)
,#sequence = #sequence + 1;
else
select #sequence = 0,
#firstTimeInSequence = null;
fetch next from turnCursor into #code_turn, #time, #taked;
end
close turnCursor;
deallocate turnCursor;
if #sequence = #GivenSequence
select top (#GivenSequence) * from [Turns-time] where [time] >= #firstTimeInSequence
order by [time] asc
WITH Base AS (
SELECT *,
CASE WHEN EXISTS(
SELECT *
FROM Taken_turns taken
WHERE taken.cod_turn = turns.cod_turn) THEN 1 ELSE 0 END AS taken
FROM [Turns-time] turns)
, RecursiveCTE As (
SELECT TOP 1 cod_turn, [time], taken AS run, 0 AS grp
FROM Base
WHERE [time] >= #start_time
ORDER BY [time]
UNION ALL
SELECT R.cod_turn, R.[time], R.run, R.grp
FROM (
SELECT T.*,
CASE WHEN T.taken = 0 THEN 0 ELSE run+1 END AS run,
CASE WHEN T.taken = 0 THEN grp + 1 ELSE grp END AS grp,
rn = ROW_NUMBER() OVER (ORDER BY T.[time])
FROM Base T
JOIN RecursiveCTE R
ON R.[time] < T.[time]
) R
WHERE R.rn = 1 AND run < #run_length
), T AS(
SELECT *,
MAX(grp) OVER () AS FinalGroup,
COUNT(*) OVER (PARTITION BY grp) AS group_size
FROM RecursiveCTE
)
SELECT cod_turn,time
FROM T
WHERE grp=FinalGroup AND group_size=#run_length
I think there is not a simple way to achieve this.
But probably there are many complex ways :). This is an approach that should work in Transact-SQL:
CREATE TABLE #CONSECUTIVE_TURNS (id int identity, time datetime, consecutive int)
INSERT INTO #CONSECUTIVE_TURNS (time, consecutive, 0)
SELECT cod_turn
, time
, 0
FROM Turns-time
ORDER BY time
DECLARE #i int
#n int
SET #i = 0
SET #n = 3 -- Number of consecutive not taken records
while (#i < #n) begin
UPDATE #CONSECUTIVE_TURNS
SET consecutive = consecutive + 1
WHERE not exists (SELECT 1
FROM Taken-turns
WHERE id = cod_turn + #i
)
SET #i = #i + 1
end
DECLARE #firstElement int
SELECT #firstElement = min(id)
FROM #CONSECUTIVE_TURNS
WHERE consecutive >= #n
SELECT *
FROM #CONSECUTIVE_TURNS
WHERE id between #firstElement
and #firstElement + #n - 1
This is untested but I think it will work.
Pure SQL
SELECT TOP 3 time FROM [turns-time] WHERE time >= (
-- get first result of the 3 consecutive results
SELECT TOP 1 time AS first_result
FROM [turns-time] tt
-- start from given time, which is 8:00 in this case
WHERE time >= '08:00'
-- turn is not taken
AND cod_turn NOT IN (SELECT cod_turn FROM taken_turns)
-- 3 consecutive turns from current turn are not taken
AND (
SELECT COUNT(*) FROM
(
SELECT TOP 3 cod_turn AS selected_turn FROM [turns-time] tt2 WHERE tt2.time >= tt.time
GROUP BY cod_turn ORDER BY tt2.time
) AS temp
WHERE selected_turn NOT IN (SELECT cod_turn FROM taken_turns)) = 3
) ORDER BY time
Note: I tested it on Postgresql (with some code modification), but not MS SQL Server. I'm not sure about performance compared to T-SQL.
Another set-based solution (tested):
DECLARE #Results TABLE
(
cod_turn INT NOT NULL
,[status] TINYINT NOT NULL
,RowNumber INT PRIMARY KEY
);
INSERT #Results (cod_turn, [status], RowNumber)
SELECT a.cod_turn
,CASE WHEN b.cod_turn IS NULL THEN 1 ELSE 0 END [status] --1=(not taken), 0=(taken)
,ROW_NUMBER() OVER(ORDER BY a.[time]) AS RowNumber
FROM [Turns-time] a
LEFT JOIN [Taken_turns] b ON a.cod_turn = b.cod_turn
WHERE a.[time] >= #Start;
--SELECT * FROM #Results r ORDER BY r.RowNumber;
SELECT *
FROM
(
SELECT TOP(1) ca.LastRowNumber
FROM #Results a
CROSS APPLY
(
SELECT SUM(c.status) CountNotTaken, MAX(c.RowNumber) LastRowNumber
FROM
(
SELECT TOP(#Len)
b.RowNumber, b.[status]
FROM #Results b
WHERE b.RowNumber <= a.RowNumber
ORDER BY b.RowNumber DESC
) c
) ca
WHERE ca.CountNotTaken = #Len
ORDER BY a.RowNumber ASC
) x INNER JOIN #Results y ON x.LastRowNumber - #Len + 1 <= y.RowNumber AND y.RowNumber <= x.LastRowNumber;

SQL text wrapping

Hello and good afternoon. I am facing an issue with the system i support. I am able to build "Macros" which can pull sql views to a document. The issue is that the columns for these views do not wrap on the document. If a certain row contains too much text, it will push the other columns out of line. To resolve this i am trying to build a function to use with my view that will sort of brute force the wrap by looping through each row and creating an additional row to hold text for certain columns where the text limit was reached. I have something that works, but it's terribly slow at times. Does anyone have any ideas on how i can optimize this?
(
)
RETURNS #medlist2 TABLE (uniq_id UNIQUEIDENTIFIER, enterprise_id CHAR(5), practice_id CHAR (4), person_id UNIQUEIDENTIFIER,
enc_id UNIQUEIDENTIFIER, medication_name VARCHAR (70), sig_desc VARCHAR (512), start_date VARCHAR(10), row_num INT)
AS
BEGIN
DECLARE #medlist TABLE (uniq_id UNIQUEIDENTIFIER, enterprise_id CHAR (5), practice_id CHAR (4), person_id UNIQUEIDENTIFIER,
enc_id UNIQUEIDENTIFIER, medication_name VARCHAR (70), sig_desc VARCHAR (512), start_date DATETIME, processed INT)
DECLARE #medicationName VARCHAR (70)
DECLARE #sigDesc VARCHAR (512)
DECLARE #startDate VARCHAR (10)
DECLARE #uniqID UNIQUEIDENTIFIER
DECLARE #enterpriseID CHAR (5)
DECLARE #practiceID CHAR (4)
DECLARE #personID UNIQUEIDENTIFIER
DECLARE #encID UNIQUEIDENTIFIER
DECLARE #RowNum INT
DECLARE #RowCount INT
INSERT INTO #medlist (uniq_id, enterprise_id, practice_id, person_id,
enc_id, medication_name, sig_desc, start_date, processed)
SELECT uniq_id, enterprise_id, practice_id, person_id,
enc_id, medication_name, sig_desc, start_date, 0
FROM med_table
WHERE person_id IN (select distinct person_id from active_users where create_timestamp > GETDATE()-.2)
AND date_stopped = ''
ORDER BY medication_name
SET #RowCount = (SELECT COUNT(*) FROM #medlist WHERE processed = 0)
SET #RowNum = 0
WHILE #RowCount > 0
BEGIN
SET #RowNum = #RowNum + 1
SELECT TOP(1) #uniqid = uniq_id, #enterpriseID = enterprise_id, #practiceID = practice_id,
#personID = person_id, #encID = enc_id, #medicationName = '- ' +medication_name, #sigDesc = sig_desc,
#startDate = CONVERT(VARCHAR(10), start_date, 101)
FROM #medlist
WHERE processed = 0
INSERT INTO #medlist2(uniq_id, enterprise_id, practice_id, person_id,
enc_id, start_date, row_num, medication_name, sig_desc)
SELECT #uniqID, #enterpriseID, #practiceID, #personID, #encID, #startDate, #RowNum,
(CASE WHEN DATALENGTH(#medicationName) > 28 THEN LEFT(#medicationNAME, 28) + '-' ELSE #medicationName END),
(CASE WHEN DATALENGTH(#sigDesc) > 41 THEN LEFT(#sigDesc, 41) + '-' ELSE #sigDesc END)
WHILE DATALENGTH(#sigDesc) > 42 OR DATALENGTH(#medicationName) > 29
BEGIN
SET #medicationName = substring(#medicationName, 29,DATALENGTH(#medicationName))
SET #sigDesc = substring(#sigDesc, 42,DATALENGTH(#sigDesc))
SET #RowNum = #RowNum + 1
INSERT INTO #medlist2 (uniq_id, enterprise_id, practice_id, person_id,
enc_id, medication_name, sig_desc, row_num)
SELECT #uniqID, #enterpriseID, #practiceID, #personID, #encID, LEFT(#medicationNAME, 28), LEFT(#sigDesc, 41), #RowNum
IF DATALENGTH(#sigDesc) < 42 OR DATALENGTH(#medicationName) > 29
BREAK
ELSE
CONTINUE
END
UPDATE #medlist
SET processed = 1
WHERE uniq_id = #uniqID
SET #RowCount = (SELECT COUNT(*) FROM #medlist WHERE processed = 0)
IF #RowCount = 0
BREAK
ELSE
CONTINUE
END
RETURN
END
Don't do this in the database. Do it in your application layer!
Something as trivial as wrapping text is extremely expensive when SQL server is doing it on a row-by-row bases, but should be very quick to do in whatever application is displaying your results.

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