I have stored procedure that handles Insert and Update transactions in one of my tables for now. I'm still testing to see if there is any potential problem with this solution and how I can improve the process. This SP takes few arguments then checks for matching ID and preforms Insert or Update. I have read this Article about primary key violation error, showing that MERGE is vulnerable to concurrency problems like a multi-statement conditional INSERT/UPDATE . Seems that they have solved some issues using WITH (HOLDLOCK). I'm new in stored procedure and merge world. I would like to here your opinion if this is reliable code for application with high transactions? I might have multiple users Inserting in the same table or running Update statement at the same time. Also is there any potential issue with parameter sniffing in this case? If it is should I consider using OPTION (RECOMPILE) or that only applies to SELECT search queries? Here is example of my SQL code:
USE [TestDB]
GO
/****** Object: StoredProcedure [dbo].[SaveMaster] Script Date: 08/21/2018 10:05:21 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: M, D
-- Create date: 08/21/2018
-- Description: Insert/Update Master table
-- =============================================
ALTER PROCEDURE [dbo].[SaveMaster]
#RecordID INT = NULL,
#Status BIT = NULL,
#Name VARCHAR(50) = NULL,
#Code CHAR(2) = NULL,
#ActionDt DATETIME = NULL,
#ActionID UNIQUEIDENTIFIER = NULL
AS
MERGE dbo.Master WITH (HOLDLOCK) AS Target
USING (SELECT #RecordID,#Status,#Name,#Code,#ActionDt,#ActionID)
AS Source (RecordID,Status,Name,Code,ActionDt,ActionID)
ON Target.RecID = Source.RecordID
WHEN MATCHED THEN
UPDATE
SET Target.Status = Source.Status,
Target.Name = Source.Name,
Target.Code = Source.Code,
Target.ActionDt = Source.ActionDt,
Target.ActionID = Source.ActionID
WHEN NOT MATCHED THEN
INSERT(
Status,Name,Code,ActionDt,ActionID
)VALUES(
Source.Status,
Source.Name,
Source.Code,
Source.ActionDt,
Source.ActionID
);
RETURN ##ERROR;
Here is example on how I call Stored Procedure with server side language (ColdFusion 2016):
<cftransaction action="begin">
<cftry>
<cfstoredproc procedure="SaveMaster" datasource="#dsn#">
<cfprocparam dbvarname="#RecordID" value="#trim(arguments.frm_recid)#" cfsqltype="cf_sql_integer" null="#!len(trim(arguments.frm_recid))#" />
<cfprocparam dbvarname="#Status" value="#trim(arguments.frm_status)#" cfsqltype="cf_sql_bit" null="#!len(trim(arguments.frm_status))#" />
<cfprocparam dbvarname="#Name" value="#trim(arguments.frm_name)#" cfsqltype="cf_sql_varchar" maxlength="50" null="#!len(trim(arguments.frm_name))#" />
<cfprocparam dbvarname="#Code" value="#trim(frm_code)#" cfsqltype="cf_sql_char" maxlength="2" null="#!len(trim(frm_code))#" />
<cfprocparam dbvarname="#ActionDt" value="#trim(NOW())#" cfsqltype="cf_sql_datetime" />
<cfprocparam dbvarname="#ActionID" value="#trim(SESSION.UserID)#" cfsqltype="cf_sql_idstamp" null="#!len(trim(SESSION.UserID))#" />
<cfprocresult name="MasterResult"/>
</cfstoredproc>
<cfset local.fnResults = {status : "200", message : "Record successully saved!", RecID : MasterResult.RecID}>
<cfcatch type="any">
<cftransaction action="rollback" />
<cfset local.fnResults = {status : "400", message : "Error! Please contact your administrator."}>
</cfcatch>
</cftry>
</cftransaction>
As you can see I expect that Stored Procedure returns RecID that should be returned (Same ID that I pass in my stored procedure for existing records, or if does not exist then will be generated and returned like this for Insert SELECT SCOPE_IDENTITY() AS RecID; or Update like this SELECT #RecordID AS RecID). If anyone have any suggestions and know the best way to return RecID from SP that runs Insert/Update with Merge please let me know.
If anyone have any suggestions and know the best way to return RecID from SP that runs Insert/Update with Merge please let me know.
You can add an OUTPUT clause to your MERGE statement. That will allow you to return a result set containing the new ID and, if you like, what action it selected:
MERGE dbo.Master WITH (HOLDLOCK) AS Target
USING (SELECT #RecordID,#Status,#Name,#Code,#ActionDt,#ActionID)
AS Source (RecordID,Status,Name,Code,ActionDt,ActionID)
ON Target.RecID = Source.RecordID
WHEN MATCHED THEN
UPDATE
SET Target.Status = Source.Status,
Target.Name = Source.Name,
Target.Code = Source.Code,
Target.ActionDt = Source.ActionDt,
Target.ActionID = Source.ActionID
WHEN NOT MATCHED THEN
INSERT(
Status,Name,Code,ActionDt,ActionID
)VALUES(
Source.Status,
Source.Name,
Source.Code,
Source.ActionDt,
Source.ActionID
)
OUTPUT inserted.RedIC,$action as Action;
I'm assuming coldfusion will be able to consume this result set. If not, switch to the variant of OUTPUT which populates a table variable instead (OUTPUT ... INTO) and use that to set OUTPUT parameters which you add to the procedure.
Related
Is it possible populate a schema_only table (with some initial values) when the database starts, e.g. by invoking a stored procedure?
The logic that normally operates on this table would be more complicated if I had to detect whether a cold start had occurred.
edit:
It seems like sp_procoption gets me halfway there. However, the stored procedure configured this way is not executed when ALTER DATABASE <dbname> SET ONLINE; is run. Which is a bummer because the data does go away when ALTER DATABASE <dbname> SET OFFLINE; is run.
You can detect ALTER DATABASE <dbname> SET ONLINE; statements with DDL trigger for ALTER_DATABASE event type. The difficult part is to find when it's state is changed from OFFLINE to ONLINE (instead of some other ALTER DATABASE statement, like MODIFY FILE for example). When the trigger is fired, EVENTDATA() function will return XML like these:
<EVENT_INSTANCE>
<EventType>ALTER_DATABASE</EventType>
<PostTime>2018-12-17T16:26:25.250</PostTime>
<SPID>80</SPID>
<ServerName>xxxxxxx</ServerName>
<LoginName>xxxxxxxxxxxxxxxxxx</LoginName>
<DatabaseName>xxxxx</DatabaseName>
<TSQLCommand>
<SetOptions ANSI_NULLS="ON" ANSI_NULL_DEFAULT="ON" ANSI_PADDING="ON" QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" />
<CommandText>ALTER DATABASE xxxxx SET OFFLINE</CommandText>
</TSQLCommand>
</EVENT_INSTANCE>
<EVENT_INSTANCE>
<EventType>ALTER_DATABASE</EventType>
<PostTime>2018-12-17T16:26:36.953</PostTime>
<SPID>80</SPID>
<ServerName>xxxxxxx</ServerName>
<LoginName>xxxxxxxxxxxxxxxxxx</LoginName>
<DatabaseName>xxxxx</DatabaseName>
<TSQLCommand>
<SetOptions ANSI_NULLS="ON" ANSI_NULL_DEFAULT="ON" ANSI_PADDING="ON" QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" />
<CommandText>ALTER DATABASE xxxxx SET ONLINE</CommandText>
</TSQLCommand>
</EVENT_INSTANCE>
In theory you can try to parse <CommandText>, but it may not be as easy as it sounds. Instead of that, you can check is your database currently ONLINE and are there any rows in the schema_only table. Of course, in the trigger you should also check is this event related to your database. So the trigger could look something like this:
CREATE TRIGGER DDL_ALTER_DATABASE_TRIGGER
ON ALL Server
FOR ALTER_DATABASE
AS
BEGIN
declare #DatabaseName nvarchar(200), #TSQL nvarchar(2000), #event XML
select #event = EVENTDATA()
select #DatabaseName = #event.value('(/EVENT_INSTANCE/DatabaseName)[1]','varchar(200)' )
select #TSQL = #event.value('(/EVENT_INSTANCE/TSQLCommand)[1]','varchar(2000)' ) -- Check the command text if you want
if #DatabaseName = '<my database name>'
begin
declare #DatabaseCurrentState int
select #DatabaseCurrentState = state
from sys.databases
where name = '<my database name>'
if #DatabaseCurrentState = 0 -- It is ONLINE now
begin
if not exists(select * from [<my database name>].schema.schema_only_table)
begin
insert into [<my database name>].schema.schema_only_table(field1, field2)
values(1, 2)
-- or simply execute your auto executed stored procedure here
end
end
end
END
I am currently working on getting a set of records from a view in the Oracle database and trying to insert/update them in to the table in the SQL Server table depending on a column using BizTalk.
For this I created a stored procedure:
Create PROCEDURE [dbo].[uspInsertorUpdateDepartment]
#dept_name varchar(64),
#jax_dept_id char(32)
AS
BEGIN
SET NOCOUNT ON;
IF (SELECT TOP (1) 1 FROM afm.[jax_dept]
WHERE jax_dept_id = #jax_dept_id) IS NULL
INSERT INTO afm.[jax_dept](dept_name, jax_dept_id)
VALUES (#dept_name,#jax_dept_id)
ELSE
UPDATE afm.[jax_dept]
SET dept_name = #dept_name
WHERE jax_dept_id = #jax_dept_id
END
I created the schema for the stored procedure using consume adapter service. Used them in the mapping and the orchestration. Though I was not able to use the lopping functoid in the mapping
So removed the lopping and deployed the application. And tried to run and it ran without any error but just insert the first record from the oracle view in to the SQL Server database leaving all the other records. How can this be approached so the entire set of records from the oracle is inserted/updated in to SQL Server database.
Here I converted the separate update and insert into one merge statement:
Create PROCEDURE [dbo].[uspInsertorUpdateDepartment]
#dept_name varchar(64),
#jax_dept_id char(32)
AS
BEGIN
SET NOCOUNT ON;
merge afm.[jax_dept] as target
using (select #dept_name as dept_name, #jax_dept_id as jax_dept_id) as source
on source.jax_dept_id = target.jax_dept_id
when matched then
update target
SET dept_name = #dept_name
when not matched then
insert (dept_name, jax_dept_id)
values (#dept_name,#jax_dept_id)
;
END
Use table type as a parameter for the SP, instead of passing individually. We can
use looping functoid if we use User Defined Table value as a parameter.
CREATE TYPE dbo.SampleType AS TABLE
(
dept_name varchar(64) not null,
jax_dept_id char(32) not null
)
---
Create PROCEDURE [dbo].[uspInsertorUpdateDepartment]
#TVP dbo.SampleType READONLY
AS
BEGIN
SET NOCOUNT ON;
--your insert or update query
For more infor on how to use table value parameter check out this link:-
https://learn.microsoft.com/en-us/sql/relational-databases/tables/use-table-valued-parameters-database-engine
I have a windows forms application that needs to edit an existing record if it already exists and create it if it does not. I'm using SQL Server 2008 R2. My application reads data from various tables which includes an ID field for the output table if a record already exists.
The ID field is blank if a new record is being created. The ID field is the primary key and an Identity (auto increment) field for the destination table.
I have created a stored procedure using MERGE that I hope will create a new record or update the existing one. The update part is working but I can't figure out what to do with the ID field when creating.
When doing an update I pass in an ID Parameter and the existing record is located. Obviously if it is a new record I won't have an ID yet but I can't then leave that Parameter out or I get an unassigned variable error as you would expect.
Here is my stored procedure. Am I just barking up the wrong tree here
somewhere?
Should I just create two stored procedures and call Update if I have and ID and Call Create if I don't have and ID?
Thanks for any assistance.
USE [Insurance]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[CreateModifyValuation]
-- Add the parameters for the stored procedure here
#ValuationID int,
#OwnersCorporationID int,
#ValDate datetime,
#ValuerID int,
#Amount money,
#Printed bit,
#Approved bit,
#Notes varchar(max),
#MultiplierValue money,
#MultiplierClass char(10),
#Adjustment money,
#SubmittedDate datetime
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for procedure here
Merge Valuation as Target
USING (Select
#ValuationID,
#OwnersCorporationID,
#ValDate,
#ValuerID,
#Amount,
#Printed,
#Approved,
#Notes,
#MultiplierValue,
#MultiplierClass,
#Adjustment,
#SubmittedDate
)
As Source(
ValuationID,
OwnersCorporationID,
ValDate,
ValuerID,
Amount,
Printed,
Approved,
Notes,
MultiplierValue,
MultiplierClass,
Adjustment,
SubmittedDate
)
ON Source.ValuationID = Target.ValuationID
WHEN MATCHED THEN
UPDATE SET
Target.OwnersCorporationID = Source.OwnersCorporationID,
Target.ValDate = Source.ValDate,
Target.ValuerID = Source.ValuerID,
Target.Amount = Source.Amount,
Target.Printed = Source.Printed,
Target.Approved = Source.Approved,
Target.Notes = Source.Notes,
Target.MultiplierValue = Source.MultiplierValue,
Target.MultiplierClass = Source.MultiplierClass,
Target.Adjustment = Source.Adjustment,
Target.SubmittedDate = Source.SubmittedDate
WHEN NOT MATCHED BY Target THEN
INSERT (
OwnerscorporationID,
ValDate,
ValuerID,
Amount,
Printed,
Approved,
Notes,
MultiplierValue,
MultiplierClass,
Adjustment,
SubmittedDate
)
VALUES (
Source.OwnersCorporationID,
Source.ValDate,
Source.ValuerID,
Source.Amount,
Source.Printed,
Source.Approved,
Source.Notes,
Source.MultiplierValue,
Source.MultiplierClass,
Source.Adjustment,
Source.SubmittedDate
);
END
I feel like I cheated but it's only one line so how bad can it be :)
In My SQL I added this line before the "SET NOCOUNT ON;"
(Funny if I put it after SET NOCOUNT ON I get a syntax error)
if (#ValuationID = 0) set #ValuationID = null
Then in my C# code I set the ID to 0 for a new record and it seems to work after a couple of tests. There may be a better way to do this but like most things in life you stop looking once it works.
Thanks again to those who commented.
David
Here is my stored procedure:
ALTER PROCEDURE [dbo].[Dan]
#numbers varchar(10)
AS
BEGIN
SET NOCOUNT ON;
select #numbers numbers
END
In SSMS, I can execute it successfully like this:
exec dbo.Dan '1.2'
In ColdFusion, I can execute it successfully with either of these two sets of commands:
<cfstoredproc procedure="dbo.dan" datasource="ClinicalDataDev">
<cfprocparam cfsqltype="cf_sql_varchar" value="1,2">
<cfprocresult name="abc">
</cfstoredproc>
<cfquery name="abcd" datasource="ClinicalDataDev">
exec dbo.Dan <cfqueryparam cfsqltype="cf_sql_varchar" value='1,2' list="no">
</cfquery>
However, I'm looking to improve on this by specifying the value as a list of integers. Since cfprocparam does not have a list attribute, I think I am restricted to the cfquery approach. My efforts and results so far are:
<cfqueryparam cfsqltype="cf_sql_integer" value='1' list="no">
executes successfully. The purpose is to see if the procedure accepts an
integer - it does.
<cfqueryparam cfsqltype="cf_sql_integer" value='1,2' list="no">
also executes sucessfully, but returns a value of 42006. Probably not
worth persuing.
<cfqueryparam cfsqltype="cf_sql_integer" value='1,2' list="yes">
throws an error for two many paramters.
The same thing happens with cf_sql_varchar.
As stated earlier, I can pass the list as a string, but that seems like a bit of a hack. Is there a way to pass the list of integers as a list of integers?
As other commenters mentioned before, passing table-valued parameters is the way to go. This will require you to change the input in the SP to take a table input and in your query you can do
<cfquery>
CREATE TABLE #temp (usedID int)
INSERT INTO #temp
(usedID)
VALUES
(1)
,(2)
exec dbo.Dan #temp
</cfquery>
You may have to change the way you pass your parameters to the SP, but this is the general idea.
rodmunera's answer has the correct general idea. Here is how I finally got it to work.
In sql server, I started with this:
CREATE TYPE pt.IntegerTableType AS TABLE
( integerIN int);
grant execute on type::pt.IntegerTableType to theAppropriateRole
Then I changed by stored proc to this:
ALTER PROCEDURE [dbo].[Dan]
#numbers pt.IntegerTableType readonly
AS
BEGIN
SET NOCOUNT ON;
select 1 record
where 1 in (select integerIN from #numbers)
END
The Coldfusion code is this:
<cfset numbers = "1,2">
<cfquery name="abcd" datasource="ClinicalDataDev">
declare #dan as pt.IntegerTableType
insert into #dan
select null
where 1 = 2
<cfloop list="#numbers#" index="number">
union
select <cfqueryparam cfsqltype="cf_sql_integer" value="#number#">
</cfloop>
exec dbo.Dan #dan
</cfquery>
<cfdump var="#abcd#">
Instead of trigger i am planning to write a procedure which we can run using job which will work same way as TRIGGER
with these two tables in the same way.
how can i do that?
here are my tables with column names
1.tblcal
ID(int,not null)
UID(varchar(10),null)
Desc(varchar(200),null)
Date(datetime,null)
avbl(varchar(5),null)
2.tblEvent
ID(int,notnull)
UID(varchar(10),null)
Desc(varchar(200),null)
Date(datetime,null)
Down is my trigger on tblEvent..
ALTER TRIGGER [dbo].[trU] ON [dbo].[tblEvent]
FOR INSERT
AS
Declare #CuID char(6),
#CuDesc char(40),
#CuDate datetime
SET NOCOUNT ON
Select #CuID = i.UID , #CuDesc=i.Desc, #CuDate=i.Date From Inserted i
If(#CuDesc !='available')
Begin
Update tblCal set avbl='Out', Desc=#CurDesc where cadate=#CuDate and UID=#CuID
ENd
SET NOCOUNT OFF
I have another problem with Desc column.Desc which are going to be in and out Basically we need to update tblcal differently for different descriptions;in that case I don't think trigger is that reliable;Means for example for 10 Desc we need to update in and for other 10 we need to update out
Actually every thursday on the tblevent data is loaded once its loaded it fired a trigger and will update in tblcal.
but my client is looking for a procedure which we can schedule as a job after the tblevent entry done on Thursday.
How can i do with stored procedure?
Procedure
CREATE PROCEDURE dbo.usp_UpdateEventData
AS
BEGIN
SET NOCOUNT ON;
UPDATE C
SET c.avbl = 'Out'
,c.[Desc] = e.[Desc]
FROM [dbo].tblCal C
INNER JOIN [dbo].[tblEvent] e ON c.[UID] = e.[UID]
AND c.cadate = e.[Date] --<-- check if you only want
WHERE e.[Desc] <> 'available' -- to join on date not datetime
END -- CAST both columns to DATE
Also if you are keeping your Trigger as it is you will need to modify the trigger definition to handle multiple Inserts, You can use the same logic as in this procedure to update your trigger definition.
Trigger Fix
ALTER TRIGGER [dbo].[trU] ON [dbo].[tblEvent]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE C
SET c.avbl = 'Out'
,c.[Desc] = i.[Desc]
FROM [dbo].tblCal C
INNER JOIN inserted i ON c.[UID] = i.[UID]
AND c.cadate = i.[Date]
WHERE i.[Desc] <> 'available'
END