SQL Server update trigger with no primary key - sql-server

I have a table with no primary key. This is beyond my control and I can't change it.
I need to add a trigger that updates one column on an update. Is there a way with no primary key?
If need to do:
update myTable
set someField = someValue
where myTable.pkID = inserted.pdID
However, I don't have a primary key so I don't know how to do the where clause.

You don't have to have a formal primary key per se, but you need some combination of values to identify the record(s) affected-- even if that is every column in the table:
UPDATE myTable
SET someField = someValue
FROM myTable INNER JOIN inserted ON
myTable.[col1] = inserted.[col1]
AND myTable.[col2] = inserted.[col2]
AND myTable.[col3] = inserted.[col3]
--etc
If you don't have some combination of values that is unique, I'm afraid you'd be out of luck with this approach.

Related

Duplicates not getting ignored in SQL Server

I have a temp table that has two rows.
Their Id is 999359143, 999365081
I have a table that doesn't have a primary key but has a unique index based off of the id and date.
This 999359143 already exists in the table. So when I use my query it still is trying to insert the row from the temp table into the normal table and it errors. This is the query below
INSERT INTO [XferTable]
([DataDate]
,[LoanNum]
)
SELECT Distinct t1.[DataDate]
,t1.[LoanNum]
FROM #AllXfers t1 WITH(HOLDLOCK)
WHERE NOT EXISTS(SELECT t2.LoanNum, t2.DataDate
FROM XferTable t2 WITH(HOLDLOCK)
WHERE t2.LoanNum = t1.LoanNum AND t2.DataDate = t1.DataDate
)
Is there a better way to do this?
You should use the MERGE statement, which acts atomically so you shouldn't need to do your own locking (also, isolation query hints on temporary tables doesn't achieve anything).
MERGE XferTable AS SOURCE
USING #AllXfers AS TARGET
ON
SOURCE.[DataDate] = TARGET.[DataDate]
AND SOURCE.[LoanNum] = TARGET.[LoanNum]
WHEN NOT MATCHED BY TARGET--record in SOURCE but not in TARGET
THEN INSERT
(
[DataDate]
,[LoanNum]
)
VALUES
(
SOURCE.[DataDate]
,TARGET.[LoanNum]
);
Your primary key violation is probably because you are using (Date, Loan#) as the uniqueness criteria and your primary key is probably only on Loan#.

SQL Server check constraints - only one particular value per group [duplicate]

How could I set a constraint on a table so that only one of the records has its isDefault bit field set to 1?
The constraint is not table scope, but one default per set of rows, specified by a FormID.
Use a unique filtered index
On SQL Server 2008 or higher you can simply use a unique filtered index
CREATE UNIQUE INDEX IX_TableName_FormID_isDefault
ON TableName(FormID)
WHERE isDefault = 1
Where the table is
CREATE TABLE TableName(
FormID INT NOT NULL,
isDefault BIT NOT NULL
)
For example if you try to insert many rows with the same FormID and isDefault set to 1 you will have this error:
Cannot insert duplicate key row in object 'dbo.TableName' with unique
index 'IX_TableName_FormID_isDefault'. The duplicate key value is (1).
Source: http://technet.microsoft.com/en-us/library/cc280372.aspx
Here's a modification of Damien_The_Unbeliever's solution that allows one default per FormID.
CREATE VIEW form_defaults
AS
SELECT FormID
FROM whatever
WHERE isDefault = 1
GO
CREATE UNIQUE CLUSTERED INDEX ix_form_defaults on form_defaults (FormID)
GO
But the serious relational folks will tell you this information should just be in another table.
CREATE TABLE form
FormID int NOT NULL PRIMARY KEY
DefaultWhateverID int FOREIGN KEY REFERENCES Whatever(ID)
From a normalization perspective, this would be an inefficient way of storing a single fact.
I would opt to hold this information at a higher level, by storing (in a different table) a foreign key to the identifier of the row which is considered to be the default.
CREATE TABLE [dbo].[Foo](
[Id] [int] NOT NULL,
CONSTRAINT [PK_Foo] PRIMARY KEY CLUSTERED
(
[Id] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[DefaultSettings](
[DefaultFoo] [int] NULL
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[DefaultSettings] WITH CHECK ADD CONSTRAINT [FK_DefaultSettings_Foo] FOREIGN KEY([DefaultFoo])
REFERENCES [dbo].[Foo] ([Id])
GO
ALTER TABLE [dbo].[DefaultSettings] CHECK CONSTRAINT [FK_DefaultSettings_Foo]
GO
You could use an insert/update trigger.
Within the trigger after an insert or update, if the count of rows with isDefault = 1 is more than 1, then rollback the transaction.
CREATE VIEW vOnlyOneDefault
AS
SELECT 1 as Lock
FROM <underlying table>
WHERE Default = 1
GO
CREATE UNIQUE CLUSTERED INDEX IX_vOnlyOneDefault on vOnlyOneDefault (Lock)
GO
You'll need to have the right ANSI settings turned on for this.
I don't know about SQLServer.But if it supports Function-Based Indexes like in Oracle, I hope this can be translated, if not, sorry.
You can do an index like this on suposed that default value is 1234, the column is DEFAULT_COLUMN and ID_COLUMN is the primary key:
CREATE
UNIQUE
INDEX only_one_default
ON my_table
( DECODE(DEFAULT_COLUMN, 1234, -1, ID_COLUMN) )
This DDL creates an unique index indexing -1 if the value of DEFAULT_COLUMN is 1234 and ID_COLUMN in any other case. Then, if two columns have DEFAULT_COLUMN value, it raises an exception.
The question implies to me that you have a primary table that has some child records and one of those child records will be the default record. Using address and a separate default table here is an example of how to make that happen using third normal form. Of course I don't know if it's valuable to answer something that is so old but it struck my fancy.
--drop table dev.defaultAddress;
--drop table dev.addresses;
--drop table dev.people;
CREATE TABLE [dev].[people](
[Id] [int] identity primary key,
name char(20)
)
GO
CREATE TABLE [dev].[Addresses](
id int identity primary key,
peopleId int foreign key references dev.people(id),
address varchar(100)
) ON [PRIMARY]
GO
CREATE TABLE [dev].[defaultAddress](
id int identity primary key,
peopleId int foreign key references dev.people(id),
addressesId int foreign key references dev.addresses(id))
go
create unique index defaultAddress on dev.defaultAddress (peopleId)
go
create unique index idx_addr_id_person on dev.addresses(peopleid,id);
go
ALTER TABLE dev.defaultAddress
ADD CONSTRAINT FK_Def_People_Address
FOREIGN KEY(peopleID, addressesID)
REFERENCES dev.Addresses(peopleId, id)
go
insert into dev.people (name)
select 'Bill' union
select 'John' union
select 'Harry'
insert into dev.Addresses (peopleid, address)
select 1, '123 someplace' union
select 1,'work place' union
select 2,'home address' union
select 3,'some address'
insert into dev.defaultaddress (peopleId, addressesid)
select 1,1 union
select 2,3
-- so two home addresses are default now
-- try adding another default address to Bill and you get an error
select * from dev.people
join dev.addresses on people.id = addresses.peopleid
left join dev.defaultAddress on defaultAddress.peopleid = people.id and defaultaddress.addressesid = addresses.id
insert into dev.defaultaddress (peopleId, addressesId)
select 1,2
GO
You could do it through an instead of trigger, or if you want it as a constraint create a constraint that references a function that checks for a row that has the default set to 1
EDIT oops, needs to be <=
Create table mytable(id1 int, defaultX bit not null default(0))
go
create Function dbo.fx_DefaultExists()
returns int as
Begin
Declare #Ret int
Set #ret = 0
Select #ret = count(1) from mytable
Where defaultX = 1
Return #ret
End
GO
Alter table mytable add
CONSTRAINT [CHK_DEFAULT_SET] CHECK
(([dbo].fx_DefaultExists()<=(1)))
GO
Insert into mytable (id1, defaultX) values (1,1)
Insert into mytable (id1, defaultX) values (2,1)
This is a fairly complex process that cannot be handled through a simple constraint.
We do this through a trigger. However before you write the trigger you need to be able to answer several things:
do we want to fail the insert if a default exists, change it to 0 instead of 1 or change the existing default to 0 and leave this one as 1?
what do we want to do if the default record is deleted and other non default records are still there? Do we make one the default, if so how do we determine which one?
You will also need to be very, very careful to make the trigger handle multiple row processing. For instance a client might decide that all of the records of a particular type should be the default. You wouldn't change a million records one at a time, so this trigger needs to be able to handle that. It also needs to handle that without looping or the use of a cursor (you really don't want the type of transaction discussed above to take hours locking up the table the whole time).
You also need a very extensive tesing scenario for this trigger before it goes live. You need to test:
adding a record with no default and it is the first record for that customer
adding a record with a default and it is the first record for that customer
adding a record with no default and it is the not the first record for that customer
adding a record with a default and it is the not the first record for that customer
Updating a record to have the default when no other record has it (assuming you don't require one record to always be set as the deafault)
Updating a record to remove the default
Deleting the record with the deafult
Deleting a record without the default
Performing a mass insert with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record inserts
Performing a mass update with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record updates
Performing a mass delete with multiple situations in the data including two records which both have isdefault set to 1 and all of the situations tested when running individual record deletes
#Andy Jones gave an answer above closest to mine, but bearing in mind the Rule of Three, I placed the logic directly in the stored proc that updates this table. This was my simple solution. If I need to update the table from elsewhere, I will move the logic to a trigger. The one default rule applies to each set of records specified by a FormID and a ConfigID:
ALTER proc [dbo].[cpForm_UpdateLinkedReport]
#reportLinkId int,
#defaultYN bit,
#linkName nvarchar(150)
as
if #defaultYN = 1
begin
declare #formId int, #configId int
select #formId = FormID, #configId = ConfigID from csReportLink where ReportLinkID = #reportLinkId
update csReportLink set DefaultYN = 0 where isnull(ConfigID, #configId) = #configId and FormID = #formId
end
update
csReportLink
set
DefaultYN = #defaultYN,
LinkName = #linkName
where
ReportLinkID = #reportLinkId

mssql table multi foreign key cascade

I'm confident that this is possible but for the life of me I can't figure it out.
What I have created is a user history MSSQL table to hold the changes made to a user and by whom. This table contains two foreign keys which reference my other table (User) - one fkey for the affected user and the other fkey for the user making the changes.
What I need is for any changes to the (User) table to cascade and update the corresponding entries in this new table.
The fields in the new table (User_History) are as follows (Each user is identified by two fields):
Affected_User_House_Id - int
Affected_User_Id - int
Modified_By_User_House_Id - int
Modified_By_User_Id – int
Modification_Date - datetime
ModificationMade - ntext
Each field is a primary key except for ‘ModificationMade’. The field ‘Modification_Date’ is accurate down to 1 second.
The problem I am having is creating said cascade.
I have tried running the following T-SQL code:
ALTER TABLE [User_History] WITH CHECK
ADD CONSTRAINT [FK_User_History_User] FOREIGN KEY([Affected_User_House_Id], [Affected_User_Id])
REFERENCES [User] ([User_House_Id], [User_ID])
ON UPDATE CASCADE
GO
ALTER TABLE [User_History] CHECK CONSTRAINT [FK_User_History_User]
GO
ALTER TABLE [User_History] WITH CHECK
ADD CONSTRAINT [FK_User_History_User_ModifiedBy] FOREIGN KEY([Modified_By_User_House_Id], [Modified_By_User_Id])
REFERENCES [User] ([User_House_Id], [User_ID])
ON UPDATE CASCADE
GO
ALTER TABLE [User_History] CHECK CONSTRAINT [FK_User_History_User_ModifiedBy]
GO
This T-SQL gave me the following error:
*'User' table saved successfully
'User_History' table
- Unable to create relationship 'FK_User_History_User_ModifiedBy'.
Introducing FOREIGN KEY constraint 'FK_User_History_User_ModifiedBy' on table 'User_History' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.*
The code works if I remove the second “ON UPDATE CASCADE” the however that will mean the values in the fields “Modified_By_User_House_Id” and “Modified_By_User_Id” will not be updated to match their referenced values in the User table.
I am at a lost as to how to acomplish this goal.
You can only specify a single cascade. Here's an attempt to simulate multiple cascades with two triggers:
create table TabA (
ID1 int not null,
ID2 int not null,
_RowID int IDENTITY(1,1) not null,
constraint PK_TabA PRIMARY KEY (ID1,ID2),
constraint UQ_TabA__RowID UNIQUE (_RowID)
)
go
create table TabB (
ID1a int not null,
ID2a int not null,
ID1b int not null,
ID2b int not null,
constraint PK_TabB PRIMARY KEY (ID1a,ID2a,ID1b,ID2b)
)
They're simpler than your tables, but hopefully close enough. We need an immutable identifier in TabA, and obviously the IDs aren't it, since the whole point is to cascade changes to them. So I've added _RowID.
It would be nice to implement at least a real foreign key and just simulate the cascade behaviour on top of that, but some simple reflection will demonstrate that there's always a point where the FK would be broken. So we simulate it:
create trigger FK_TabB_TabA on TabB
after insert,update
as
set nocount on
if exists (
select
*
from
inserted i
left join
TabA a
on
i.ID1a = a.ID1 and
i.ID2a = a.ID2
left join
TabA b
on
i.ID1b = b.ID1 and
i.ID2b = b.ID2
where
a._RowID is null or
b._RowID is null)
begin
declare #Error varchar(max)
set #Error = 'The INSERT statement conflicted with the Foreign Key constraint "FK_TabB_TabA". The conflict occurred in database "'+DB_NAME()+'", table "dbo.TabB".'
RAISERROR(#Error,16,0)
rollback
end
And then the cascading update:
create trigger FK_TabB_TabA_Cascade on TabA
after update
as
set nocount on
;with Updates as (
select
d.ID1 as OldID1,
d.ID2 as OldID2,
i.ID1 as NewID1,
i.ID2 as NewID2
from
inserted i
inner join
deleted d
on
i._RowID = d._RowID
)
update b
set
ID1a = COALESCE(u1.NewID1,ID1a),
ID2a = COALESCE(u1.NewID2,ID2a),
ID1b = COALESCE(u2.NewID1,ID1b),
ID2b = COALESCE(u2.NewID2,ID2b)
from
TabB b
left join
Updates u1
on
b.ID1a = u1.OldID1 and
b.ID2a = u1.OldID2
left join
Updates u2
on
b.ID1b = u2.OldID1 and
b.ID2b = u2.OldID2
where
u1.OldID1 is not null or
u2.OldID1 is not null
go
Some simple inserts:
insert into TabA (ID1,ID2)
values (1,1),(1,2),(2,1),(2,2)
go
insert into TabB (ID1a,ID2a,ID1b,ID2b)
values (1,1,2,2)
Then the following gets an error. Not quite like a built in FK violation, but close enough:
insert into TabB (ID1a,ID2a,ID1b,ID2b)
values (1,1,2,3)
--Msg 50000, Level 16, State 0, Procedure FK_TabB_TabA, Line 28
--The INSERT statement conflicted with the Foreign Key constraint "FK_TabB_TabA". The conflict occurred in database "Flange", table "dbo.TabB".
--Msg 3609, Level 16, State 1, Line 1
--The transaction ended in the trigger. The batch has been aborted.
This is the update that we wanted to be able to perform:
update TabA set ID2 = ID2 + 1
And we query the FK table:
select * from TabB
Result:
ID1a ID2a ID1b ID2b
----------- ----------- ----------- -----------
1 2 2 3
So the update cascaded.
Why you can't use real FKs:
You want to have cascading updates. That means that the ID values in TabA are going to change to a new value that doesn't currently exist (caveat - we're excluding situations where 2n rows swap their identity values) - otherwise, the primary key constraint will be broken by this update.
As such, we know that the new key value will not yet exist. If we were to attempt cascading updates using an INSTEAD OF trigger (to update the child table before the parent) then the new values we attempt to update to in TabB do not yet exist. Alternately, if we attempt to do cascading updates using an AFTER trigger - well, we're too late. The FK constraint has already prevented the update.
I suppose you could implement an INSTEAD OF trigger that inserts the new rows as "duplicates", updates the children, then deletes the old rows. In such a circumstance, I think you could have real FKs. But I don't want to try writing that trigger to be right in all circumstances (e.g where you have three rows being updated. Two swap their ID values and the other creates a new ID)
According to this knowledge base article, this error message occurs when "a table cannot appear more than one time in a list of all the cascading referential actions that are started by either a DELETE or an UPDATE statement."
Since you have two paths coming from the same table, a possible workaround may involve creating a new key on the parent table and creating a single foreign key on the child ([Affected_User_House_Id], [Affected_User_Id], [Modified_By_User_House_Id], [Modified_By_User_Id]). This however, will likely create a lot of overhead. As a last resort, you can use triggers to enforce the relational integrity.

Do Foreign Key constraints get checked on an SQL update statement that doesn't update the columns with the Constraint?

Do Foreign Key constraints get checked on an SQL update statement that doesn't update the columns with the Constraint? (In MS SQL Server)
Say I have a couple of tables with the following columns:
OrderItems
- OrderItemID
- OrderItemTypeID (FK to a OrderItemTypeID column on another table called OrderItemTypes)
- ItemName
If I just update
update [dbo].[OrderItems]
set [ItemName] = 'Product 3'
where [OrderItemID] = 2508
Will the FK constraint do it's lookup/check with the update statement above? (even thought the update is not change the value of that column?)
No, the foreign key is not checked. This is pretty easy to see by examining the execution plans of two different updates.
create table a (
id int primary key
)
create table b (
id int,
fkid int
)
alter table b add foreign key (fkid) references a(id)
insert into a values (1)
insert into a values (2)
insert into b values (5,1) -- Seek on table a's PK
update b set id = 6 where id = 5 -- No seek on table a's PK
update b set fkid = 2 where id = 6 -- Seek on table a's PK
drop table b
drop table a
No. Since the SQL update isn't updating a column containing a constraint, what exactly would SQL Server be checking in this case? This is similar to asking, "does an insert trigger get fired if I only do an update?" Answer is no.
There is a case when the FK not existing will prevent updates to other columns even though the FK is not changed and that is when the FK is created WITH NOCHECK and thus not checked at the time of creation. Per Books Online:
If you do not want to verify new CHECK or FOREIGN KEY constraints
against existing data, use WITH NOCHECK. We do not recommend doing
this, except in rare cases. The new constraint will be evaluated in
all later data updates. Any constraint violations that are suppressed
by WITH NOCHECK when the constraint is added may cause future updates
to fail if they update rows with data that does not comply with the
constraint.

SQL Server: change primary key with related rows

I want to change the primary key value for one row in a table that has relations with other tables:
For example
Table Person { Id, Name, +50 fields }
Table Address { Id, City, +10 fields }
Table Person2Address { Id, PersonId, AddressId }
I want to change Person.Id and Person2Address.PersonId
I try something like:
BEGIN TRANSACTION
UPDATE Pers SET Id = NewId WHERE Id = OldId
UPDATE Person2Address SET PersonId = NewId WHERE PersonId = OldId
COMMIT TRANSACTION
But of course it provides conflicts :)
How can I temporary suppress foreign key constraints or is there a better way to change Id for person?
First off, changing the primary key value is never a good idea. Your main focus should be to try and avoid that by all means.
If you cannot eliminate the need to update the primary key value, then your best bet would be to define the foreign key relationship between those two tables to be using ON UPDATE CASCADE, so that any changes to the main table's primary key will be automatically cascaded down to the child table.
To do this, drop your existing foreign key relationship and then add:
ALTER TABLE dbo.Person2Address
ADD CONSTRAINT FK_Person2Address_Person
FOREIGN KEY (PersonId) REFERENCES dbo.Person(Id)
ON UPDATE CASCADE
That should then automatically update the Person2Address table's PersonId value if the Id on the person changes.
Now you should be able to just call
UPDATE dbo.Person SET Id = NewId WHERE Id = OldId
and that should be all there is!
Your easiest bet for things like this is to use something like:
BEGIN TRANSACTION
UPDATE Pers SET tempId = NewId WHERE Id = OldId
UPDATE Person2Address SET tempPersonId = NewId WHERE PersonId = OldId
COMMIT TRANSACTION
Then drop the Id and PersonId fields and rename the temp ones.
You can drop FK constraints and recreate them when finished.
ALTER TABLE some_table DROP CONSTRAINT my_constraint
Check this article for creating and modifying constraints.

Resources