Commit an temporary table while fetching rows T-SQL (SqlServer2005) - sql-server

I Update a temporary table during a "While.. fetch next"
The problem is that, when the cursor fetch the next row th e temporary table is not update with the fetch next before so at the end of the while the temporary table it's update with the data of last row instead of data of each row.
Ps : i don't speak very well english, i hope you can understand my request
DECLARE #IdType UNIQUEIDENTIFIER
DECLARE #SerialNumber NVARCHAR(64)
DECLARE DeviceCursor CURSOR LOCAL FOR
SELECT DISTINCT
--Acc.[AccountHierarchyId],
MC.SerialNumber AS SerialNumber,
MC.IdType AS IdType
FROM
#MachinesContrat MC
WHERE
MC.TypeAffaire = 'Essentiel'
OPEN DeviceCursor
FETCH NEXT FROM DeviceCursor INTO #SerialNumber, #IdType
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE
MI SET MI.SerialNumber = #SerialNumber
FROM
#MachinesInventory MI
WHERE
MI.fk_IdType = #IdType AND isnull(MI.SerialNumber,'Dummy') <> #SerialNumber
FETCH NEXT FROM DeviceCursor INTO #SerialNumber, #IdType
END
CLOSE DeviceCursor;
DEALLOCATE DeviceCursor;

You don't need a cursor to do the job. A simple join in your update statement can do it and will be more performant.

Related

SQL Server stored procedure , loop through table and get values?

In the code shown below, I loop through a table (in this case the disciplines table) and want to add records to another table, I'm rather new to this
How can I retrieve values from those tables for the insert command?
CREATE PROCEDURE cdisc
-- check disciplines
AS
BEGIN
DECLARE test CURSOR FAST_FORWARD FOR
SELECT *
FROM tbl_portfolio
WHERE show = 'Ja' ;
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE disc CURSOR FAST_FORWARD FOR
SELECT *
FROM disciplines;
WHILE ##fetch_status = 0
BEGIN
INSERT INTO ProjectDisciplines (Proj_id, discipline_ID)
VALUES (disc!ID, test!id)
END
CLOSE disc
DEALLOCATE disc
END
CLOSE test
DEALLOCATE test
END

SQL Server trigger Insert/Update specific column

Point is to make a trigger which will:
Check the configuration table which contains a column ConnectionField nvarchar(50)
It should return the string value (columnName) which will be used as a key
So on insert/update on table Workers, the code should set my Xfield value to the value from column ConnectionField, read from the Configuration table.
In short since this is all messy. I want to be able to let my end user to write down in configuration which column he will use as unique (Worker ID, SNSID, Name etc... ) based on his pick trigger need to put that field value to my Xfield
Don't ask why. It's really confusing.
I've written a trigger which will do that but it just is stuck somewhere in an infinite loop
CREATE TRIGGER [dbo].Tr_ConnectionField
ON [dbo].Workers
FOR INSERT, UPDATE
AS
SET NOCOUNT ON;
DECLARE #ID BIGINT
DECLARE #tmpUpit CURSOR;
DECLARE #ConFieldSETUP NVARCHAR(50)
-- Here I will read the field from configuration which will be used as key
SET #ConFieldSETUP = (SELECT TOP 1 ISNULL(ConnectionField, 'SNSID')
FROM ConfigurationTable)
BEGIN
SET #tmpUpit = CURSOR LOCAL SCROLL FOR
SELECT i.id FROM inserted i
OPEN #tmpUpit
END
FETCH NEXT FROM #tmpUpit INTO #ID
WHILE ##fetch_status = 0
BEGIN
-- Here I will use the configuration columns value to my Xfield
UPDATE Workers
SET Xfield = (SELECT #ConFieldSETUP
FROM Workers cld
WHERE cld.Id = #ID)
WHERE Id = #ID
END
FETCH NEXT FROM #tmpUpit INTO #ID
DEALLOCATE #tmpUpit
Try
CREATE TRIGGER [dbo].Tr_ConnectionField ON [dbo].Textt
FOR INSERT, UPDATE AS
SET NOCOUNT ON;
DECLARE #ConFieldSETUP nvarchar(50);
-- Stop recursion for the trigger
IF TRIGGER_NESTLEVEL(OBJECT_ID('dbo.Tr_ConnectionField')) > 1
RETURN;
-- Here i will read the field from configuration which will be used as key
SET #ConFieldSETUP = (SELECT TOP 1 ISNULL(ConnectionField, 'SNSID')
FROM ConfigurationTable
-- ORDER BY ...
);
-- Update Xfield depending on configuration
UPDATE w
SET Xfield = CASE #ConFieldSETUP
WHEN 'SNSID' THEN w.SNSID
WHEN 'Name' THEN w.Name
...
END
FROM Workers w
JOIN inserted i ON i.Id = w.Id;

How to optimize cursor in a stored procedure

I'm having problems with a stored procedure that iterates over a table, it works fine with a few hundred rows however when the table is over the thousands it saturates the memory and crashes.
The procedure should iterate row by row and fill a column with a value which is calculated from another column in the row. I suspect it is the cursor that crashes the procedure and in other questions I've read to use a while loop but I'm no expert in sql and the examples I tried from those answers didn't work.
CREATE PROCEDURE [dbo].[GenerateNewHashes]
AS
BEGIN
SET NOCOUNT ON;
DECLARE #module BIGINT = 382449983
IF EXISTS(SELECT 1 FROM dbo.telephoneSource WHERE Hash IS NULL)
BEGIN
DECLARE hash_cursor CURSOR FOR
SELECT a.telephone, a.Hash
FROM dbo.telephoneSource AS a
OPEN hash_cursor
FETCH FROM hash_cursor
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE dbo.telephoneSource
SET Hash = CAST(telephone AS BIGINT) % #module
WHERE CURRENT OF hash_cursor
FETCH NEXT FROM hash_cursor
END
CLOSE hash_cursor
DEALLOCATE hash_cursor
END
END
Basically the stored procedure is intended to fill a new column called Hash that was added to the existing table, when the script that updates the table ends the new column is filled with NULL values and then this stored procedure is supposed to fill each null value with the operation telephone number (which is a bigint) % module variable (big int as well).
Is there anything besides changing to a while loop that I can do to make it use less memory or just don't crash? Thanks in advance.
You could do the following:
WHILE 1=1
BEGIN
UPDATE TOP (10000) dbo.telephoneSource
SET Hash = CAST(telephone AS BIGINT)%#module
WHERE Hash IS NULL;
IF ##ROWCOUNT = 0
BEGIN
BREAK;
END;
END;
This will update Hash as long as there are NULL values and will exit once there have been no records updated.
Adding a filtered index could be useful as well:
CREATE NONCLUSTERED INDEX IX_telephoneSource_Hash_telephone
ON dbo.telephoneSource (Hash)
INCLUDE (telephone)
WHERE Hash IS NULL;
It will speed up lookups in order to update it. But this might be not needed.
Here is example of code to do it in loops from my comment above with out using a cursor, and if you add where your field you are updating IS NOT NULL into the inner loop it wont update ones that were already done (in case you need to restart the process or something.
I didnt include your specific tables in there but if you need me to I can add it in there.
DECLARE #PerBatchCount as int
DECLARE #MAXID as bigint
DECLARE #WorkingOnID as bigint
Set #PerBatchCount = 1000
--Find range of IDs to process using yoru tablename
SELECT #WorkingOnID = MIN(ID), #MAXID = MAX(ID)
FROM YouTableHere WITH (NOLOCK)
WHILE #WorkingOnID <= #MAXID
BEGIN
-- do an update on all the ones that exist in the offer table NOW
--DO YOUR UPDATE HERE
-- include this where clause where ID is your PK you are looping through
WHERE ID BETWEEN #WorkingOnID AND (#WorkingOnID + #PerBatchCount -1)
set #WorkingOnID = #WorkingOnID + #PerBatchCount
END
SET NOCOUNT OFF;
I would simply add computed column:
ALTER TABLE dbo.telephoneSource
ADD Hash AS (CAST(telephone AS BIGINT)%382449983) PERSISTED;

T-SQL: Set Operations Equivalent for cursor operation

I have a table like below where I iterate using a cursor to replace one value for another.
Is there a way to do this using set operations in SQL?
In this example, I would replace the column with value 2525 with the value 255 and iterate through using a cursor.
company_name_id replacement_company_name_id
2525 255
11000201010737 10000701010293
12000301010533 12000301010532
Here's the code I am running:
declare #company_name_id bigint, #replacement_company_name_id bigint
declare company_name_cursor cursor for
select
company_name_id
, replacement_company_name_id
from #replacements
open company_name_cursor
fetch next from company_name_cursor into #company_name_id, #replacement_company_name_id
while ##FETCH_STATUS <> -1
begin
update user_job_during_school_job
set company_name_id = #replacement_company_name_id
where company_name_id = #company_name_id
fetch next from company_name_cursor into #company_name_id, #replacement_company_name_id
end
close company_name_cursor
deallocate company_name_cursor
Try this one -
UPDATE uj
SET company_name_id = r.replacement_company_name_id
FROM dbo.user_job_during_school_job uj
JOIN #replacements r ON uj.company_name_id = r.company_name_id
I think you need to show your code for cursor so we have idea of what you are doing. If it's just to replace one column with another, an UPDATE statement could do that
UPDATE myTable SET
company_name_id = replacement_company_name_id
And if restricting the value 2525, then add a WHERE clause
UPDATE myTable SET
company_name_id = replacement_company_name_id
WHERE
company_name_id = 2525

Why does my cursor stop in the middle of a loop?

The code posted here is 'example' code, it's not production code. I've done this to make the problem I'm explaining readable / concise.
Using code similar to that below, we're coming across a strange bug. After every INSERT the WHILE loop is stopped.
table containst 100 rows, when the insert is done after 50 rows then the cursor stops, having only touched the first 50 rows. When the insert is done after 55 it stops after 55, and so on.
-- This code is an hypothetical example written to express
-- an problem seen in production
DECLARE #v1 int
DECLARE #v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
WHILE(##FETCH_STATUS=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
END
CLOSE MyCursor
DEALLOCATE MyCursor
There is an AFTER INSERT trigger on table2 which is used to log mutaties on table2 into an third table, aptly named mutations. This contains an cursor which inserts to handle the insert (mutations are logged per-column in an very specific manner, which requires the cursor).
A bit of background: this exists on an set of small support tables. It is an requirement for the project that every change made to the source data is logged, for auditing purposes. The tables with the logging contain things such as bank account numbers, into which vast sums of money will be deposited. There are maximum a few thousand records, and they should only be modified very rarely. The auditing functionality is there to discourage fraud: as we log 'what changed' with 'who did it'.
The obvious, fast and logical way to implement this would be to store the entire row each time an update is made. Then we wouldn't need the cursor, and it would perform an factor better. However the politics of the situation means my hands are tied.
Phew. Now back to the question.
Simplified version of the trigger (real version does an insert per column, and it also inserts the old value):
--This cursor is an hypothetical cursor written to express
--an problem seen in production.
--On UPDATE a new record must be added to table Mutaties for
--every row in every column in the database. This is required
--for auditing purposes.
--An set-based approach which stores the previous state of the row
--is expressly forbidden by the customer
DECLARE #col1 int
DECLARE #col2 int
DECLARE #col1_old int
DECLARE #col2_old int
--Loop through old values next to new values
DECLARE MyTriggerCursor CURSOR FAST_FORWARD FOR
SELECT i.col1, i.col2, d.col1 as col1_old, d.col2 as col2_old
FROM Inserted i
INNER JOIN Deleted d ON i.id=d.id
OPEN MyTriggerCursor
FETCH NEXT FROM MyTriggerCursor INTO #col1, #col2, #col1_old, #col2_old
--Loop through all rows which were updated
WHILE(##FETCH_STATUS=0)
BEGIN
--In production code a few more details are logged, such as userid, times etc etc
--First column
INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
VALUES ('table2', 'col1', #col1, #col1_old)
--Second column
INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
VALUES ('table2', 'col2', #col2, #col1_old)
FETCH NEXT FROM MyTriggerCursor INTO #col1, #col2, #col1_old, #col2_old
END
CLOSE MyTriggerCursor
DEALLOCATE MyTriggerCursor
Why is the code exiting in the middle of the loop?
Your problem is that you should NOT be using a cursor for this at all! This is the code for the example given above.
INSERT INTO table2(col1)
SELECT Col1 FROM table
where col1>10
You also should never ever use a cursor in a trigger, that will kill performance. If someone added 100,000 rows in an insert this could take minutes (or even hours) instead of millseconds or seconds. We replaced one here (that predated my coming to this job) and reduced an import to that table from 40 minites to 45 seconds.
Any production code that uses a cursor should be examined to replace it with correct set-based code. in my experience 90+% of all cursors can be reqwritten in a set-based fashion.
Ryan, your problem is that ##FETCH_STATUS is global to all cursors in an connection.
So the cursor within the trigger ends with an ##FETCH_STATUS of -1. When control returns to the code above, the last ##FETCH_STATUS was -1 so the cursor ends.
That's explained in the documentation, which can be found on MSDN here.
What you can do is use an local variable to store the ##FETCH_STATUS, and put that local variable in the loop. So you get something like this:
DECLARE #v1 int
DECLARE #v2 int
DECLARE #FetchStatus int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
SET #FetchStatus = ##FETCH_STATUS
WHILE(#FetchStatus=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
SET #FetchStatus = ##FETCH_STATUS
END
CLOSE MyCursor
DEALLOCATE MyCursor
It's worth noting that this behaviour does not apply to nested cursors. I've made an quick example, which on SqlServer 2008 returns the expected result (50).
USE AdventureWorks
GO
DECLARE #LocationId smallint
DECLARE #ProductId smallint
DECLARE #Counter int
SET #Counter=0
DECLARE MyFirstCursor CURSOR FOR
SELECT TOP 10 LocationId
FROM Production.Location
OPEN MyFirstCursor
FETCH NEXT FROM MyFirstCursor INTO #LocationId
WHILE (##FETCH_STATUS=0)
BEGIN
DECLARE MySecondCursor CURSOR FOR
SELECT TOP 5 ProductID
FROM Production.Product
OPEN MySecondCursor
FETCH NEXT FROM MySecondCursor INTO #ProductId
WHILE(##FETCH_STATUS=0)
BEGIN
SET #Counter=#Counter+1
FETCH NEXT FROM MySecondCursor INTO #ProductId
END
CLOSE MySecondCursor
DEALLOCATE MySecondCursor
FETCH NEXT FROM MyFirstCursor INTO #LocationId
END
CLOSE MyFirstCursor
DEALLOCATE MyFirstCursor
--
--Against the initial version of AdventureWorks, counter should be 50.
--
IF(#Counter=50)
PRINT 'All is good with the world'
ELSE
PRINT 'Something''s wrong with the world today'
this is a simple misunderstanding of triggers... you don't need a cursor at all for this
if UPDATE(Col1)
begin
insert into mutaties
(
tablename,
columnname,
newvalue
)
select
'table2',
coalesce(d.Col1,''),
coalesce(i.Col1,''),
getdate()
from inserted i
join deleted d on i.ID=d.ID
and coalesce(d.Col1,-666)<>coalesce(i.Col1,-666)
end
basically what this code does is it checks to see if that column's data was updated. if it was, it compares the new and old data, and if it's different it inserts into your log table.
you're first code example could easily be replaced with something like this
insert into table2 (col1)
select Col2
from table
where Col1>10
This code does not fetch any further values from the cursor, nor does it increment any values. As it is, there is no reason to implement a cursor here.
Your entire code could be rewritten as:
DECLARE #v1 int
DECLARE #v2 int
SELECT #v1 = Col1, #v2 = Col2
FROM table
IF(#v1>10)
INSERT INTO table2(col1) VALUES (#v2)
Edit: Post has been edited to fix the problem I was referring to.
You do not have to use a cursor to insert each column as a separate row.
Here is an example:
INSERT LOG.DataChanges
SELECT
SchemaName = 'Schemaname',
TableName = 'TableName',
ColumnName = CASE ColumnID WHEN 1 THEN 'Column1' WHEN 2 THEN 'Column2' WHEN 3 THEN 'Column3' WHEN 4 THEN 'Column4' END
ID = Key1,
ID2 = Key2,
ID3 = Key3,
DataBefore = CASE ColumnID WHEN 1 THEN I.Column1 WHEN 2 THEN I.Column2 WHEN 3 THEN I.Column3 WHEN 4 THEN I.Column4 END,
DataAfter = CASE ColumnID WHEN 1 THEN D.Column1 WHEN 2 THEN D.Column2 WHEN 3 THEN D.Column3 WHEN 4 THEN D.Column4 END,
DateChange = GETDATE(),
USER = WhateverFunctionYouAreUsingForThis
FROM
Inserted I
FULL JOIN Deleted D ON I.Key1 = D.Key1 AND I.Key2 = D.Key2
CROSS JOIN (
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
) X (ColumnID)
In the X table, you could code additional behavior with a second column that specially describes how to handle just that column (let's say you wanted some to post all the time, but others only when the value changes). What's important is that this is an example of the cross join technique of splitting rows into each column, but there is a lot more that can be done. Note that the full join allows this to work on inserts and deletes as well as updates.
I also fully agree that storing each row is FAR superior. See this forum for more about this.
As ck mentioned, you are not fetching any further values. The ##FETCH_STATUS thus get's its value from your cursor contained in your AFTER INSERT trigger.
You should change your code to
DECLARE #v1 int
DECLARE #v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
WHILE(##FETCH_STATUS=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
END

Resources