SQL while loop with select query - sql

I'm trying to accomplish the following thing:
I receive a DocumentID. Find all the records in a table that match the specific DocumentID, for example I have 10 records matching and every record is with different DocumentAttachmentID.
I update all the records with the new data.
The problem comes that I need to insert some of the information from these ten records + other information received to a new table, which is History table, i.e. I need to insert ten new records there.
I've succeeded to this with Cursor, but it looks like that the Cursor isn't really good, because of the performance.
Is there a way to loop throught the 10 records that I selected from this table and for every record to take some information, add some additional info and then insert this in the other table ?
EDIT:
I tried to do this withoud looping(thanks you all for the answers)
I will try it tomorrow, do you think this is gonna work ?:
With the first Update, I update all documentAttachments,
The second block is INSERT TO, which should insert all document attachments in the other table with some extra columns.
UPDATE [sDocumentManagement].[tDocumentAttachments]
SET DeletedBy = #ChangedBy,
DeletedOn = #CurrentDateTime,
IsDeleted = 1,
WHERE DocumentID = #DocumentID;
INSERT INTO [sDocumentManagement].[tDocumentHistory] ( DocumentAttachmentID, DocumentID, ActivityCodeID, ChangedOn, ChangedBy, AdditionalInformation )
SELECT DocumentAttachmentID,
#DocumentID, [sCore].[GetActivityCodeIDByName] ( 'DeletedDocument' ),
#CurrentDateTime,
#ChangedBy,
#AdditionalInformation
FROM [sDocumentManagement].[tDocumentAttachments]
WHERE DocumentID = #DocumentID;

for looping without a cursor I quite often use the following technique:
DECLARE #items TABLE(id INT, val INT);
DECLARE #id INT;
DECLARE #val INT;
WHILE EXISTS(SELECT * FROM #items) BEGIN
SELECT TOP(1) #id = id, #val = val FROM #items;
DELETE FROM #items WHERE (id = #id);
--do what is needed with the values here.
SELECT #id, #val;
END
this treats the #items table as a queue pulling the rows off one at a time till it is empty.

Related

SQL Server 2008 Is there a more efficient way to do this update loop?

First posted question, I apologize in advance for any blunders.
The table contains records that are assigned to a team, the initial assignments are done with another process. Frequently, we have to reassign an agent's records and spread them out equally to the rest of the team. We have been doing this by hand, one by one, which was cumbersome. So I came up with this solution:
DECLARE #UpdtAgt TABLE (ID INT, Name varchar(25))
INSERT INTO #UpdtAgt
VALUES (1, 'Gandalf')
,(2,'Hank')
,(3,'Icarus')
CREATE TABLE #UpdtQry (TblID varchar(25))
INSERT INTO #UpdtQry
SELECT ShtID
FROM TestUpdate
DECLARE #RowID INT
DECLARE #AgtID INT
DECLARE #Agt varchar(25)
DECLARE #MaxID INT
SET #MaxID = (SELECT COUNT(*) FROM #UpdtAgt)
SET #AgtID = 1
--WHILE ((SELECT COUNT(*) FROM #UpdtQry) > 0)
WHILE EXISTS (SELECT TblID FROM #UpdtQry)
BEGIN
SET #RowID = (SELECT TOP 1 TblID FROM #UpdtQry)
SET #Agt = (SELECT Name FROM #UpdtAgt WHERE ID = #AgtID)
UPDATE TestUpdate
SET Assignment = #Agt
WHERE ShtID = #RowID
DELETE #UpdtQry WHERE TblID = #RowID
IF #AgtID < #MaxID
SET #AgtID = #AgtID + 1
ELSE
SET #AgtID = 1
END
DROP TABLE #UpdtQry
This is really my first attempt at doing something this in-depth. An update of 100 rows takes about 30 seconds to do. The UPDATE table, TestUpdate, has only the CLUSTERED index. How can I make this more efficient?
EDIT: I didn't define the #UpdtAgt and #UpdtQry tables very well in my explanation. #UpdtAgt will hold the agents that are being reassigned the records, and will likely change each time this is used. #UpdtQry will have a WHERE clause to define which agents records will be getting reassigned, again, this will change with each use. I hope that makes this a little more clear. Again, apologies for not getting it right the first time.
EDIT 2: I commented out the old WHILE clause and inserted the one that HABO suggested. Thank you again HABO.
I think this is what you're looking for:
DECLARE #UpdtAgt TABLE
(
ID INT,
Name VARCHAR(25)
)
INSERT #UpdtAgt
VALUES (1, 'Gandalf')
,(2, 'Hank')
,(3, 'Icarus')
UPDATE t
SET t.Assignment = a.Name
FROM TestUpdate AS t
INNER JOIN #UpdtAgt AS a
ON t.ShtID = a.ID
That should do all 4 rows at once.
P.S...
If you do create tables like in your original post in future, please try and keep the naming of your columns and variables consistent with their purpose!
In your example you used ID, AgtID, and ShtID and (most confusingly) TblID (and I think they're all the same thing? [please correct me if I'm wrong!]). If you called it AgtID everywhere (and #AgtID for the variable [There's no real need for #RowID]) then it would be much easier to see at a glance what'd going on! The same thing goes with Assignment and Name.
Because this is your first attempt at something like this, I want to congratulate you on something that works. While it is not ideal (and what is?) it meets the main goal: it works. There is a better way to do this using something known as a cursor. I remind myself of the proper syntax using the following page from Microsoft: Click here for full instruction on cursors
Having said that, the code at the end of this post shows my quick solution to your situation. Notice the following:
The #TestUpdate table is defined so that the query will run in MSSQL without using permanent tables.
Only the #UpdtAgt table needs to be setup as a temp table. However, if this is used regularly, it would be best to make it a permanent table.
The CLOSE and DEALLOCATE statements at the end are IMPORTANT - forgetting these will have rather unpleasant consequences.
DECLARE #TestUpdate TABLE (ShtID int, Assignment varchar(25))
INSERT INTO #TestUpdate
VALUES (1,'Fred')
,(2,'Barney')
,(3,'Fred')
,(4,'Wilma')
,(5,'Betty'),(6,'Leopold'),(7,'Frank'),(8,'Fred')
DECLARE #UpdtAgt TABLE (ID INT, Name varchar(25))
INSERT INTO #UpdtAgt
VALUES (1, 'Gandalf')
,(2,'Hank')
,(3,'Icarus')
DECLARE #recid int
DECLARE #AgtID int SET #AgtID=0
DECLARE #MaxID int SET #MaxID = (SELECT COUNT(*) FROM #UpdtAgt)
DECLARE assignment_cursor CURSOR
FOR SELECT ShtID FROM #TestUpdate
OPEN assignment_cursor
FETCH NEXT FROM assignment_cursor
INTO #recid
WHILE ##FETCH_STATUS = 0
BEGIN
SET #AgtID = #AgtID + 1
IF #AgtID > #MaxID SET #AgtID = 1
UPDATE #TestUpdate
SET Assignment = (SELECT TOP 1 Name FROM #UpdtAgt WHERE ID=#AgtID)
FROM #TestUpdate TU
WHERE ShtID=#recid
FETCH NEXT FROM assignment_cursor INTO #recid
END
CLOSE assignment_cursor
DEALLOCATE assignment_cursor
SELECT * FROM #TestUpdate

How to get a inserted id in other table from inside a trigger?

I have 3 tables tbl_Users, tbl_Protocol and tbl_ProtocolDetails and inside of my trigger on Users, I have to inserted into Protocol and then insert into ProtocolDetails, but I don't know how work the inserted scope.
Something like that:
CREATE TRIGGER tg_Users ON tbl_Users
AFTER INSERT, UPDATE AS
BEGIN
DECLARE #UserId = Int
DECLARE #ProtocolId = Int
DECLARE #UserDetail = NVARCHAR(255)
SELECT
#UserId = user_id,
#UserDetail = user_detail + '#' + user_explanation
FROM INSERTED
INSERT INTO tbl_Protocol (user_id, inserted_date)
VALUES (#UserId, GetDate())
-- Return Inserted Id from tbl_Protocol into #ProtocolDetail then
INSERT INTO tbl_ProtocolDetails (protocol_id, protocol_details)
VALUES (#ProtocolId, #UserDetail)
END
Your trigger has a MAJOR flaw in that you seems to expect to always have just a single row in the Inserted table - that is not the case, since the trigger will be called once per statement (not once for each row), so if you insert 20 rows at once, the trigger is called only once, and the Inserted pseudo table contains 20 rows.
Therefore, code like this:
Select #UserId = user_id,
#UserDetail = user_detail + '#' + user_explanation
From INSERTED;
will fail, since you'll retrieve only one (arbitrary) row from the Inserted table, and you'll ignore all other rows that might be in Inserted.
You need to take that into account when programming your trigger! You have to do this in a proper, set-based fashion - not row-by-agonizing-row stlye!
Try this code:
CREATE TRIGGER tg_Users ON tbl_Users
AFTER INSERT, UPDATE AS
BEGIN
-- declare an internal table variable to hold the inserted "ProtocolId" values
DECLARE #IdTable TABLE (UserId INT, ProtocolId INT);
-- insert into the "tbl_Protocol" table from the "Inserted" pseudo table
-- keep track of the inserted new ID values in the #IdTable
INSERT INTO tbl_Protocol (user_id, inserted_date)
OUTPUT Inserted.user_id, Inserted.ProtocolId INTO #IdTable(UserId, ProtocolId)
SELECT user_id, SYSDATETIME()
FROM Inserted;
-- insert into the "tbl_ProtocolDetails" table from both the #IdTable,
-- as well as the "Inserted" pseudo table, to get all the necessary values
INSERT INTO tbl_ProtocolDetails (protocol_id, protocol_details)
SELECT
t.ProtocolId,
i.user_detail + '#' + i.user_explanation
FROM
#IdTable t
INNER JOIN
Inserted i ON i.user_id = t.UserId
END
There is nothing in this trigger that would handle a multiple insert/update statement. You will need to either use one scenario that will handle multiple records or check how many records were effected with a IF ##ROWCOUNT = 1 else statement. In your example, I would just use something like
insert into tbl_Protocol(user_id, inserted_date)
select user_id, user_detail + '#' + user_explanation
From INSERTED;
As for your detail table, I see Marc corrected his answer to include the multiple lines and has a simple solution or you can create a second trigger on the tbl_Protocol. Another solution I have used in the past is a temp table for processing when I have very complicated triggers.

Keep the last 'x' results in a database

I would like to ask if there is a quick way to keep the last 'x' inserted rows in a database.
For example, i have an application through which users can search for items and I want to keep the last 10 searches that each user has made. I do not want to keep all his searches into the database as this will increase db_size. Is there a quick way of keeping only the latest 10 searches on my db or shall i check every time:
A) how many searches has been stored on database so far
B) if (searches = 10) delete last row
C) insert new row
I think that this way will have an impact on performance as it will need 3 different accesses on the database: check, delete and insert
I don't think an easy/quick way to do this. Based on your conditions i created the below stored procedure.
I considered SearchContent which is going to store the data.
CREATE TABLE SearchContent (
Id INT IDENTITY (1, 1),
UserId VARCHAR (8),
SearchedOn DATETIME,
Keyword VARCHAR (40)
)
In the stored procedure passing the UserId, Keyword and do the calculation. The procedure will be,
CREATE PROCEDURE [dbo].[pub_SaveSearchDetails]
(
#UserId VARCHAR (8),
#Keyword VARCHAR (40)
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Count AS INT = 0;
-- A) how many searches has been stored on database so far
SELECT #Count = COUNT(Keyword) FROM SearchContent WHERE UserId = #UserId
IF #Count >= 10
BEGIN
-- B) if (searches = 10) delete last row
DELETE FROM SearchContent WHERE Id IN ( SELECT TOP 1 Id FROM SearchContent WHERE UserId = #UserId ORDER BY SearchedOn ASC)
END
-- C) insert new row
INSERT INTO SearchContent (UserId, SearchedOn, Keyword)
VALUES (#UserId, GETDATE(), #Keyword)
END
Sample execution: EXEC pub_SaveSearchDetails 'A001', 'angularjs'

SQL stored procedure inserting duplicate OrderNumber

I searched the Internet for days, no effort, maybe I cant ask in a right way.
I have a sql table like this:
create table Items
(
Id int identity(1,1),
OrderNumber varchar(7),
ItemName varchar(255),
Count int
)
Then I have a stored procedure inserting items, on demand creating new OrderNumber:
create procedure spx_insertItems
#insertNewOrderNr bit,
#orderNumber varchar(7),
#itemName varchar(255),
#count int
as
begin
set nocount on;
if (#insertNewOrderNr = 1)
begin
declare #newNr = (select dbo.fun_getNewOrderNr())
INSERT INTO Items (OrderNumber, ItemName, Count) values (#newNr, #itemName, #count)
select #newNr
end
else
begin
INSERT INTO Items (OrderNumber, ItemName, Count) values (#orderNumber, #itemName, #count)
select scope_identity()
end
end
Finally there is a user defined function returning new OrderNumber:
create function dbo.fun_getNewOrderNr
()
return varchar(7)
as
begin
/* this func works well */
declare #output varchar(7)
declare #currentMaxNr varchar(7)
set #currentMaxNr = (isnull((select max(OrderNumber) from Items), 'some_default_value_here')
/* lets assume the #currentMaxNr is '01/2014', here comes logic that increments to #newValue='02/2014' and sets to #output, so: */
set #output = #newValue
return #output
end
Into Items can be inserted items that do as well that do not belong to any OrderNumber.
Whether an Item should become new OrderNumber, the procedure is called with #insertNewOrderNr=1, returns the new order number, that can be used to insert next items with that OrderNumber while #insertNewOrderNr=0.
Occasionally there happens that there come simultaneously 2 requests to #insertNewOrderNr and THERE IS THE PROBLEM - Items, that should correspond with different OrderNumbers get the same OrderNumber.
I tried to use transaction with no success.
The table structure cant be modified by me.
What would be the right way to ensure, that there won't be used the same newOrderNumber when simultaneous requests to the procedure come?
I am stuck here for a long time till now. Please, help.
You will have that problem as long as you use MAX(OrderNumber).
You might consider using sequences:
Create sequence
CREATE SEQUENCE dbo.OrderNumbers
AS int
START WITH 1
INCREMENT BY 1
NO CACHE;
GO
CREATE SEQUENCE dbo.OrderNumberYear
AS int
START WITH 2014
INCREMENT BY 1
NO CACHE;
SELECT NEXT VALUE FOR dbo.OrderNumberYear; --Important, run this ONE time after creation, this years value must be returned one initial time to work correctly.
Insert code
DECLARE #orderNumberYear INT = (SELECT CONVERT(INT, current_value) FROM sys.sequences WHERE name = 'OrderNumberYear');
IF(#orderNumberYear < YEAR(GETDATE()))
BEGIN
SELECT #orderNumberYear = NEXT VALUE FOR dbo.OrderNumberYear;
ALTER SEQUENCE dbo.OrderNumbers RESTART WITH 1 ;
END
IF(#orderNumberYear != YEAR(GETDATE()))
RAISERROR(N'Order year sequence is out of sync.', 16, 1);
DECLARE #newNr VARCHAR(15) = CONCAT(FORMAT(NEXT VALUE FOR dbo.OrderNumbers, '00/', 'en-US'), #orderNumberYear);
INSERT INTO Items (OrderNumber, ItemName, Count) values (#newNr, #itemName, #count)
SELECT #newNr
The duplicity still occured, not so often, but did.
The trick I finally used to get around didn't find itself inside SQL. Since the DB is always used by ONLY one web app that is used by several users, this is the solution:
in all (about 5) places in my VB.NET code I surrounded the myCommand.ExecuteScalar() with SyncLock (read more) statement that DID the trick :)

How do I group related data into a batch on an insert using Declare Cursor?

I have one table to select from, one table to update where I create a new transaction ID and another table to insert into using the transaction ID. I want to group all of my like transactions from the first table into one insert using the a transaction ID that is created on the update.
Here is the first tables data
This is the Query I am using now to select update and insert
DECLARE #TransactionNo int
Declare #Counter int
Declare #DateOut Date
Declare #Department_No nvarchar (100)
Declare #Job_Id nvarchar(50)
Declare Cur cursor for select Counter, DateOut, Job, Department from [dbo].[TimeCards_Inv]
open cur
fetch next from cur into #Counter, #DateOut, #Department_no,#Job_id
While ##Fetch_Status = 0
Begin
--creates a new transaction number
UPDATE cas_tekworks.dbo.next_number
SET next_trx = next_trx + 1
WHERE table_name_no='next_inventory_qty_jrnl'
Select #TransactionNo = [next_trx] From /cas_tekworks.dbo.next_number
WHERE table_name_no='next_inventory_qty_jrnl'
if #PunchType = 'O'
begin
-- Insert header record
INSERT INTO cas_tekworks.dbo.inventory_job_h
( transaction_no, dateout,job)
Select #TransactionNo, #DateOut, #Job_ID
end
fetch next from cur into #Counter, #DateOut, #Department_no,#Job_id
end
close cur
deallocate cur
Currently this code creates a new transaction ID for each Line in the database but I would like it to create just 1 transaction when I group by job and department. These are the current results. In my scenario. I would like it just to have 160 for job 10000 not two transaction ids.
How do you want your update?
I mean if you want to add the number of line, why making a loop adding one, instead of doing a count adding the result?
You have this possibility if you want to add the count :
UPDATE cas_tekworks.dbo.next_number
SET next_trx = next_trx + (select count(*) from [dbo].[TimeCards_Inv]) --it's possible you have to do a distinct or something else here
WHERE table_name_no='next_inventory_qty_jrnl'
If you can add multiple values, it's still possible to do it in one operation.
You can create a temp table with the appropriate values, and in your update you add an output clause saving all the information you need. In your case it would be job, date, id.
Then you can make an insert from the temp table.
I resolved this issue by creating the transaction number first and then declaring a sub cursor within the first. I also took out the counter ID field and just grouped by job and department. While selecting date out I used MAX(dateout)