Can I use a variable inside cursor declaration? - sql-server

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)
)

Related

(SOLVED) - First iteration of WHILE loop runs out of memory despite manual reconstruction of query succeeding

Environment: SQL Server 2019 (v15).
I have a large query that uses too much space when run as a single SELECT statement. When I try to run it, I get the following error:
Could not allocate a new page for database 'TEMPDB' because of insufficient disk space in filegroup 'DEFAULT'.
However, the problem breaks down naturally into a dozen or so pieces, so I wrote a WHILE loop to iterate through each piece and insert into a results table. Unfortunately, the first iteration of the WHILE loop also returns the same memory error. All the WHILE loop is doing is changing a few values in the WHERE clause.
The key thing confusing me here, is that when I manually run one iteration of the INSERT statement, absent all looping logic, it works perfectly.
Manually coding the first iteration to use the first institution_name just works, so I don't think the joins here are going wrong and causing the memory error.
WITH my_cte AS
(
SELECT [columns]
FROM mytable a
INNER JOIN bigtable b ON a.institution_name = b.institution_name
AND a.personID = b.personID
WHERE a.institution_name = 'ABC'
AND b.institution_name = 'ABC'
)
INSERT INTO results (personID, institution_name, ...)
SELECT personID, institution_name, [some aggregations]
FROM my_cte
GROUP BY personID, institution_name;
The version with the WHILE loop fails. I need to run the query with different values for institution_name.
Here I show three different values but even just the first iteration fails.
DECLARE #INSTITUTION varchar(10)
DECLARE #COUNTER int
SET #COUNTER = 0
DECLARE #LOOKUP table (temp_val varchar(10), temp_id int)
INSERT INTO #LOOKUP (temp_val, temp_id)
VALUES ('ABC', 1), ('DEF', 2), ('GHI', 3)
WHILE #COUNTER < 3
BEGIN
SET #COUNTER = #COUNTER + 1
SELECT #INSTITUTION = temp_val
FROM #LOOKUP
WHERE temp_id = #COUNTER;
WITH my_cte AS
(
SELECT [columns]
FROM mytable a
INNER JOIN bigtable b ON a.institution_name = b.institution_name
AND a.personID = b.personID
WHERE a.institution_name = #INSTITUTION
AND b.institution_name = #INSTITUTION
)
INSERT INTO results (personID, institution_name, ...)
SELECT personID, institution_name, [some aggregations]
FROM my_cte
GROUP BY personID, institution_name
END
As I write this question, I have quite literally just copy-pasted the insert statement a dozen times, changed the relevant WHERE clause, and run it without errors. Could it be some kind of datatype issue where the query can properly subset if a string literal is put in the WHERE column, but the lookup on my temporary table is failing due to the datatype? I notice that mytable.institution_name is varchar(10) while bigtable.institution_name is nvarchar(10). Setting the temp table to use nvarchar(10) didn't fix it either.

SQL using UPDLOCK in query to update top 1 record after filtering and ordering table

I have a stored procedure as follows:
CREATE PROCEDURE [dbo].[RV_SM_WORKITEM_CHECKWORKBYTYPE]
(
#v_ServiceName Nvarchar(20)
,#v_WorkType Nvarchar(20)
,#v_WorkItemThreadId nvarchar(50)
)
AS BEGIN
;WITH updateView AS
(
SELECT TOP 1 *
FROM rv_sm_workitem WITH (UPDLOCK)
WHERE stateofitem = 0
AND itemtype = #v_worktype
ORDER BY ITEMPRIORITY
)
UPDATE updateView
SET assignedto = #v_ServiceName,
stateofitem = 1,
dateassigned = getdate(),
itemthreadid = #v_WorkItemThreadId
OUTPUT INSERTED.*
END
It does the job I need it to do, namely, grab 1 record with a highest priority, change it's state from Available(0) to Not-Available(1), and return the record for work to be done with it. I should be able to have many threads (above 20) use this proc and have all 20 constantly running/grabbing a new workitem. However I am finding that beyond 2 threads, addition threads are waiting on locks; I'm guessing the UPDLOCK is causing this.
I have 2 questions, is there a better way to do this?
Can I do this without the UPDLOCK in the cte since the update statement by default uses UPDLOCK? Note, at any given time, there are over 400,000 records in this table.
I had to so something similar once and this is what I would suggest:
AS BEGIN
DECLARE #results table (id int, otherColumns varchar(50))
WHILE (EXISTS(SELECT TOP 1 * FROM #results))
BEGIN
;WITH updateView AS
(
SELECT TOP 1 *
FROM rv_sm_workitem
WHERE stateofitem = 0
AND itemtype = #v_worktype
ORDER BY ITEMPRIORITY
)
UPDATE updateView
SET assignedto = #v_ServiceName,
stateofitem = 1,
dateassigned = getdate(),
itemthreadid = #v_WorkItemThreadId
OUTPUT INSERTED.* into #results
where stateofitem = 0
END
END
This ensures that the call cannot not allow a item to be double processed. (because of the where clause on the update statement).
There are other variations of this idea, but this is an easy way to convey it. This is not production ready code though, as it will continually circle in the while loop until there is something to process. But I leave it to you to decide how to break out or not loop and return empty (and let the client side code deal with it.)
Here is the answer that helped me when I had this issue.

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.

tsql bulk update

MyTableA has several million records. On regular occasions every row in MyTableA needs to be updated with values from TheirTableA.
Unfortunately I have no control over TheirTableA and there is no field to indicate if anything in TheirTableA has changed so I either just update everything or I update based on comparing every field which could be different (not really feasible as this is a long and wide table).
Unfortunately the transaction log is ballooning doing a straight update so I wanted to chunk it by using UPDATE TOP, however, as I understand it I need some field to determine if the records in MyTableA have been updated yet or not otherwise I'll end up in an infinite loop:
declare #again as bit;
set #again = 1;
while #again = 1
begin
update top (10000) MyTableA
set my.A1 = their.A1, my.A2 = their.A2, my.A3 = their.A3
from MyTableA my
join TheirTableA their on my.Id = their.Id
if ##ROWCOUNT > 0
set #again = 1
else
set #again = 0
end
is the only way this will work if I add in a
where my.A1 <> their.A1 and my.A2 <> their.A2 and my.A3 <> their.A3
this seems like it will be horribly inefficient with many columns to compare
I'm sure I'm missing an obvious alternative?
Assuming both tables are the same structure, you can get a resultset of rows that are different using
SELECT * into #different_rows from MyTable EXCEPT select * from TheirTable and then update from that using whatever key fields are available.
Well, the first, and simplest solution, would obviously be if you could change the schema to include a timestamp for last update - and then only update the rows with a timestamp newer than your last change.
But if that is not possible, another way to go could be to use the HashBytes function, perhaps by concatenating the fields into an xml that you then compare. The caveat here is an 8kb limit (https://connect.microsoft.com/SQLServer/feedback/details/273429/hashbytes-function-should-support-large-data-types) EDIT: Once again, I have stolen code, this time from:
http://sqlblogcasts.com/blogs/tonyrogerson/archive/2009/10/21/detecting-changed-rows-in-a-trigger-using-hashbytes-and-without-eventdata-and-or-s.aspx
His example is:
select batch_id
from (
select distinct batch_id, hash_combined = hashbytes( 'sha1', combined )
from ( select batch_id,
combined =( select batch_id, batch_name, some_parm, some_parm2
from deleted c -- need old values
where c.batch_id = d.batch_id
for xml path( '' ) )
from deleted d
union all
select batch_id,
combined =( select batch_id, batch_name, some_parm, some_parm2
from some_base_table c -- need current values (could use inserted here)
where c.batch_id = d.batch_id
for xml path( '' ) )
from deleted d
) as r
) as c
group by batch_id
having count(*) > 1
A last resort (and my original suggestion) is to try Binary_Checksum? As noted in the comment, this does open the risk for a rather high collision rate.
http://msdn.microsoft.com/en-us/library/ms173784.aspx
I have stolen the following example from lessthandot.com - link to the full SQL (and other cool functions) is below.
--Data Mismatch
SELECT 'Data Mismatch', t1.au_id
FROM( SELECT BINARY_CHECKSUM(*) AS CheckSum1 ,au_id FROM pubs..authors) t1
JOIN(SELECT BINARY_CHECKSUM(*) AS CheckSum2,au_id FROM tempdb..authors2) t2 ON t1.au_id =t2.au_id
WHERE CheckSum1 <> CheckSum2
Example taken from http://wiki.lessthandot.com/index.php/Ten_SQL_Server_Functions_That_You_Have_Ignored_Until_Now
I don't know if this is better than adding where my.A1 <> their.A1 and my.A2 <> their.A2 and my.A3 <> their.A3, but I would definitely give it a try (assuming SQL Server 2005+):
declare #again as bit;
set #again = 1;
declare #idlist table (Id int);
while #again = 1
begin
update top (10000) MyTableA
set my.A1 = their.A1, my.A2 = their.A2, my.A3 = their.A3
output inserted.Id into #idlist (Id)
from MyTableA my
join TheirTableA their on my.Id = their.Id
left join #idlist i on my.Id = i.Id
where i.Id is null
/* alternatively (instead of left join + where):
where not exists (select * from #idlist where Id = my.Id) */
if ##ROWCOUNT > 0
set #again = 1
else
set #again = 0
end
That is, declare a table variable for collecting the IDs of the rows being updated and use that table for looking up (and omitting) IDs that have already been updated.
A slight variation on the method would be to use a local temporary table instead of a table variable. That way you would be able to create an index on the ID lookup table, which might result in better performance.
If schema change is not possible. How about using trigger to save off the Ids that have changed. And only import/export those rows.
Or use trigger to export it immediately.

Avoiding Cursor in SQL

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

Resources