Is there a way to enforce constraint checking in MSSQL only when inserting new rows? I.e. allow the constraints to be violated when removing/updating rows?
Update: I mean FK constraint.
You could create an INSERT TRIGGER that checks that the conditions are met. That way all updates will go straight through.
CREATE TRIGGER employee_insupd
ON employee
FOR INSERT
AS
/* Get the range of level for this job type from the jobs table. */
DECLARE #min_lvl tinyint,
#max_lvl tinyint,
#emp_lvl tinyint,
#job_id smallint
SELECT #min_lvl = min_lvl,
#max_lvl = max_lvl,
#emp_lvl = i.job_lvl,
#job_id = i.job_id
FROM employee e INNER JOIN inserted i ON e.emp_id = i.emp_id
JOIN jobs j ON j.job_id = i.job_id
IF (#job_id = 1) and (#emp_lvl <> 10)
BEGIN
RAISERROR ('Job id 1 expects the default level of 10.', 16, 1)
ROLLBACK TRANSACTION
END
ELSE
IF NOT (#emp_lvl BETWEEN #min_lvl AND #max_lvl)
BEGIN
RAISERROR ('The level for job_id:%d should be between %d and %d.',
16, 1, #job_id, #min_lvl, #max_lvl)
ROLLBACK TRANSACTION
END
I think your best bet is to remove the explicit constraint and add a cursor for inserts, so you can perform your checking there and raise an error if the constraint is violated.
What sort of constraints? I'm guessing foreign key constraints, since you imply that deleting a row might violate the constraint. If that's the case, it seems like you don't really need a constraint per se, since you're not concerned with referential integrity.
Without knowing more about your specific situation, I would echo the intent of the other posters, which seems to be "enforce the insert requirements in your data access layer". However, I'd quibble with their implementations. A trigger seems like overkill and any competent DBA should sternly rap you on the knuckles with a wooden ruler for trying to use a cursor to perform a simple insert. A stored procedure should suffice.
Related
Is there a way to enforce referential integrity without foreign keys? Is there a way to achieve what I am trying to do below with alter table statement?
ALTER TABLE no.Man
WITH CHECK ADD CONSTRAINT chk_Son_Weight CHECK
(Son_Weight IN (Select distinct Weight from no.Man))
GO
I got the below error by using the code above
Subqueries are not allowed in this context. Only scalar expressions are allowed.
I'm not sure I understand why you think this is better than foreign keys, but yes, you can implement referential integrity in other (inferior) ways. These will be slower than doing it right and fixing the design.
Check constraint + UDF
CREATE FUNCTION dbo.IsItAValidWeight(#Son_Weight int)
RETURNS bit
WITH SCHEMABINDING
AS
BEGIN
RETURN
(
SELECT CASE WHEN EXISTS
(
SELECT 1
FROM no.Man WHERE Weight = #Son_Weight
) THEN 1 ELSE 0 END
);
END
GO
ALTER TABLE no.Man WITH CHECK
ADD CONSTRAINT chk_Son_Weight
CHECK dbo.IsItAValidWeight(Son_Weight) = 1;
Trigger
Need to know a lot more about the schema I think, but you can research.
Before each Update/Insert statement, should I :
IF...EXIST to test the primary key
Just let a transaction fail if primary key is already there (and rely on ##rowcount if I
have some logic related to primary key already being there)
TRY ... CATCH an error (raised by the Update/Insert statement itself or have a trigger test primary key and raise errors)
Other solutions ?
How do you write with primary key constraint ?
My preferred method for single-row upsert is:
BEGIN TRANSACTION;
UPDATE dbo.t WITH (HOLDLOCK, SERIALIZABLE)
SET ...
WHERE [key] = #key;
IF ##ROWCOUNT = 0
BEGIN
INSERT dbo.t ...
END
COMMIT TRANSACTION;
If you believe you will much more often be performing an insert, you can swap the logic around so you try that first:
BEGIN TRANSACTION;
INSERT dbo.t ...
SELECT #key, ...
WHERE NOT EXISTS
(
SELECT 1 FROM dbo.t WITH (UPDLOCK, SERIALIZABLE)
WHERE [key] = #key
);
IF ##ROWCOUNT = 0
BEGIN
UPDATE dbo.t SET val = #val WHERE [key] = #key;
END
COMMIT TRANSACTION;
Some background:
Please stop using this UPSERT anti-pattern
Checking for potential constraint violations before entering TRY/CATCH
So, you want to use MERGE, eh?
What you describe is often called an "UPSERT" (in case you need a Google term for further research).
We use MERGE statements, since they allow us to specify both actions in one statement.
However, the syntax is a bit complex and there are some gotchas (don't forget to use HOLDLOCK, etc.), so we have abstracted away the actual SQL generation into an InsertOrUpdate(table, fieldsAndValuesToUpdate, keyFieldsAndValues) helper method in our source code. This also allows us to change the implementation later, if required.
When writing SQL code manually, I use IF...EXISTS (inside a transaction and also with HOLDLOCK), since it's easier to read and easier to write.
That depends on the situation.
Suppose you would write an insert with a where not exists clause and when ##rowcount = 0 you would do an update because this row already seems to exist.
If this is the most performant way to do it, that depends on your data.
if you would know that for example in 80% of the cases the insert would succeed, then this approach would actually perform very good.
If it seems that most of the times an update is needed, then you could turn the code around, do the update and then check the ##rowcount.
This only works off course if you can determine before you start if you will have mostly updates or mostly inserts.
The advantage of this method (certainly when you do update first) is that you do not need to check each row first with an if...exists first, you just do you insert/update and find out after if it worked or not. And because you know before that the insert or update will succeed most of the times, you gain performance
My objective is to throw an exception back to the caller but continue execution of the SQL Server stored procedure. So, in essence, what I'm trying to accomplish is a try..catch..finally block, even though SQL Server has no concept of a try..catch..finally block, to my knowledge.
I have a sample stored procedure to illustrate. It's just an example I came up with off the top of my head, so please don't pay too much attention to the table schema. Hopefully, you understand the gist of what I'm trying to carry out here. Anyway, the stored proc contains an explicit transaction that throws an exception within the catch block. There's further execution past the try..catch block but it's never executed, if THROW is executed. From what I understand, at least in SQL Server, THROW cannot distinguish between inner and outer transactions or nested transactions.
In this stored procedure, I have two tables: Tbl1 and Tbl2. Tbl1 has a primary key on Tbl1.ID. Tbl2 has a foreign key on EmpFK that maps to Tbl1.ID. EmpID has a unique constraint. No duplicate records can be inserted into Tbl1. Both Tbl1 and Tbl2 have primary key on ID and employ identity increment for auto-insertion. The stored proc has three input parameters, one of which is employeeID.
Within the inner transaction, a record is inserted in Tbl1 -- a new employee ID is added. If it fails, the idea is the transaction should gracefully error out but the stored proc should still continue running until completion. Whether table insert succeeds or fails, EmpID will be employed later to fill in EmpFk.
After the try..catch block, I perform a lookup of Tbl1.ID, via the employeeID parameter that's passed into the stored proc. Then, I insert a record into TBl2; Tbl1.ID is the value for Tbl2.EmpFK.
(And you might be asking "why use such a schema? Why not combine into one table with such a small dataset?" Again, this is just an example. It doesn't have to be employees. You can pick anything. It's just a widget. Imagine Tbl1 may contain a very, very large data set. What's set in stone is there are two tables which have a primary key / foreign key relationship.)
Here's the sample data set:
Tbl1
ID EmpID
1 AAA123
2 AAB123
3 AAC123
Tbl2
ID Role Location EmpFK
1 Junior NW 1
2 Senior NW 2
3 Manager NE 2
4 Sr Manager SE 3
5 Director SW 3
Here's the sample stored procedure:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[usp_TestProc]
#employeeID VARCHAR(10)
,#role VARCHAR(50)
,#location VARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #employeeFK INT;
BEGIN TRY
BEGIN TRANSACTION MYTRAN;
INSERT [Tbl1] (
[EmpID]
)
VALUES (
#employeeID
);
COMMIT TRANSACTION MYTRAN;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION MYTRAN;
END;
THROW; -- Raises exception, exiting stored procedure
END CATCH;
SELECT
#employeeFK = [ID]
FROM
[Tbl1]
WHERE
[EmpID] = #employeeID;
INSERT [Tbl2] (
[Role]
,[Location]
,[EmpFK]
)
VALUES (
#role
,#location
,#employeeFK
);
END;
So, again, I still want to return the error to the caller to, i.e. log the error, but I don't wish for it to stop stored procedure execution cold in its tracks. It should continue on very similarly to a try..catch..finally block. Can this be accomplished with THROW or I must use alternative means?
Maybe I'm mistaken but isn't THROW an upgraded version of RAISERROR and, going forward, we should employ the former for handling exceptions?
I've used RAISERROR in the past for these situations and it's suited me well. But THROW is a more simpler, elegant solution, imo, and may be better practice going forward. I'm not quite sure.
Thank you for your help in advance.
What's set in stone is there are two tables which have a primary key /
foreign key relationship.
Using THROW in an inner transaction is not the way to do what you want. Judging from your code, you want to insert a new employee, unless that employee already exists, and then, regardless of whether the employee already existed or not, you want to use that employee's PK/id in a second insert into a child table.
One way to do this is to split the logic. This is psuedocode for what I mean:
IF NOT EXISTS(Select employee with #employeeId)
INSERT the new employee
SELECT #employeeFK like you are doing.
INSERT into Table2 like you are doing.
If you still need to raise an error when an #employeeId that already exists is passed, you can put an ELSE after the IF, and populate a string variable, and at the end of the proc, if the variable was populated, then throw/raise an error.
We need to change the data type of about 10 primary keys in our db from numeric(19,0) to bigint. On the smaller tables a simple update of the datatype works just fine but on the larger tables (60-70 million rows) it takes a considerable amount of time.
What is the fastest way to achieve this, preferably without locking the database.
I've written a script that generates the following (which I believe I got from a different SO post)
--Add a new temporary column to store the changed value.
ALTER TABLE query_log ADD id_bigint bigint NULL;
GO
CREATE NONCLUSTERED INDEX IX_query_log_id_bigint ON query_log (id_bigint)
INCLUDE (id); -- the include only works on SQL 2008 and up
-- This index may help or hurt performance, I'm not sure... :)
GO
declare #count int
declare #iteration int
declare #progress int
set #iteration = 0
set #progress = 0
select #count = COUNT(*) from query_log
RAISERROR ('Processing %d records', 0, 1, #count) WITH NOWAIT
-- Update the table in batches of 10000 at a time
WHILE 1 = 1 BEGIN
UPDATE X -- Updating a derived table only works on SQL 2005 and up
SET X.id_bigint = id
FROM (
SELECT TOP 10000 * FROM query_log WHERE id_bigint IS NULL
) X;
IF ##RowCount = 0 BREAK;
set #iteration = #iteration + 1
set #progress = #iteration * 10000
RAISERROR ('processed %d of %d records', 0, 1, #progress, #count) WITH NOWAIT
END;
GO
--kill the pkey on the old column
ALTER TABLE query_log
DROP CONSTRAINT PK__query_log__53833672
GO
BEGIN TRAN; -- now do as *little* work as possible in this blocking transaction
UPDATE T -- catch any updates that happened after we touched the row
SET T.id_bigint = T.id
FROM query_log T WITH (TABLOCKX, HOLDLOCK)
WHERE T.id_bigint <> T.id;
-- The lock hints ensure everyone is blocked until we do the switcheroo
EXEC sp_rename 'query_log.id', 'id_numeric';
EXEC sp_rename 'query_log.id_bigint', 'id';
COMMIT TRAN;
GO
DROP INDEX IX_query_log_id_bigint ON query_log;
GO
ALTER TABLE query_log ALTER COLUMN id bigint NOT NULL;
GO
/*
ALTER TABLE query_log DROP COLUMN id_numeric;
GO
*/
ALTER TABLE query_log
ADD CONSTRAINT PK_query_log PRIMARY KEY (id)
GO
This works very well for the smaller tables but is extremely slow going for the very large tables.
Note this is in preparation for a migration to Postgres and the EnterpriseDB Migration toolkit doesn't seem to understand the numeric(19,0) datatype
If is not possible to change a primary key without locking. The fastest way with the least impact is to create a new table with the new columns and primary keys without foreign keys and indexes. Then batch insert blocks of data in sequential order relative to their primary key(s). When that is finished, add your indexes, then foreign keys back. Finally, drop or rename the old table and rename your new table to the systems expected table name.
In practice your approach will have to vary based on how many records are inserted, updated, and/or deleted. If you're only inserting then you can perform the initial load, and top of the table just before your swap.
This approach should provide the fastest migration, minimal logs, and very little fragmentation on your table and indexes.
You have to remember that every time you modify a record, the data is being modified, indexes are being modified, and foreign keys are being checked. All within one implicit or explicit transaction. The table and/or row(s) will be locked while all changes are made. Even if your database is set to simple logging, the server will still write all changes to the log files. Updates actually are a delete paired with an insert so it is not possible to prevent fragmentation during any other process.
For a trigger that is tracking UPDATEs to a table, two temp tables may be referenced: deleted and inserted. Is there a way to cross-reference the two w/o using an INNER JOIN on their primary key?
I am trying to maintain referential integrity without foreign keys (don't ask), so I'm using triggers. I want UPDATEs to the primary key in table A to be reflected in the "foreign key" of look-up table B, and for this to happen when an UPDATE affects multiple records in table A.
All UPDATE trigger examples that I've seen hinge on joining the inserted and deleted tables to track changes; and they use the updated table's ID field (primary key) to set the join. But if that ID field (GUID) is the changed field in a record (or set of records), is there a good way to track those changes, so that I can enforce those changes in the corresponding look-up table?
I've just had this issue (or rather, a similar one), myself, hence the resurrection...
My eventual approach was to simply disallow updates to the PK field precisely because it would break the trigger. Thankfully, I had no business case to support updating the primary key column (these were surrogate IDs, anyway), so I could get away with it.
SQL Server offers the UPDATE function, for use within triggers, to check for this edge case:
CREATE TRIGGER your_trigger
ON your_table
INSTEAD OF UPDATE
AS BEGIN
IF UPDATE(pk1) BEGIN
ROLLBACK
DECLARE #proc SYSNAME, #table SYSNAME
SELECT TOP 1
#proc = OBJECT_NAME(##PROCID)
,#table = OBJECT_NAME(parent_id)
FROM sys.triggers
WHERE object_id = ##PROCID
RAISERROR ('Trigger %s prevents UPDATE of table %s due to locked primary key', 16, -1, #proc, #table) WITH NOWAIT
END
ELSE UPDATE t SET
col1 = i.col1
,col2 = i.col2
,col3 = i.col3
FROM your_table t
INNER JOIN inserted i ON t.pk1 = i.pk1
END
GO
(Note that the above is untested, and probably contains all manner of issues with regards to XACT_STATE or TRIGGER_NESTLEVEL -- it's just there to demonstrate the principle)
It gets a bit messy, though, so I would definitely consider code generation for this, to handle changes to the table during development (maybe even done by a DDL trigger on CREATE/ALTER table).
If you have a composite primary key, you can use IF UPDATE(pk1) OR UPDATE(pk2)... or do some bitwise work with the COLUMNS_UPDATED function, which will give you a bitmask based on the column ordinal (but I'm not going to cover that here -- see MSDN/BOL).
The other (simpler) option is to DENY UPDATE ON your_table(pk) TO public, but remember that any member of sysadmins (and probably dbo) will not honour this.
I'm with #Aaron, without a primary key you're stuck. If you have DDL privileges to add a trigger can't you add a auto increment PK column while you're at it? If you'd like, it doesn't even need to be the PK.