SQL Query Exclusive Read/Write to Table - sql-server

SQL Server 2008: I've got a situation here in which I wish to read from a table and write a row under certain conditions. The problem is that I don't want another request coming it at exactly the same time and doing the same thing. I'll try to explain here:
Table Name: RequestQueue
Columns:
RequestID, StartDate, EndDate, RequestResult
Sample Data
1, 12/4/10 1:00pm, 12/4/10 1:02pm, Success
2, 12/4/10 1:04pm, 12/4/10 1:05pm, Success
3, 12/4/10 1:00pm, NULL, NULL
When a page loads in my app, I want it to look at this table and if there is a request still pending like (ID #3) it will not do anything. Otherwise, if there are no requests pending, it creates a new row with the ID and StartDate filled in.
The issue is that we could get into a situation where the page is loaded twice at almost exactly the same time. If they happen to both read from the table before the new row is produced, then I could get two new rows in there. I want to have some sort of query that reads from the table and if there are no requests pending, inserts the new row with the StartDate filled in. I want that query to run all the way before another page can even read from this table so I don't get the "double row" effect.
I might need "locking" or something, I googled that but haven't found something for my exact situation. I'm sure this could be a simple stored procedure I just need a push in the right direction here.
Thanks,
Robert

Assuming that you just want to block all concurrent access to the table you could just do.
BEGIN TRAN
DECLARE #StartDate datetime,
#EndDate datetime
SELECT TOP 1
#StartDate = StartDate,
#EndDate = EndDate
FROM RequestQueue WITH(TABLOCK,XLOCK)
ORDER BY RequestID DESC
IF #EndDate IS NULL
SELECT #StartDate AS 'StartDate'
ELSE
INSERT INTO RequestQueue (StartDate)
OUTPUT INSERTED.* /* Or use SCOPE_IDENTITY() instead*/
VALUES (GETDATE())
COMMIT
Alternatively you could just serialise access to the SELECT/INSERT code inside the specific procedure without taking an exclusive table lock by using sp_getapplock

Related

SSIS data flow - copy new data or update existing

I queried some data from table A(Source) based on certain condition and insert into temp table(Destination) before upsert into Crm.
If data already exist in Crm I dont want to query the data from table A and insert into temp table(I want this table to be empty) unless there is an update in that data or new data was created. So basically I want to query only new data or if there any modified data from table A which already existed in Crm. At the moment my data flow is like this.
clear temp table - delete sql statement
Query from source table A and insert into temp table.
From temp table insert into CRM using script component.
In source table A I have audit columns: createdOn and modifiedOn.
I found one way to do this. SSIS DataFlow - copy only changed and new records but no really clear on how to do so.
What is the best and simple way to achieve this.
The link you posted is basically saying to stage everything and use a MERGE to update your table (essentially an UPDATE/INSERT).
The only way I can really think of to make your process quicker (to a significant degree) by partially selecting from table A would be to add a "last updated" timestamp to table A and enforcing that it will always be up to date.
One way to do this is with a trigger; see here for an example.
You could then select based on that timestamp, perhaps keeping a record of the last timestamp used each time you run the SSIS package, and then adding a margin of safety to that.
Edit: I just saw that you already have a modifiedOn column, so you could use that as described above.
Examples:
There are a few different ways you could do it:
ONE
Include the modifiedOn column on in your final destination table.
You can then build a dynamic query for your data flow source in a SSIS string variable, something like:
"SELECT * FROM [table A] WHERE modifiedOn >= DATEADD(DAY, -1, '" + #[User::MaxModifiedOnDate] + "')"
#[User::MaxModifiedOnDate] (string variable) would come from an Execute SQL Task, where you would write the result of the following query to it:
SELECT FORMAT(CAST(MAX(modifiedOn) AS date), 'yyyy-MM-dd') MaxModifiedOnDate FROM DestinationTable
The DATEADD part, as well as the CAST to a certain degree, represent your margin of safety.
TWO
If this isn't an option, you could keep a data load history table that would tell you when you need to load from, e.g.:
CREATE TABLE DataLoadHistory
(
DataLoadID int PRIMARY KEY IDENTITY
, DataLoadStart datetime NOT NULL
, DataLoadEnd datetime
, Success bit NOT NULL
)
You would begin each data load with this (Execute SQL Task):
CREATE PROCEDURE BeginDataLoad
#DataLoadID int OUTPUT
AS
INSERT INTO DataLoadHistory
(
DataLoadStart
, Success
)
VALUES
(
GETDATE()
, 0
)
SELECT #DataLoadID = SCOPE_IDENTITY()
You would store the returned DataLoadID in a SSIS integer variable, and use it when the data load is complete as follows:
CREATE PROCEDURE DataLoadComplete
#DataLoadID int
AS
UPDATE DataLoadHistory
SET
DataLoadEnd = GETDATE()
, Success = 1
WHERE DataLoadID = #DataLoadID
When it comes to building your query for table A, you would do it the same way as before (with the dynamically generated SQL query), except MaxModifiedOnDate would come from the following query:
SELECT FORMAT(CAST(MAX(DataLoadStart) AS date), 'yyyy-MM-dd') MaxModifiedOnDate FROM DataLoadHistory WHERE Success = 1
So the DataLoadHistory table, rather than your destination table.
Note that this would fail on the first run, as there'd be no successful entries on the history table, so you'd need you insert a dummy record, or find some other way around it.
THREE
I've seen it done a lot where, say your data load is running every day, you would just stage the last 7 days, or something like that, some margin of safety that you're pretty sure will never be passed (because the process is being monitored for failures).
It's not my preferred option, but it is simple, and can work if you're confident in how well the process is being monitored.

Date range based on Column Date

I am using the latest SQL Server. I have a table with a CreatedDate column. I need to write a Query that uses dates that are plus or minus 7 from the Date in CreatedDate. I have no clue how to go about this. My thought was this:
DECLARE #Date datetime
DECLARE #SevenBefore datetime
DECLARE #SevenAfter datetime
SET #Date = CreatedDate
SET #SevenBefore = DATEADD(day,-7,#Date)
SET #SevenAfter = DATEADD(day,7,#Date)
SELECT *
FROM <table>
WHERE <table> BETWEEN #SevenBefore AND #SevenAfter
The issue with this is that I cannot use "CreatedDate" as a SET #DATE because SQL gives an error "Invalid column name 'CreatedDate'"
Any help would be appreciated. I cannot list a date because every date in that column could be different.
Thanks
In this case, you need to stop thinking as a programmer would, and start thinking as a Database programmer would.
Lets work only with this central part of your query:
SELECT *
FROM <table>
WHERE <table> BETWEEN #SevenBefore AND #SevenAfter
Now, you say that the CreatedDate is a column in a table. For this example, I will assume that the CreatedDate is in a table other than the one in your example above. For this purpose, I will give two fake names to the tables. The table with the CreatedDate, I will call tblCreated, and the one from the query above I will call tblData.
Looking above, it's pretty obvious that you can't compare an entire table row to a date. There must be a field in that table that contains a date/time value. I will call this column TargetDate.
Given these assumptions, your query would look something like:
SELECT *
FROM tblCreated tc
INNER JOIN tblData td
ON td.TargetDate BETWEEN DATEADD(day, -7, tc.CreatedDate) and DATEADD(day, 7, tc.CreatedDate)
Looking at this, it is clear that you still need some other associations between the tables. Do you only want all data rows per customer based on the Created date, or perhaps only want Creations where some work was done on them as shown in the Data records, or ??. Without a fuller specification, we can't help with that, though.

SQL server GetDate in trigger called sequentially has the same value

I have a trigger on a table for insert, delete, update that on the first line gets the current date with GetDate() method.
The trigger will compare the deleted and inserted table to determine what field has been changed and stores in another table the id, datetime and the field changed. This combination must be unique
A stored procedure does an insert and an update sequentially on the table. Sometimes I get a violation of primary key and I suspect that the GetDate() returns the same value.
How can I make the GetDate() return different values in the trigger.
EDIT
Here is the code of the trigger
CREATE TRIGGER dbo.TR
ON table
FOR DELETE, INSERT, UPDATE
AS
BEGIN
SET NoCount ON
DECLARE #dt Datetime
SELECT #dt = GetDate()
insert tableLog (id, date, field, old, new)
select I.id, #dt, 'field', D.field, I.field
from INSERTED I LEFT JOIN DELETED D ON I.id=D.id
where IsNull(I.field, -1) <> IsNull(D.field, -1)
END
and the code of the calls
...
insert into table ( anotherfield)
values (#anotherfield)
if ##rowcount=1 SET #ID=##Identity
...
update table
set field = #field
where Id = #ID
...
Sometimes the GetDate() between the 2 calls (insert and update) takes 7 milliseconds and sometimes it has the same value.
That's not exactly full solution but try using SYSDATETIME instead and of course make sure that target table can store up datetime2 up to microseconds.
Note that you can't force different datetime regardless of precision (unless you will start counting up to ticks) as stuff can just happen at the same time wihthin given precision.
If stretching up to microseconds won't solve the issue on practical level, I think you will have to either redesign this logging schema (perhaps add identity column on top of what you have) or add some dirty trick - like make this insert in try catch block and add like microsecond (nanosecond?) in a loop until you insert successfully. Definitely not s.t. I would recommend.
Look at this answer: SQL Server: intrigued by GETDATE()
If you are inserting multiple ROWS, they will all use the same value of GetDate(), so you can try wrapping it in a UDF to get unique values. But as I said, this is just a guess unless you post the code of your trigger so we can see what you are actually doing?
It sounds like you're trying to create an audit trail - but now you want to forge some of the entries?
I'd suggest instead adding a rowversion column to the table and including that in your uniqueness criteria - either instead of or as well as the datetime value that is being recorded.
In this way, even if two rows are inserted with identical date/time data, you can still tell the actual insertion order.

TSQL Trigger with INSTEAD OF

So I am trying to build a simple trigger that will cause an error message to popup if a credit card date entered has passed. If the cards date is still good, I would like it to update or insert as usual. I am still very new to dealing with SQL Server and trying to wrap my head around the contexts of how things should be setup correctly. I have a feeling I am way off on setting this up correctly but any advice would be kindly appreciated.
The Table contains, (CreditCardID (PK), CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate
USE AdventureWorks2012
GO
CREATE TRIGGER BadCreditCardDate
ON Sales.CreditCard
INSTEAD OF UPDATE, INSERT
AS
Begin
DECLARE #ExpMonth tinyint,
#ExpYear smallint
SELECT #ExpMonth=ExpMonth, #ExpYear=ExpYear
FROM INSERTED
IF
#ExpMonth < MONTH(GETDATE())
AND
#ExpYear < YEAR(GETDATE())
RAISERROR ('The Credit Card you have entered has expired.' ,10,1)
ELSE
DECLARE #CreditCardID INT
INSERT INTO CreditCard (CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate)
Select CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate FROM inserted
WHERE CreditCardID = #CreditCardID
END
First of all, your ELSE condition only apply to the next statement which is DECLARE #CreditCardID INT. You should use BEGIN/END around your entire ELSE block.
Second, the logic in this trigger will fail if you decide to update multiple rows since it is only considering one row updates. This may happen if you need to import customer data from another source.
Third. You should handle UPDATES and INSERTS separately. I would probably consider creating 2 triggers, one for update, another for insert, just to avoid costly determination of what happened and a lot of if and else logic.

SQL Server Primary Key Using Date Range

I'm writing an app that allows people to send SMS messages, and if we recognize the word they sent, we do something. The keywords we recognize (handle) change based on the date. For example, the church I work for always needs a lot of Easter volunteers, so for the two months before Easter, we want to specify 'easter' as a keyword, and after Easter, we want to disable it.
So the primary key needs to be <keyword, date-range>. I could set that up as <keyword, date-start, date-end>, but I want there to be a PK conflict (or at least some sort of constraint conflict) if I try to insert a new record with the same keyword and a date-start or date-end between another row's date-start and date-end.
What's my best course of action? Does SQL Server have this capability built in? Do I need to create some sort of custom type with .NET? Do I do this with a Primary Key or a secondary check constraint?
I always like optimizing for speed, but in truth, this app doesn't really need it.
Unless I'm missing something important I believe you can accomplish this using acheckconstraint paired with a function:
-- test table
CREATE TABLE keyword_ranges (Keyword varchar(10), Startdate date, Enddate date);
-- function that checks if ranges overlap
CREATE FUNCTION CheckKeywordRange
(#Keyword VARCHAR(10), #Startdate date, #Enddate date)
RETURNS int
AS
BEGIN
DECLARE #retval int
SELECT #retval = COUNT(*)
FROM keyword_ranges
WHERE keyword = #Keyword
AND
(#Startdate <= Enddate) and (#Enddate >= Startdate)
RETURN #retval
END;
GO
-- constraint that calls the function
ALTER TABLE keyword_ranges
ADD CONSTRAINT chkKeywordRange
CHECK (dbo.CheckKeywordRange(Keyword, Startdate, Enddate) = 1);
-- this insert will succeed
INSERT keyword_ranges VALUES ('Holiday', '2014-01-01', '2014-01-05')
-- this insert will conflict with the check and fail
INSERT keyword_ranges VALUES ('Holiday', '2014-01-03', '2014-01-07')
Sample SQL Fiddle
You'll probably need to enforce this at the application level, instead of the Sql Server level. Sql Server can enforce ranges, but only if each range evaluated to fixed canonical start times and lengths: (ie: always have 2 month ranges that always start on the first day of even months). If you want arbitrary start times or lengths, you're stuck doing it with application logic, or at best in a trigger.

Resources