UPDATE from a table in SQL Native stored procedure (Hekaton) - sql-server

I'm migrating a queue in disk to in memory SQL Server 2016 to implement a queue.
This is my queue format:
CREATE TABLE dbo.SimpleQueue
(
MsgId BIGINT NOT NULL PRIMARY KEY NONCLUSTERED IDENTITY(1, 1),
Payload VARCHAR(7500) NOT NULL,
IsDeleted BIT NOT NULL
) WITH (MEMORY_OPTIMIZED=ON)
GO
This is my Enqueue native SQL Server stored procedure:
CREATE PROCEDURE dbo.Enqueue(#Payload VARCHAR(7500), #IsDeleted BIT)
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER
AS BEGIN ATOMIC WITH
(TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = 'english')
INSERT INTO dbo.SimpleQueue (Payload, IsDeleted) VALUES (#Payload, #IsDeleted);
END
GO
I'm trying to write down the Dequeue native SQL Server stored procedure, but I'm having some difficulties on how to implement an UPDATE using results of a SELECT or a variable table.
So far I tried:
CREATE PROCEDURE dbo.Dequeue(#BatchSize INT = 1)
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER
AS BEGIN ATOMIC WITH
( TRANSACTION ISOLATION LEVEL = SNAPSHOT,LANGUAGE = 'english' )
UPDATE dbo.SimpleQueue
SET IsDeleted=1
WHERE MsgId = (
SELECT TOP(#BatchSize) MsgId, Payload
FROM dbo.SimpleQueue
WHERE IsDeleted = 0)
END
GO
But I get this error:
Subqueries (queries nested inside another query) is only supported in SELECT statements with natively compiled modules.
So I tried a different approach by using a variable to store the result.
First I created a Table type:
CREATE TYPE dbo.SimpleDequeue
AS TABLE
(
MsgId BIGINT NOT NULL PRIMARY KEY NONCLUSTERED,
Payload INT NOT NULL
)
WITH (MEMORY_OPTIMIZED=ON)
GO
So far so good, then I tried to use it:
CREATE PROCEDURE dbo.Dequeue(#BatchSize INT = 1)
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER
AS BEGIN ATOMIC WITH
( TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = 'english')
DECLARE #result dbo.SimpleDequeue;
INSERT #result
SELECT TOP(#BatchSize) MsgId, Payload FROM dbo.SimpleQueue
WHERE IsDeleted = 0
UPDATE dbo.SimpleQueue
SET IsDeleted = 1
WHERE
#result.MsgId = dbo.SimpleQueue.MsgId
SELECT MsgId, Payload FROM #result
END
GO
I get this error:
Must declare the scalar variable "#result".
(only when is using #result on WHERE #result.MsgId = dbo.SimpleQueue.MsgId)
Here is the old dequeue process using in disk SQL Server tables:
CREATE PROCEDURE dbo.DequeueInDisk
#BatchSize INT = 1
AS
BEGIN
SET NOCOUNT ON;
WITH
cte AS (
SELECT TOP(#BatchSize) Payload
FROM dbo.SimpleQueue WITH (ROWLOCK, READPAST)
ORDER BY MsgId
)
DELETE FROM cte OUTPUT deleted.Payload;
END
How can I make that UPDATE and OUTPUT the updated values (with high performance, since this is critical)?

I think your approach makes perfect sense from SQL development point of view - you have to think in sets rather than in row-by-row approach. But it looks like Microsoft thinks that you require different approach for native compiled procedures, more imperative and really row-by-row (see Implementing UPDATE with FROM or Subqueries or Implementing MERGE Functionality in a Natively Compiled Stored Procedure.
So your procedure can look like this:
create or alter procedure [dbo].[Dequeue](#BatchSize int = 1)
with native_compilation, schemabinding, execute as owner
AS
BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = 'english')
declare
#result dbo.SimpleDequeue;
declare
#MsgId int,
#Payload varchar(7500),
#i int = 0;
while #i < #BatchSize
begin
select top (1)
#MsgId = s.MsgId,
#Payload = s.Payload
from dbo.SimpleQueue as s
where
s.IsDeleted = 0
order by
s.MsgId;
if ##rowcount = 0
begin
set #i = #BatchSize;
end
else
begin
update dbo.SimpleQueue set IsDeleted = 1 where MsgId = #MsgId;
insert into #result (MsgId, Payload)
select #MsgId, #Payload;
set #i += 1;
end;
end;
select MsgId, Payload from #result;
END
I've not tested how fast it will work, but I'll definitely will test it with some real numbers, cause we have a couple of these table-queues implemented and I wonder if we can get some performance boost with Hekaton.

In your old routine you use the TOP(#BatchSize) with an ORDER BY MsgId. The new approach seems not to have this ORDER BY... You'll get random result...
Your
WHERE MsgId = (
SELECT TOP(#BatchSize) MsgId, Payload
FROM dbo.SimpleQueue
WHERE IsDeleted = 0
/*added this!*/ ORDER BY MsgId )
will come back with two columns and - probably - several rows. You cannot compare this with an "=".
What you can try:
WHERE MsgId IN (
SELECT TOP(#BatchSize) MsgId
FROM dbo.SimpleQueue
WHERE IsDeleted = 0
ORDER BY MsgId)
Or you could try to use an INNER JOIN, something like this:
UPDATE dbo.SimpleQueue
SET IsDeleted=1
FROM dbo.SimpleQeueu
INNER JOIN dbo.SimpleQueue AS sq ON dbo.SimpleQeueu.MsgId=sq.MsgId
AND sq.IsDeleted=0
--this is missing the TOP-clause
What else: You could try an INNER JOIN (SELECT TOP ... ) AS InnerSimpleQueue ON .. or maybe a CROSS APPLY.
EDIT: One more approach with a CTE:
WITH myCTE AS
(
SELECT TOP(#BatchSize) MsgId
FROM dbo.SimpleQueue
WHERE IsDeleted = 0
ORDER BY MsgId
)
UPDATE dbo.SimpleQueue
SET IsDeleted=1
FROM dbo.SimpleQeueu
INNER JOIN myCTE ON myCTE.MsgId=dbo.SimpleQueue.MsgId

Related

How to get and use the value returned by a stored procedure to a INSERT INTO... SELECT... statement

I am just new in SQL language and still studying it. I'm having hard time looking for answer on how can I use the stored procedure and insert value into a table.
I have this stored procedure:
CREATE PROCEDURE TestID
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NewID VARCHAR(30),
#GenID INT,
#BrgyCode VARCHAR(5) = '23548'
SET #GenID = (SELECT TOP (1) NextID
FROM dbo.RandomIDs
WHERE IsUsed = 0
ORDER BY RowNumber)
SET #NewID = #BrgyCode + '-' + CAST(#GenID AS VARCHAR (30))
UPDATE dbo.RandomIDs
SET dbo.RandomIDs.IsUsed = 1
WHERE dbo.RandomIDs.NextID = #GenID
SELECT #NewID
END;
and what I'm trying to do is this:
INSERT INTO dbo.Residents([ResidentID], NewResidentID, [ResLogdate],
...
SELECT
[ResidentID],
EXEC TestID ,
[ResLogdate],
....
FROM
source.dbo.Resident;
There is a table dbo.RandomIDs containing random 6 digit non repeating numbers where I'm pulling out the value via the stored procedure and updating the IsUsed column of the table to 1. I'm transferring data from one database to another database and doing some processing on the data while transferring. Part of the processing is generating a new ID with the required format.
But I can't get it to work Sad I've been searching the net for hours now but I'm not getting the information that I need and that the reason for my writing. I hope someone could help me with this.
Thanks,
Darren
your question is little bit confusing, because you have not explained what you want to do. As i got your question, you want to fetch random id from randomids table and after performed some processing on nextid you want to insert it into resident table [newresidentid] and end of the procedure you fetch data from resident table. if i get anything wrong feel free to ask me.
your procedure solution is following.
CREATE PROCEDURE [TestId]
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NEWID NVARCHAR(30)
DECLARE #GENID BIGINT
DECLARE #BRGYCODE VARCHAR(5) = '23548'
DECLARE #COUNT INTEGER
DECLARE #ERR NVARCHAR(20) = 'NO IDS IN RANDOM ID'
SET #COUNT = (SELECT COUNT(NEXTID) FROM RandomIds WHERE [IsUsed] = 0)
SET #GENID = (SELECT TOP(1) [NEXTID] FROM RandomIds WHERE [IsUsed] = 0 ORDER BY [ID] ASC)
--SELECT #GENID AS ID
IF #COUNT = 0
BEGIN
SELECT #ERR AS ERROR
END
ELSE
BEGIN
SET #NEWID = #BRGYCODE + '-' + CAST(#GENID AS varchar(30))
UPDATE RandomIds SET [IsUsed] = 1 WHERE [NextId] = #GENID
INSERT INTO Residents ([NewResidentId] , [ResLogDate] ) VALUES (#NEWID , GETDATE())
SELECT * FROM Residents
END
END
this procedure will fetch data from your randomids table and perform some processing on nextid than after it directs insert it into resident table and if you want to insert some data through user you can use parameter after declaring procedure name
E.G
CREATE PROCEDURE [TESTID]
#PARAM1 DATATYPE,
#PARAM2 DATATYPE
AS
BEGIN
END
I'm not convinced that your requirement is a good one but here is a way to do it.
Bear in mind that concurrent sessions will not be able to read your update until it is committed so you have to kind of "lock" the update so you will get a block until you're going to commit or rollback. This is rubbish for concurrency, but that's a side effect of this requirement.
declare #cap table ( capturedValue int);
declare #GENID int;
update top (1) RandomIds set IsUsed=1
output inserted.NextID into #cap
where IsUsed=0;
set #GENID =(select max( capturedValue) from #cap )
A better way would be to use an IDENTITY or SEQUENCE to solve your problem. This would leave gaps but help concurrency.

SQL Trigger Inconsistently firing

I have a SQL Trigger on a table that works... most of the time. And I cannot figure out why sometimes the fields are NULL
The trigger works by Updateing the LastUpdateTime whenever something is modified in the field, and the InsertDatetime when first Created.
For some reason this only seems to work some times.
ALTER TRIGGER [dbo].[DateTriggerTheatreListHeaders]
ON [dbo].[TheatreListHeaders]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS(SELECT * FROM DELETED)
BEGIN
UPDATE ES
SET InsertDatetime = Getdate()
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
END
IF UPDATE(LastUpdateDateTime) OR UPDATE(InsertDatetime)
RETURN;
IF EXISTS (
SELECT
*
FROM
INSERTED I
JOIN
DELETED D
-- make sure to compare inserted with (same) deleted person
ON D.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
)
BEGIN
UPDATE ES
SET InsertDatetime = ISNULL(es.Insertdatetime,Getdate())
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER
END
END
A much simpler and efficient approach to do what you are trying to do, would be something like...
ALTER TRIGGER [dbo].[DateTriggerTheatreListHeaders]
ON [dbo].[TheatreListHeaders]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
--Determine if this is an INSERT OR UPDATE Action .
DECLARE #Action as char(1);
SET #Action = (CASE WHEN EXISTS(SELECT * FROM INSERTED)
AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set Action to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set Action to Insert.
END);
UPDATE ES
SET InsertDatetime = CASE WHEN #Action = 'U'
THEN ISNULL(es.Insertdatetime,Getdate())
ELSE Getdate()
END
,LastUpdateDateTime = Getdate()
FROM TheatreListHeaders es
JOIN Inserted I ON es.UNIQUETHEATRELISTNUMBER = I.UNIQUETHEATRELISTNUMBER;
END
"If update()" is poorly defined/implemented in sql server IMO. It does not do what is implied. The function only determines if the column was set by a value in the triggering statement. For an insert, every column is implicitly (if not explicitly) assigned a value. Therefore it is not useful in an insert trigger and difficult to use in a single trigger that supports both inserts and updates. Sometimes it is better to write separate triggers.
Are you aware of recursive triggers? An insert statement will execute your trigger which updates the same table. This causes the trigger to execute again, etc. Is the (database) recursive trigger option off (which is typical) or adjust your logic to support that?
What are your expectations for the insert/update/merge statements against this table? This goes back to your requirements. Is the trigger to ignore any attempt to set the datetime columns directly and set them within the trigger always?
And lastly, what exactly does "works sometimes" actually mean? Do you have a test case that reproduces your issue. If you don't, then you can't really "fix" the logic without a specific failure case. But the above comments should give you sufficient clues. To be honest, your logic seems to be overly complicated. I'll add that it also is logically flawed in the way that it set insertdatetime to getdate if the existing value is null during an update. IMO, it should reject any update that attempts to set the value to null because that is overwriting a fact that should never change. M.Ali has provided an example that is usable but includes the created timestamp problem. Below is an example that demonstrates a different path (assuming the recursive trigger option is off). It does not include the rejection logic - which you should consider. Notice the output of the merge execution carefully.
use tempdb;
set nocount on;
go
create table zork (id integer identity(1, 1) not null primary key,
descr varchar(20) not null default('zippy'),
created datetime null, modified datetime null);
go
create trigger zorktgr on zork for insert, update as
begin
declare #rc int = ##rowcount;
if #rc = 0 return;
set nocount on;
if update(created)
select 'created column updated', #rc as rc;
else
select 'created column NOT updated', #rc as rc;
if exists (select * from deleted) -- update :: do not rely on ##rowcount
update zork set modified = getdate()
where exists (select * from inserted as ins where ins.id = zork.id);
else
update zork set created = getdate(), modified = getdate()
where exists (select * from inserted as ins where ins.id = zork.id);
end;
go
insert zork default values;
select * from zork;
insert zork (descr) values ('bonk');
select * from zork;
update zork set created = null, descr = 'upd #1' where id = 1;
select * from zork;
update zork set descr = 'upd #2' where id = 1;
select * from zork;
waitfor delay '00:00:02';
merge zork as tgt
using (select 1 as id, 'zippity' as descr union all select 5, 'who me?') as src
on tgt.id = src.id
when matched then update set descr = src.descr
when not matched then insert (descr) values (src.descr)
;
select * from zork;
go
drop table zork;

SQL - UPDATE within a SWITCH CASE

I'm trying to run an update based on the value of a flag sent into a procedure but it does not like the syntax of the UPDATE statement here. What's wrong with it?
CREATE PROC [dbo].[TestProc]
#ID int,
#GET_COUNT bit
AS
BEGIN
SELECT
CASE
WHEN #GET_COUNT = 1
THEN (SELECT COUNT(*) FROM [ORDERS]EMPLOYEE_ID = #ID)
WHEN #GET_COUNT = 0
THEN UPDATE ORDERS SET EMPLOYEE_ID = null WHERE EMPLOYEE_ID = #ID
END
GO
You are confusing a SELECT . . . CASE with IF. Your code looks like T-SQL, so this is probably what you intend:
IF #GET_COUNT = 1
BEGIN
SELECT COUNT(*) FROM [ORDERS] EMPLOYEE_ID = #ID;
END;
ELSE IF #GET_COUNT = 0
BEGIN
UPDATE ORDERS SET EMPLOYEE_ID = null WHERE EMPLOYEE_ID = #ID
END;
Some SQL scripting languages do use CASE for control-flow as well as expression evaluations. However, I think that IF is clearer in this context.

how to check data if exists "type of table" in stored procedure

I have created a type in sql server 2008 for to pass datatable to stored procedure.
My SP works ok but how can I check data if exists in the table?
(for example: check detailid or id
if exists: update them
if not exists: insert new )
here is my SP:
ALTER PROCEDURE [dbo].[Insert_Data]
(
#empinfo myType READONLY
)
AS
BEGIN
SET NOCOUNT ON;
Insert into TableEmp(ID, DetailID, Text)
select id, detailid, text from #empinfo
END
Thnx all
ALTER PROCEDURE [dbo].[Insert_Data]
(
#empinfo myType READONLY
)
AS
BEGIN
SET NOCOUNT ON;
MERGE TableEmp AS t
USING (select id, detailid, [text] from #empinfo) AS s
ON s.ID = t.ID
WHEN MATCHED THEN
UPDATE SET t.detailid = s.detailid,
t.[text] = s.[text]
WHEN NOT MATCHED THEN
INSERT(id, detailid, [text])
VALUES(s.id, s.detailid, s.[text]);
END
You can declare a variable and count the records in the table.
DECLARE #count INT
SET #count = (SELECT COUNT(ID)
FROM TableEmp)
-- Do Something with the results
So then using conditional logic, you can do something with the count to accomplish different results.
IF #count = 0
BEGIN
-- Do Something cool like insert data
END
ELSE
BEGIN
-- Do Something else like update data
END
This is a simple example where you can search for a specific record and update it. If you need to update many records, then you can use cursors and iterate through the required records and update what you need.
More information about cursors can be found here:
http://technet.microsoft.com/en-us/library/ms180169.aspx

Select / Insert version of an Upsert: is there a design pattern for high concurrency?

I want to do the SELECT / INSERT version of an UPSERT. Below is a template of the existing code:
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))
IF NOT EXISTS (SELECT * FROM Table WHERE RowValue = #VALUE)
BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPEIDENTITY()
END
ELSE
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE)
The query will be called from many concurrent sessions. My performance tests show that it will consistently throw primary key violations under a specific load.
Is there a high-concurrency method for this query that will allow it to maintain performance while still avoiding the insertion of data that already exists?
You can use LOCKs to make things SERIALIZABLE but this reduces concurrency. Why not try the common condition first ("mostly insert or mostly select") followed by safe handling of "remedial" action? That is, the "JFDI" pattern...
Mostly INSERTs expected (ball park 70-80%+):
Just try to insert. If it fails, the row has already been created. No need to worry about concurrency because the TRY/CATCH deals with duplicates for you.
BEGIN TRY
INSERT Table VALUES (#Value)
SELECT #id = SCOPE_IDENTITY()
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE -- only error was a dupe insert so must already have a row to select
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
END CATCH
Mostly SELECTs:
Similar, but try to get data first. No data = INSERT needed. Again, if 2 concurrent calls try to INSERT because they both found the row missing the TRY/CATCH handles.
BEGIN TRY
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
IF ##ROWCOUNT = 0
BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPE_IDENTITY()
END
END TRY
BEGIN CATCH
IF ERROR_NUMBER() <> 2627
RAISERROR etc
ELSE
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
END CATCH
The 2nd one appear to repeat itself, but it's highly concurrent. Locks would achieve the same but at the expense of concurrency...
Edit:
Why not to use MERGE...
If you use the OUTPUT clause it will only return what is updated. So you need a dummy UPDATE to generate the INSERTED table for the OUTPUT clause. If you have to do dummy updates with many calls (as implied by OP) that is a lot of log writes just to be able to use MERGE.
// CREATE TABLE Table (RowID INT NOT NULL IDENTITY(1,1), RowValue VARCHAR(50))
-- be sure to have a non-clustered unique index on RowValue and RowID as your clustered index.
IF EXISTS (SELECT * FROM Table WHERE RowValue = #VALUE)
SELECT #id = RowID FROM Table WHERE RowValue = #VALUE
ELSE BEGIN
INSERT Table VALUES (#Value)
SELECT #id = SCOPEIDENTITY()
END
As always, gbn's answer is correct and ultimately lead me to where I needed to be. However, I found a particular edge case that wasn't covered by his approach. That being a 2601 error which identifies a Unique Index Violation.
To compensate for this, I've modified his code as follow
...
declare #errornumber int = ERROR_NUMBER()
if #errornumber <> 2627 and #errornumber <> 2601
...
Hopefully this helps someone!

Resources