Avoiding Cursor in SQL - sql-server

What is the best alternative to using a cursor in SQL if I am suffering from performance issues ?
I got the following code wherein it uses Cursor to loop through and insert records.
DECLARE #AuditBatchID_logRow INT,
#AuditOperationID_logRow INT,
#RowIdentifier_logRow nvarchar(200),
#AuditDBTableID_logRow INT,
#AuditLogRowID INT,
#AuditDBColumnID INT,
#NewValue nvarchar(200),
#PreviousVaue nvarchar(200),
#NewDisplayValue nvarchar(200)
DECLARE Crsr_AUDITLOGROW CURSOR LOCAL FORWARD_ONLY STATIC
FOR
SELECT [t0].[AuditBatchID],
[t1].[AuditOperationID],
[t1].[RowIdentifier],
[t0].[AuditTableID],
[t1].[AuditLogRowID]
FROM [AuditBatchTable] AS [t0]
INNER JOIN [AuditLogRow] AS [t1]
ON [t0].[AuditBatchTableID] = [t1].[AuditBatchTableID]
Open Crsr_AUDITLOGROW
FETCH NEXT FROM Crsr_AUDITLOGROW
INTO #AuditBatchID_logRow,
#AuditOperationID_logRow,
#RowIdentifier_logRow,
#AuditDBTableID_logRow,
#AuditLogRowID
While(##FETCH_STATUS = 0)
BEGIN
INSERT INTO AuditLog(AuditLogRowID, AuditColumnID,
NewValue, OldDisplayValue, NewDisplayValue)
(SELECT #AuditLogRowID,
[ac].[AuditColumnID],
[t0].[UserEnteredValue],
[t0].[PreviousDisplayValue],
[t0].[DisplayValue]
FROM FMG_PROD.dbo.AuditLog AS [t0]
INNER JOIN FMG_PROD.dbo.AuditDBColumn AS [t1]
ON [t0].[AuditDBColumnID] = [t1].[AuditDBColumnID]
INNER JOIN FMG_PROD.dbo.AuditDBTable AS [t2]
ON [t1].[AuditDBTableID] = [t2].[AuditDBTableID]
INNER JOIN AuditTable AS [AT]
ON [t2].AuditDBTable = [AT].AuditTable
INNER JOIN AuditColumn AS [AC]
ON [AT].AuditTableID = [AC].AuditTableID
WHERE
([t0].[AuditBatchID] = #AuditBatchID_logRow)
AND ([t0].[AuditOperationID] = #AuditOperationID_logRow)
AND ([AT].[AuditTableID] = #AuditDBTableID_logRow)
AND [AC].AuditColumn = [t1].AuditDBColumn
AND (#RowIdentifier_logRow =
CASE ISNUMERIC(#RowIdentifier_logRow)
WHEN 1 then
CAST ([t0].[RowID] AS VARCHAR(200))
ELSE
CAST([t0].[RowGUID] AS VARCHAR(200))
END))
FETCH NEXT FROM Crsr_AUDITLOGROW
INTO #AuditBatchID_logRow,
#AuditOperationID_logRow,
#RowIdentifier_logRow,
#AuditDBTableID_logRow,
#AuditLogRowID
END
CLOSE Crsr_AUDITLOGROW
DEALLOCATE Crsr_AUDITLOGROW

Well, you're thinking and coding like a structured programmer - linearly, one by one, in tighest control of the program flow. That's how we (almost) all have been thought to program.
You need to think like a SQL guy - in SETS of data (not single rows, one at a time).
Avoid the need to tightly control each step of the algorithm - instead, just tell SQL Server WHAT you want - not HOW to do each step!
In the end, you're inserting a bunch of rows into the AuditLog table. Why do you need a cursor for that??
INSERT INTO AuditLog(...list of columns.....)
SELECT (....list of columns....)
FROM Table1
INNER JOIN ..........
INNER JOIN .........
WHERE ........
and you're done! Define what you want inserted into the table - DO NOT tell SQL Server in excrutiating detail how to do it - it'll know very well, thank you!
Marc

Related

Can I use a variable inside cursor declaration?

Is this code valid?
-- Zadavatel Login ID
DECLARE #ZadavatelLoginId nvarchar(max) =
(SELECT TOP 1 LoginId
FROM
(SELECT Z.LoginId, z.Prijmeni, k.spojeni
FROM TabCisZam Z
LEFT JOIN TabKontakty K ON Z.ID = K.IDCisZam
WHERE druh IN (6,10)) t1
LEFT JOIN
(SELECT ko.Prijmeni, k.spojeni, ko.Cislo
FROM TabCisKOs KO
LEFT JOIN TabKontakty K ON K.IDCisKOs = KO.id
WHERE druh IN (6, 10)) t2 ON t1.spojeni = t2.spojeni
AND t1.Prijmeni = t2.Prijmeni
WHERE
t2.Cislo = (SELECT CisloKontOsoba
FROM TabKontaktJednani
WHERE id = #IdKJ))
-- Pokud je řešitelský tým prázdný
IF NOT EXISTS (SELECT * FROM TabKJUcastZam WHERE IDKJ = #IdKJ)
BEGIN
DECLARE ac_loginy CURSOR FAST_FORWARD LOCAL FOR
-- Zadavatel
SELECT #ZadavatelLoginId
END
ELSE BEGIN
I am trying to pass the variable #ZadavatelLoginId into the cursor declaration and SSMS keeps telling me there is a problem with the code even though it is working.
Msg 116, Level 16, State 1, Procedure et_TabKontaktJednani_ANAFRA_Tis_Notifikace, Line 575 [Batch Start Line 7]
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS
Can anyone help?
I do not see anything in your posted query that could trigger the specific message that you listed. You might get an error if the subquery (SELECT CisloKontOsoba FROM TabKontaktJednani WHERE id = #IdKJ) returned more than one value, but that error would be a very specific "Subquery returned more than 1 value...".
However, as written, your cursor query is a single select of a scalar, which would never yield anything other than a single row.
If you need to iterate over multiple user IDs, but wish to separate your selection query from your cursor definition, what you likely need is a table variable than can hold multiple user IDs instead of a scalar variable.
Something like:
DECLARE #ZadavatelLoginIds TABLE (LoginId nvarchar(max))
INSERT #ZadavatelLoginIds
SELECT t1.LoginId
FROM ...
DECLARE ac_loginy CURSOR FAST_FORWARD LOCAL FOR
SELECT LoginId
FROM #ZadavatelLoginIds
OPEN ac_loginy
DECLARE #LoginId nvarchar(max)
FETCH NEXT FROM ac_loginy INTO #LoginId
WHILE ##FETCH_STATUS = 0
BEGIN
... Send email to #LoginId ...
FETCH NEXT FROM ac_loginy INTO #LoginId
END
CLOSE ac_loginy
DEALLOCATE ac_loginy
A #Temp table can also be used in place of the table variable with the same results, but the table variable is often more convenient to use.
As others have mentioned, I believe that your login selection query is overly complex. Although this was not the focus of your question, I still suggest that you attempt to simplify it.
An alternative might be something like:
SELECT Z.LoginId
FROM TabKontaktJednani KJ
JOIN TabCisKOs KO ON KO.Cislo = KJ.CisloKontOsoba
JOIN TabCisZam Z ON Z.Prijmeni = KO.Prijmeni
JOIN TabKontakty K ON K.IDCisZam = Z.ID
WHERE KJ.id = #IdKJ
AND K.druh IN (6,10)
The above is my attempt to rewrite your posted query after tracing the relationships. I did not see any LEFT JOINS that were not superseded by other conditions that forced them into effectively being inner joins, so the above uses inner joins for everything. I have assumed that the druh column is in the TabKontakty table. Otherwise I see no need for that table. I do not guarantee that my re-interpretation is correct though.
How about you create a #temp table for each sub query since the problem is coming up due to the sub queries?
CREATE TABLE #TEMP1
(
LoginID nvarchar(max)
)
CREATE TABLE #TEMP2
(
ko.Prijmeni nvarchar(max),
k.spojeni nvarchar(max),
ko.Cislo nvarchar(max)
)

Update row with values from select on condition, else insert new row

I'm need to run a calculation for month every day. If the month period, exists already, I need to update it, else I need to create a new row for the new month.
Currently, I've written
declare #period varchar(4) = '0218'
DECLARE #Timestamp date = GetDate()
IF EXISTS(select * from #output where period=#period)
/* UPDATE #output SET --- same calculation as below ---*/
ELSE
SELECT
#period AS period,
SUM(timecard.tworkdol) AS dol_local,
SUM(timecard.tworkdol/currates.cdrate) AS dol_USD,
SUM(timecard.tworkhrs) AS hrs,
#Timestamp AS timestamp
FROM dbo.timecard AS timecard
INNER JOIN dbo.timekeep ON timecard.ttk = timekeep.tkinit
INNER JOIN dbo.matter with (nolock) on timecard.tmatter = matter.mmatter
LEFT JOIN dbo.currates with (nolock) on matter.mcurrency = currates.curcode
AND currates.trtype = 'A'
AND timecard.tworkdt BETWEEN currates.cddate1
AND currates.cddate2
WHERE timekeep.tkloc IN('06','07') AND
timecard.twoper = #period
SELECT * FROM #output;
How can simply update my row with the new data from my select.
Not sure what RDBMS are you using, but in SQL Server something like this would update the #output table with the results of the SELECT that you placed in the ELSE part:
UPDATE o
SET o.dol_local = SUM(timecard.tworkdol),
SET o.dol_USD = SUM(timecard.tworkdol/currates.cdrate),
SET o.hrs = SUM(timecard.tworkhrs),
set o.timestamp = #Timestamp
FROM #output o
INNER JOIN dbo.timecard AS timecard ON o.period = timecard.twoper
INNER JOIN dbo.timekeep ON timecard.ttk = timekeep.tkinit
INNER JOIN dbo.matter with (nolock) on timecard.tmatter = matter.mmatter
LEFT JOIN dbo.currates with (nolock) on matter.mcurrency = currates.curcode
AND currates.trtype = 'A'
AND timecard.tworkdt BETWEEN currates.cddate1
AND currates.cddate2
WHERE timekeep.tkloc IN('06','07') AND
timecard.twoper = #period
Also, I think you want to do an INSERT in the ELSE part, but you are doing just a SELECT, so I guess you should fix that too
The answer to this will vary by SQL dialect, but the two main approaches are:
1. Upsert (if your DBMS supports it), for example using a MERGE statement in SQL Server.
2. Base your SQL on an IF:
IF NOT EXISTS (criteria for dupes)
INSERT INTO (logic to insert)
ELSE
UPDATE (logic to update)

Does a WHERE clause that matches a column value to itself get optimized out in SQL Server 2008+?

I'm trying to clean up some stored procedures, and was curious about the following. I did a search, but couldn't find anything that really talked about performance.
Explanation
Imagine a stored procedure that has the following parameters defined:
#EntryId uniqueidentifier,
#UserId int = NULL
I have the following table:
tbl_Entry
-------------------------------------------------------------------------------------
| EntryId PK, uniqueidentifier | Name nvarchar(140) | Created datetime | UserId int |
-------------------------------------------------------------------------------------
All columns are NOT NULL.
The idea behind this stored procedure is that you can get an Entry by its uniqueidentifier PK and, optionally, you can validate that it has the given UserId assigned by passing that as the second parameter. Imagine administrators who can view all entries versus a user who can only view their own entries.
Option 1 (current)
DECLARE #sql nvarchar(3000);
SET #sql = N'
SELECT
a.EntryId,
a.Name,
a.UserId,
b.UserName
FROM
tbl_Entry a,
tbl_User b
WHERE
a.EntryId = #EntryId
AND b.UserId = a.UserId';
IF #UserId IS NOT NULL
SET #sql = #sql + N' AND a.UserId = #UserId';
EXECUTE sp_executesql #sql;
Option 2 (what I thought would be better)
SELECT
a.EntryId,
a.Name,
a.UserId,
b.UserName
FROM
tbl_Entry a,
tbl_User b
WHERE
a.EntryId = #EntryId
AND a.UserId = COALESCE(#UserId, a.UserId)
AND b.UserId = a.UserId;
I realize this case is fairly, simple, and could likely be optimized by a single IF statement that separates two queries. I wrote a simple case to try and concisely explain the issue. The actual stored procedure has 6 nullable parameters. There are others that have even more nullable parameters. Using IF blocks would be very complicated.
Question
Will SQL Server still check a.UserId = a.UserId on every row even though that condition will always be true, or will that condition be optimized out when it sees that #UserId is NULL?
If it would check a.UserId = a.UserId on every row, would it be more efficient to build a string like in option 1, or would it still be faster to do the a.UserId = a.UserId condition? Is that something that would depend on how many rows are in the tables?
Is there another option here that I should be considering? I wouldn't call myself a database expert by any means.
You will get the best performance (and the lowest query cost) if you replace the COALESCE with a compound predicate as follows:
(#UserId IS NULL OR a.UserId = #UserId)
I would also suggest when writing T-SQL that you utilize the join syntax rather than the antiquated ANSI-89 coding style. The revised query will look something like this:
SELECT a.EntryId, a.Name, a.UserId, b.UserName
FROM tblEntry a
INNER JOIN tblUser b ON a.UserId = b.UserId
WHERE a.EntryId = #EntryId
AND (#UserId IS NULL OR a.UserId = #UserId);

better way to assign stored procedures output values from a table select

Due to programming language restraints, my ERP system does not allow me to make advanced select queries, that´s why I need to rely on making a stored procedure on SQL Server, calling it from the ERP system and getting the result through an array.
The code belows works ok, but I think it´s not the correct way to assign the values to the output variables... I wanted to assign the output variables directly from the select, without need to make a #temp table... is it possible? or did I make it right?
If the code can be enhanced, I would gracefully accept any suggestions. The objective of the code is call a stored procedure with a RFID tag (read by a RFID card reader) and then get some employee info from another database, from another ERP, on another server (linked through SQL "linked servers")
ALTER procedure [dbo].[KSBValTag]
(
#rfid varchar(20),
#OUT_NUMCAD varchar(10) OUTPUT,
#OUT_NOMFUN varchar(50) OUTPUT,
#OUT_SIT varchar(2) OUTPUT,
#OUT_CODCCU varchar(5) OUTPUT,
#OUT_NOMCCU varchar(30) OUTPUT
) as
Begin
set #rfid = SUBSTRING(#rfid, PATINDEX('%[^0]%', #rfid+'.'), LEN(#rfid))
select fun.numcad as Numcad,
fun.nomfun as Nomefun,
fun.sitafa as Situacao,
fun.codccu as CodCCU,
ccu.nomccu as NomeCCU
into #temp
from [vetorh].vetorh.r034fun as FUN
inner join
[vetorh].vetorh.r018ccu CCU
on fun.codccu = ccu.codccu
where numcad = (select num_cartao from [ksb-app01].topacesso.dbo.Cartoes where CodigoDeBarras = #rfid)
and tipcol = '1'
set #OUT_NUMCAD = (select Numcad from #temp)
set #OUT_NOMFUN = (select Nomefun from #temp)
set #OUT_SIT = (select Situacao from #temp)
set #OUT_CODCCU = (select CodCCU from #temp)
set #OUT_NOMCCU = (select NomeCCU from #temp)
End
select
#OUT_NUMCAD = fun.numcad,
#OUT_NOMFUN = fun.nomfun,
#OUT_SIT = fun.sitafa,
#OUT_CODCCU = fun.codccu,
#OUT_NOMCCU = ccu.nomccu
from [vetorh].vetorh.r034fun as FUN
inner join
[vetorh].vetorh.r018ccu CCU
on fun.codccu = ccu.codccu
where numcad = (select num_cartao from [ksb-app01].topacesso.dbo.Cartoes where CodigoDeBarras = #rfid)
and tipcol = '1'

How to insert value to table using cursor in stored procedure without getting duplicate records?

I am trying to insert records into existing but empty table based on from date and to date parameters using cursor in stored procedure. Please let me know what I am doing wrong in the below SQL?
When executing this procedure I am getting first row duplicated multiple times.
Error:
Maximum stored procedure, function, trigger or view nesting level exceeded (limit32)
Code:
ALTER proc [dbo].[spempmaster] (#date1 datetime,#date2 datetime)
as
Begin
Set nocount on
declare #doj datetime
declare #empname nchar(10)
declare #managername nchar(10)
declare #dept varchar(50)
declare emp_report15 cursor for
select convert(varchar(10),convert(smalldatetime,emp.doj,120),103) DOJ,
(emp.name + ' ' + emp.lastname) Name,
emp1.name Manager_Name, txtdepartment Department
from empmaster emp
left outer join tbljobtitles jt
on emp.fkjobtitleid = jt.pkjobtitleid,
tbldepartment td,
tblteam t,
empmaster emp1
where
jt.fkteamid = t.pkteamid
and td.pkdeptid= t.fkdeptid
and emp.reportingto = emp1.empno
and emp.doj between #date1 and #date2
order by doj
open emp_report15
fetch emp_report15 into #doj, #empname, #managername, #dept
while ##fetch_status = 0
begin
insert into tblreport (DOJ,emp_name,manager_name,department)
values(#doj,#empname,#managername,#dept)
end
fetch next from emp_report15 into #doj,#empname,#managername,#dept
close emp_report15
deallocate emp_report15
end
First of all - there's absolutely no need for a cursor in this situation. SQL Server is a set-based system - don't apply the procedural row-by-agonizing-row approach that works in procedural languages to this set-based system! Use a set-based approach instead!
Also: don't mix the proper ANSI join syntax with the old-style, deprecated comma-separated list of tables JOIN approach. That old style has been deprecated with the SQL-92 standard - more than 20 years ago! - about time to toss it out the window and use the proper ISO/ANSI standard JOIN syntax (INNER JOIN, LEFT OUTER JOIN) all the time.
So basically, in the end - your statement would be something like:
ALTER PROCEDURE [dbo].[spempmaster] (#date1 DATETIME, #date2 DATETIME)
AS
INSERT INTO dbo.tblreport(DOJ, emp_name, manager_name, department)
SELECT
CONVERT(VARCHAR(10), CONVERT(SMALLDATETIME, emp.doj, 120), 103),
(emp.name + ' ' + emp.lastname),
emp1.name Manager_Name,
txtDepartment
FROM
dbo.empmaster emp
INNER JOIN
dbo.empmaster emp1 ON emp.reportingto = emp1.empno
LEFT OUTER JOIN
dbo.tbljobtitles jt ON emp.fkjobtitleid = jt.pkjobtitleid
LEFT OUTER JOIN
dbo.tblteam t ON jt.fkteamid = t.pkteamid
LEFT OUTER JOIN
dbo.tbldepartment td ON td.pkdeptid = t.fkdeptid
WHERE
emp.doj BETWEEN #date1 AND #date2
As for avoiding duplicates: run your SELECT query separately, and see why you're getting duplicates. Just from this code alone, there's no way for outsiders to provide a meaningful answer here - it entirely depends on what data is stored in your tables.

Resources