QUESTION:
What approach should I use to notify one databases about the changes made to a table in another database. Note: I need one notification per statement level event, this includes the merge statement which does a insert, update and delete in one.
BACKGROUND:
We're working with a third party to transfer some data from one system to another. There are two databases of interest here, one which the third party populates with normalised staging data and a second database which will be populated with the de-normalised post processed data. I've created MERGE scripts which do the heavy lifting of processing and transferral of the data from these staging tables into our shiny denormalised version, and I've written a framework which manages the data dependencies such that look-up tables are populated prior to the main data etc.
I need a reliable way to be notified of when the staging tables are updated so that my import scripts are run autonomously.
METHODS CONSIDERED:
SQL DML Triggers
I initially created a generic trigger which sends change information to the denormalised database via service broker, however this trigger is firing three times, once for insert, update and delete and is thus sending three distinct messages which is causing the import process to run three times for a single data change. It should be noted that these staging tables are also being updated using the MERGE functionality within SQL Server, so is handled in a single statement.
SQL Query Notification
This appears to be perfect for what I need, however there doesn't appear to be anyway to subscribe to notifications from within SQL Server, this can only be of used to notify change at an application layer written in .net. I guess I maybe able to manage this via CLR integration, however I'd still need to drive the notification down to the processing database to trigger the import process. This appears to be my best option although it will be long winded, difficult to debug and monitor, and probably over complicating an otherwise simple issue.
SQL Event Notification
This would be perfect although doesn't appear to function for DML, regardless of what you might find in the MS documentation. The create event notification command takes a single parameter for event_type so can be thought of as operating at the database level. DML operates at an entity level and there doesn't appear to be anyway to target a specific entity using the defined syntax.
SQL Change Tracking
This appears to capture changes on a database table but at a row level and this seems to be too heavy handed for what I require. I simply need to know that a change has happened, I'm not really interested in which rows or how many, besides I'd still need to convert this into an event to trigger the import process.
SQL Change Data Capture
This is an extension of Change Tracking and records both the change and the history of the change at the row level. This is again far too detailed and still leaves me with the issue of turning this into a notification of some kind so that import process can be kicked off.
SQL Server Default Trace / Audit
This appears to require a target which must be of either a Windows Application / Security event log or a file on the IO which I'd struggle to monitor and hook into for changes.
ADDITIONAL
My trigger based method would work wonderfully if only the trigger was fired once. I have considered creating a table to record the first of the three DML commands which could then be used to suspend the posting of information within the other two trigger operations, however I'm reasonable sure that all three DML triggers (insert, update delete) will fire in parallel rending this method futile.
Can anyone please advise on a suitable approach that ideally doesn't use a scheduled job to check for changes. Any suggestions gratefully received.
This simplest approach has been to create a secondary table to record when the trigger code is run.
CREATE TABLE [service].[SuspendTrigger]
(
[Index] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](200) NOT NULL,
[DateTime] [datetime] NOT NULL,
[SPID] [int] NOT NULL,
CONSTRAINT [pk_suspendtrigger_index] PRIMARY KEY CLUSTERED
(
[Index] ASC
) ON [PRIMARY]
) ON [PRIMARY]
Triggers run sequentially so even when a merge statement is applied to an existing table the insert, update and delete trigger code run one after the other.
The first time we enter the trigger we can therefore write to this suspension table to record the event and then execute what ever code needs to be executed.
The second time we enter the trigger we can check to see if a record already exists and therefore prevent execution of any further statements.
alter trigger [dbo].[trg_ADDRESSES]
on [dbo].[ADDRESSES]
after insert, update, delete
as
begin
set nocount on;
-- determine the trigger action - not trigger may fire
-- when nothing in either update or delete table
------------------------------------------------------
declare #action as nvarchar(6) = (case when ( exists ( select top 1 1 from inserted )
and exists ( select top 1 1 from deleted )) then N'UPDATE'
when exists ( select top 1 1 from inserted ) then N'INSERT'
when exists ( select top 1 1 from deleted ) then N'DELETE'
end)
-- check for valid action
-------------------------
if #action is not null
begin
if not exists ( select *
from [service].[SuspendTrigger] as [suspend]
where [suspend].[SPID] = ##SPID
and [suspend].[DateTime] >= dateadd(millisecond, -300, getdate())
)
begin
-- insert a suspension event
-----------------------------
insert into [service].[SuspendTrigger]
(
[Name] ,
[DateTime] ,
[SPID]
)
select object_name(##procid) as [Name] ,
getdate() as [DateTime] ,
##SPID as [SPID]
-- determine the message content to send
----------------------------------------
declare #content xml = (
select getdate() as [datetime] ,
db_name() as [source/catelogue] ,
'DBO' as [source/table] ,
'ADDRESS' as [source/schema] ,
(select [sessions].[session_id] as [#id] ,
[sessions].[login_time] as [login_time] ,
case when ([sessions].[total_elapsed_time] >= 864000000000) then
formatmessage('%02i DAYS %02i:%02i:%02i.%04i',
(([sessions].[total_elapsed_time] / 10000 / 1000 / 60 / 60 / 24)),
(([sessions].[total_elapsed_time] / (1000*60*60)) % 24),
(([sessions].[total_elapsed_time] / (1000*60)) % 60),
(([sessions].[total_elapsed_time] / (1000*01)) % 60),
(([sessions].[total_elapsed_time] ) % 1000))
else
formatmessage('%02i:%02i:%02i.%i',
(([sessions].[total_elapsed_time] / (1000*60*60)) % 24),
(([sessions].[total_elapsed_time] / (1000*60)) % 60),
(([sessions].[total_elapsed_time] / (1000*01)) % 60),
(([sessions].[total_elapsed_time] ) % 1000))
end as [duration] ,
[sessions].[row_count] as [row_count] ,
[sessions].[reads] as [reads] ,
[sessions].[writes] as [writes] ,
[sessions].[program_name] as [identity/program_name] ,
[sessions].[host_name] as [identity/host_name] ,
[sessions].[nt_user_name] as [identity/nt_user_name] ,
[sessions].[login_name] as [identity/login_name] ,
[sessions].[original_login_name] as [identity/original_name]
from [sys].[dm_exec_sessions] as [sessions]
where [sessions].[session_id] = ##SPID
for xml path('session'), type)
for xml path('persistence_change'), root('change_tracking'))
-- holds the current procedure name
-----------------------------------
declare #procedure_name nvarchar(200) = object_name(##procid)
-- send a message to any remote listeners
-----------------------------------------
exec [service].[usp_post_content_via_service_broker] #MessageContentType = 'Source Data Change', #MessageContent = #content, #CallOriginator = #procedure_name
end
end
end
GO;
All we need to do now is create an index on the [datetime] field within the suspension table so that this is used during the check. I'll probably also create a job to clear down any entries older than a couple of minutes to try to keep the contents down.
Either way, this provides a way of ensuring that only on notification is generated per table level modification.
if your interested the message contents will look something like this ...
<change_tracking>
<persistence_change>
<datetime>2016-08-01T16:08:10.880</datetime>
<source>
<catelogue>[MY DATABASE NAME]</catelogue>
<table>DBO</table>
<schema>ADDRESS</schema>
</source>
<session id="1014">
<login_time>2016-08-01T15:03:01.993</login_time>
<duration>00:00:01.337</duration>
<row_count>1</row_count>
<reads>37</reads>
<writes>68</writes>
<identity>
<program_name>Microsoft SQL Server Management Studio - Query</program_name>
<host_name>[COMPUTER NAME]</host_name>
<nt_user_name>[MY ACCOUNT]</nt_user_name>
<login_name>[MY DOMAIN]\[MY ACCOUNT]</login_name>
<original_name>[MY DOMAIN]\[MY ACCOUNT]</original_name>
</identity>
</session>
</persistence_change>
</change_tracking>
I could send over the action that triggered the notification but I'm only really interested in the fact that some data has changed in this table.
Related
I am using JMETER for load testing a SQL DB and I tryed to do (in a single JDBC Sampler):
IF NOT OBJECT_ID('tempdb.dbo.#tmp') IS NULL
BEGIN DROP TABLE #tmp END
CREATE TABLE #tmp ( entity numeric(4, 0) NOT NULL, entityCode numeric(18, 0) NOT NULL, entityTraceabilityDeclared numeric(18, 0) NOT NULL )
CREATE NONCLUSTERED INDEX IX_#tmp_entityCode_entity ON #tmp ( entityCode, entity )
{CALL entityTraceability_UpdatePieceLastMovement }
But i got a error saying "invalid object name #tmp"
If i do
SELECT * FROM #tmp
Instead of calling the sp it works.
Thanks.
You are creating a temporary table by using # in the front. This means it will only be available in the exact session it was created in. It will not be visible by any sessions you create outside of jmeter or in fact any sessions in jmeter that aren't the session that issued the create (in case you are creating multiple threads).
I noticed you are using .dbo. in tempdb.dbo.#tmp which could be an issue if dbo is not the default database. Might want to use tempdb..#tmp instead since temps go there.
Also, you could maybe print out ##SPID to make sure that you are actually doing things in the sessions you think you are. Just a thought.
After create a Stored Procedure in a Table " dbo.terms" to insert a data in it using this code:
CREATE PROCEDURE dbo.terms
#Term_en NVARCHAR(50) = NULL ,
#Createdate DATETIME = NULL ,
#Writer NVARCHAR(50) = NULL ,
#Term_Subdomain NVARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON
INSERT INTO dbo.terms
(
Term_en ,
Createdate ,
Writer ,
Term_Subdomain
)
VALUES
(
#Term_en = 'Cat' ,
#Createdate = '2013-12-12' ,
#Writer = 'Fadi' ,
#Term_Subdomain = 'English'
)
END
GO
I want to Create a Trigger in it to add another rows in a table dbo.term_prop
I used this code :
CREATE TRIGGER triggerdata
AFTER INSERT
ON dbo.terms
FOR EACH ROW
BEGIN
INSERT INTO dbo.term_prop VALUES
('قطة', term_ar, upper(:new.term_ar) , null , 'chat', term_fr, upper(:new.term_fr) , null ,'Animal', Def_en, upper(:new.Def_en) , null ,'حيوان', Def_ar, upper(:new.Def_ar) , null ,'Animal', Def_fr, upper(:new.Def_fr) , null);
END;
and it shows me an Error
To add more rows you can use SELECTED table.
This is a special table populated with rows inserted in your transaction.
An example is:
INSERT INTO dbo.term_prop VALUES
SELECT * FROM inserted
So you mustn't use FOR EACH ROW.
The correct definition of your trigger will be
CREATE TRIGGER triggername ON table AFTER INSERT
AS BEGIN
END
Joe answer is a good one and this is more a advice.
Avoid triggers, they can cause maintenance nightmares: are trick to maintain and debug.
If you want to inserts tables in another table after inserting in the first one just put that code in the same SP.
If you need a auto identity generated value you can do it by using ##identity or scope_identity() or ident_current().
Try to keep things simple.
Wow, I am still surprised that triggers are given a bad wrap! I wrote a dozen articles on them a long time ago ...
Like anything in life, the use of triggers depends on the situation.
1 - Trigger are great to track DDL changes. Who changed that table?
http://craftydba.com/?p=2015
2 - Triggers can track DML changes (insert, update, delete). However, on large tables with large transaction numbers, they can slow down processing.
http://craftydba.com/?p=2060
However, with today's hardware, what is slow for me might not be slow for you.
3 - Triggers are great at tracking logins and/or server changes.
http://craftydba.com/?p=1909
So, lets get back to center and talk about your situation.
Why are you trying to make a duplicate entry on just an insert action?
Other options right out of the SQL Server Engine to solve this problem, are:
1 - Move data from table 1 to table 2 via a custom job. "Insert Into table 1 select * from table 2 where etl_flag = 0;". Of course make it transactional and update the flag after the insert is complete. I am just considering inserts w/o deletes or updates.
2 - If you want to track just changes, check out the change data capture. It reads from the transaction log. It is not as instant as a trigger, ie - does not fire for every record. Just runs as a SQL Agent Job in the background to load cdc.tables.
3 - Replicate the data from one server1.database1.table1 to server2.database2.table2.
ETC ...
I hope my post reminds everyone that the situation determines the solution.
Triggers are good in certain situations, otherwise, they would have been removed from the product a long time ago.
And if the situation changes, then the solution might have to change ...
I want to create a simple queue with a sql database as backend.
the table have the fields, id, taskname,runat(datetime) and hidden(datetime).
I want to ensure a queue item is not run once and only once.
The idea is when a client want to dequeue, a stored procedure selects the first item(sorted by runat and hidden < now), sets the hidden field to current time + 2min and returns the item.
How does MS Sql (Azure to be precise) wokr, will two clients be able to run at the same time and both set the same item to hidden and return it? Or can i be sure that they are run one by one and the second one will not return the same item as the hidden field was changed with the first?
The key is to get a lock (Row or table) on the queue item you are receiving. You can use a couple of ways, my favorite being the UPDATE with OUTPUT clause. Either will produce serialized access to the table.
Example:
CREATE PROCEDURE spGetNextItem_output
BEGIN
BEGIN TRAN
UPDATE TOP(1) Messages
SET [Status] = 1
OUTPUT INSERTED.MessageID, INSERTED.Data
WHERE [Status] = 0
COMMIT TRAN
END
CREATE PROCEDURE spGetNextItem_tablockx
BEGIN
BEGIN TRAN
DECLARE #MessageID int, #data xml
SELECT TOP(1) #MessageID = MessageID, #Data = Data
FROM Messages WITH (ROWLOCK, XLOCK, READPAST) --lock the row, skip other locked rows
WHERE [Status] = 0
UPDATE Messages
SET [Status] = 1
WHERE MessageID = #MessageID
SELECT #MessageID AS MessageID, #Data as Data
COMMIT TRAN
END
Table definition:
CREATE TABLE [dbo].[Messages](
[MessageID] [int] IDENTITY(1,1) NOT NULL,
[Status] [int] NOT NULL,
[Data] [xml] NOT NULL,
CONSTRAINT [PK_Messages] PRIMARY KEY CLUSTERED
(
[MessageID] ASC
)
)
Windows Azure SQL Database is going to behave just like a SQL Server database in terms of concurrency. This is a database problem, not a Windows Azure problem.
Now: If you went with Windows Azure Queues (or Service Bus Queues) rather than implementing your own, then the behavior is well documented. For instance: with Azure Queues, first-in gets the queue item, and then the item is marked as invisible until it's either deleted or a timeout period has been reached.
what do Stored Procedures and Triggers in data base mean ?
how can i create Stored Procedures ?
how can i crest Triggers ?
if you have simple examples for each of these .please help :)
what i know is only about trigger which is activated if an action of(insert or delete or update ) violates the constrains specified but i don't know how to create ,so again if any have example please
Think of a Stored Procedure as a method in your code. It runs a specific set of instructions.
Stored Procedures are created to, for example, manage complex sets of data that would normally be a pain to handle along in your code.
You can create a Stored Procedure with the following instructions:
Oracle
CREATE OR REPLACE PROCEDURE P_PROCEDURE_NAME (
pParameter1 NUMBER
, pParameter2 VARCHAR2(100 Bytes)
) AS
BEGIN
-- Procedure code here...
END;
SQL Server
CREATE PROCEDURE cspProcedureName
#parameter1 int
, #parameter2 nvarchar(100)
AS
-- Procedure code here...
Oracle
As for the Triggers, they are sets of code called upon an action occuring to the related table. For instance, in Oracle, there are no INDENTITY columns such as SQL Server offers. Instead, Sequences are used along with Triggers to simulate the same. Hence, you will need to create an Oracle SEQUENCE, then the TRIGGER to update the ID field of your table.
CREATE SEQUENCE SEQ_CUSTOMERS
MINVALUE 1
MAXVALUE 65535
START WITH 1
INCREMENT BY 1;
CREATE OR REPLACE TRIGGER TRG_CUSTOMERS_INSERT
BEFORE INSERT
ON TBL_CUSTOMERS
FOR EACH ROW
BEGIN
:NEW.CUST_ID := SEQ_CUSTOMERS.NEXTVAL;
END;
SQL Server
A trigger example in SQL Server would be updating automatically the update datetime of a record. Consider the following:
CREATE TABLE Customers (
CustId int NOT NULL IDENTITY(1, 1) PRIMARY KEY
, CustName nvarchar(100) NOT NULL
, CreatedOn datetime DEFAULT GETDATE()
, LastUpdate datetime NOT NULL
)
GO
CREATE TRIGGER trgCustomersUpdt
AFTER UPDATE
ON Customers
AS
update Customers
set LastUpdate = GETDATE()
where CustId = inserted.Custid
GO
DISCLAIMER
This code has not been tested and may require minor changes for it to work properly against its respective RDBMS.
To sum it up, Triggers are mainly used to as illustrated here, despite there are many other possible use, such as building up an history of table changes that occured throught time, keeping all records of transactions into an history table or the like. The Stored Procedures are mainly used to perform complex database tasks where this would get too complex to do in code.
I am working with an insert trigger within a Sybase database. I know I can access the ##nestlevel to determine whether I am being called directly or as a result of another trigger or procedure.
Is there any way to determine, when the nesting level is deeper than 1, who performed the action causing the trigger to fire?
For example, was the table inserted to directly, was it inserted into by another trigger and if so, which one.
As far as I know, this is not possible. Your best bet is to include it as a parameter to your stored procedure(s). As explained here, this will also make your code more portable since any method used would likely rely on some database-specific call. The link there was specific for SQL Server 2005, not Sybase, but I think you're pretty much in the same boat.
I've not tested this myself, but assuming you are using Sybase ASE 15.03 or later, have your monitoring tables monProcessStatement and monSysStatement enabled, and appropriate permissions set to allow them to be accessed from your trigger you could try...
declare #parent_proc_id int
if ##nestlevel > 1
begin
create table #temp_parent_proc (
procId int,
nestLevel int,
contextId int
)
insert into #temp_parent_proc
select mss.ProcedureID,
mss.ProcNestLevel,
mss.ContextID
from monSysStatement mss
join monProcessStatement mps
on mss.KPID = mps.KPID
and mss.BatchID = mps.BatchID
and mss.SPID = mps.SPID
where mps.ProcedureID =##procid
and mps.SPID = ##spid
select #parent_proc_id = (select tpp.procId
from #temp_parent_proc tpp,
#temp_parent_proc2 tpp2
where tpp.nestLevel = tpp2.nestLevel-1
and tpp.contextId < tpp2.contextId
and tpp2.procId = ##procid
and tpp2.nestLevel = ##nestlevel
group by tpp.procId, tpp.contextId
having tpp.contextId = max(tpp.contextId ))
drop table #temp_parent_proc
end
The temp table is required because of the nature of monProcessStatement and monSysStatement.
monProcessStatement is transient and so if you reference it more than once, it may no longer hold the same rows.
monSysStatement is a historic table and is guaranteed to only return an individual rown once to any process accessing it.
if you do not have or want to set permissions to access the monitoring tables, you could put this into a stored procedure you pass ##procid, ##spid, and ##nestlevel to as parameters.
If this also isn't an option, since you cannot pass parameters into triggers, another possible work around would be to use a temporary table.
in each proc that might trigger this...
create table #trigger_parent (proc_id int)
insert into #trigger_parent ##procid
then in your trigger the temp table will be available...
if object_id('#trigger_parent') is not null
set #parent_proc = select l proc_id from #trigger_parent
you will know it was triggered from within another proc.
The trouble with this is it doesn't 'just work'. You have to enforce temp table setup.
You could do further checking to find cases where there is no #trigger_parent but the nesting level > 1 and combine a similar query to the monitoring tables as above to find potential candidates that would need to be updated.