Simple CHECK Constraint not so simple - sql-server

2nd Edit: The source code for the involved function is as follows:
ALTER FUNCTION [Fileserver].[fn_CheckSingleFileSource] ( #fileId INT )
RETURNS INT
AS
BEGIN
-- Declare the return variable here
DECLARE #sourceCount INT ;
-- Add the T-SQL statements to compute the return value here
SELECT #sourceCount = COUNT(*)
FROM Fileserver.FileUri
WHERE FileId = #fileId
AND FileUriTypeId = Fileserver.fn_Const_SourceFileUriTypeId() ;
-- Return the result of the function
RETURN #sourceCount ;
END
Edit: The example table is a simplification. I need this to work as a Scaler Function / CHECK CONSTRAINT operation. The real-world arrangement is not so simple.
Original Question: Assume the following table named FileUri
FileUriId, FileId, FileTypeId
I need to write a check constraint such that FileId are unique for a FileTypeId of 1. You could insert the same FileId as much as you want, but only a single row where FileTypeId is 1.
The approach that DIDN'T work:
1) dbo.fn_CheckFileTypeId returns INT with following logic: SELECT Count(FileId) FROM FileUri WHERE FileTypeId = 1
2) ALTER TABLE FileUri ADD CONSTRAINT CK_FileUri_FileTypeId CHECK dbo.fn_CheckFileTypeId(FileId) <= 1
When I insert FileId 1, FileTypeId 1 twice, the second insert is allowed.
Thanks SO!

You need to create a filtered unique index (SQL Server 2008)
CREATE UNIQUE NONCLUSTERED INDEX ix ON YourTable(FileId) WHERE FileTypeId=1
or simulate this with an indexed view (2000 and 2005)
CREATE VIEW dbo.UniqueConstraintView
WITH SCHEMABINDING
AS
SELECT FileId
FROM dbo.YourTable
WHERE FileTypeId = 1
GO
CREATE UNIQUE CLUSTERED INDEX ix ON dbo.UniqueConstraintView(FileId)

Why don't you make FieldTypeID and Field both the primary key of the table?
Or at least a Unique Index on the table. That should solve your problem.

Related

T-SQL crazy function result

I have this function:
CREATE FUNCTION CheckAkvNames (#Name VARCHAR(20))
RETURNS INT
AS
BEGIN
DECLARE #NoTexist int = 1
SELECT
#NoTexist = CASE WHEN COUNT(*) > 0 THEN 0 ELSE 1 END
FROM
[dbo].[Names]
WHERE
[Name] = #Name
RETURN #NoTexist
END
GO
ALTER TABLE [dbo].[Names]
ADD CONSTRAINT chkNames CHECK(dbo.CheckAkvNames([Name]) = 1);
GO
The problem is, when I run this on empty table I can't insert ...
So this change works:
CASE WHEN (COUNT(*) - 1) > 0 THEN 0 ELSE 1 END
WHY? Any ideas?
Edit:
Aim is to insert only names that are not in the table. I know it would be better to use key, point of the question is not to find better solution but why this solution does not work.
The constraint you added to the table actually means that you can't insert any name in the table, because for any value inserted in the table the function should return 1.This is impossible because if the name was inserted then the constraint would be violated.
This is why count(*) - 1 works: if there is already a name inserted and you tried to insert the same name then the constraint would be violated.
If you want unique names in a table, do not use a check constraint, use a unique constraint (or equivalently a unique index):
ALTER TABLE [dbo].[Names]
ADD CONSTRAINT unq_names_name UNIQUE (Name);

How can I use a trigger to allow an incremented, user-assigned ID?

I am moving a small database from MS Access into SQL Server. Each year, the users would create a new Access database and have clean data, but this change will put data across the years into one pot. The users have relied on the autonumber value in Access as a reference for records. That is very inaccurate if, say, 238 records are removed.
So I am trying to accommodate them with an id column they can control (somewhat). They will not see the real primary key in the SQL table, but I want to give them an ID they can edit, but still be unique.
I've been working with this trigger, but it has taken much longer than I expected.
Everything SEEMS TO work fine, except I don't understand why I have the same data in my INSERTED table as the table the trigger is on. (See note in code.)
ALTER TRIGGER [dbo].[trg_tblAppData]
ON [dbo].[tblAppData]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #NewUserEnteredId int = 0;
DECLARE #RowIdForUpdate int = 0;
DECLARE #CurrentUserEnteredId int = 0;
DECLARE #LoopCount int = 0;
--*** Loop through all records to be updated because the values will be incremented.
WHILE (1 = 1)
BEGIN
SET #LoopCount = #LoopCount + 1;
IF (#LoopCount > (SELECT Count(*) FROM INSERTED))
BREAK;
SELECT TOP 1 #RowIdForUpdate = ID, #CurrentUserEnteredId = UserEnteredId FROM INSERTED WHERE ID > #RowIdForUpdate ORDER BY ID DESC;
IF (#RowIdForUpdate IS NULL)
BREAK;
-- WHY IS THERE A MATCH HERE? HAS THE RECORD ALREADY BEEN INSERTED?
IF EXISTS (SELECT UserEnteredId FROM tblAppData WHERE UserEnteredId = #CurrentUserEnteredId)
BEGIN
SET #NewUserEnteredId = (SELECT Max(t1.UserEnteredId) + 1 FROM tblAppData t1);
END
ELSE
SET #NewUserEnteredId = #CurrentUserEnteredId;
UPDATE tblAppData
SET UserEnteredId = #NewUserEnteredId
FROM tblAppData a
WHERE a.ID = #RowIdForUpdate
END
END
Here is what I want to accomplish:
When new record(s) are added, it should increment values from the Max existing
When a user overrides a value, it should check to see the existence of that value. If found restore the existing value, otherwise allow the change.
This trigger allows for multiple rows being added at a time.
It is great for this to be efficient for future use, but in reality, they will only add 1,000 records a year.
I wouldn't use a trigger to accomplish this.
Here is a script you can use to create a sequence (op didn't tag version), create the primary key, use the sequence as your special id, and put a constraint on the column.
create table dbo.test (
testid int identity(1,1) not null primary key clustered
, myid int null constraint UQ_ unique
, somevalue nvarchar(255) null
);
create sequence dbo.myid
as int
start with 1
increment by 1;
alter table dbo.test
add default next value for dbo.myid for myid;
insert into dbo.test (somevalue)
select 'this' union all
select 'that' union all
select 'and' union all
select 'this';
insert into dbo.test (myid, somevalue)
select 33, 'oops';
select *
from dbo.test
insert into dbo.test (somevalue)
select 'oh the fun';
select *
from dbo.test
--| This should error
insert into dbo.test (myid, somevalue)
select 3, 'This is NO fun';
Here is the result set:
testid myid somevalue
1 1 this
2 2 that
3 3 and
4 4 this
5 33 oops
6 5 oh the fun
And at the very end a test, which will error.

One Bowler can not bowl two consecutive over in cricket

I am working on Cricket Project. I have a table OverDetails. I want to insert data in this table.
ID OverNumber BowlerID InningsID
1 1 150 1
2 4 160 1
3 3 165 1
4 2 150 1
Row_1, Row_2 and Row_3 are legal. Row_4 is not legal, because one bowler can not through two consecutive overs in one innings. It is not necessary that overs are added consecutively in database.
I have added a constraint in SQL Server.
#Constraint_1
ALTER TABLE OverDetails ADD CONSTRAINT UniqueOverInInning
UNIQUE(OverNumber, BowlerID, IninngsID);
This constraint works perfectly.
I need a check like this:
#Constraint_2
ALTER TABLE OverDetails ADD CONSTRAINT UniqueConsecutiveBowlerInOneInning
CHECK (OverNumber + 1 != OverNumber and BowlerID + 1 != BowlerID
and IninngID + 1 != IninngID)
You need a function which returns a last BowlerID from a given InningID:
CREATE FUNCTION dbo.GetBowlerID
( #InningId INT, #OverNumber INT, #BowlerID INT)
RETURNS INT
AS
BEGIN
RETURN (SELECT top 1 CASE WHEN
(SELECT BowlerID
FROM OverDetails
WHERE InningsId = #InningId AND OverNumber = #OverNumber - 1 ) = #BowlerID
OR
(SELECT BowlerID
FROM OverDetails
WHERE InningsId = #InningId AND OverNumber = #OverNumber + 1 ) = #BowlerID
THEN 1 else 0 end)
END
Then you can put it into a check constraint:
ALTER TABLE OverDetails ADD CONSTRAINT UniqueConsecutiveBowlerInOneInning
CHECK (dbo.GetBowlerID(InningsId, OverNumber, BowlerID)=0)
Check constraints cannot directly reference other rows data. There are some techniques that try to use UDFs to get around this limitation but they tend to not work well. Especially in this case where I presume the insert of row 4 should also be blocked if it had a bowlerID of 165 since that would mean overs 2&3 shared a bowler.
Instead, we can implement this with a pair of views. I usually put DRI somewhere in the name of views like this to indicate that they're there for Declarative Referential Integrity reasons, not because I intend people to query them.
create table dbo.Bowling (
ID int not null,
OverNumber int not null,
BowlerID int not null,
InningsID int not null,
constraint PK_Bowling PRIMARY KEY (ID),
constraint UQ_Bowling_Overs UNIQUE (OverNumber,InningsID)
)
go
create view dbo.Bowling_DRI_SuccessiveOvers_Odd
with schemabinding
as
select
(OverNumber/2) as OddON,
BowlerID
from
dbo.Bowling
go
create unique clustered index UQ_Bowling_DRI_SuccessiveOvers_Odd on dbo.Bowling_DRI_SuccessiveOvers_Odd (OddON,BowlerID)
go
create view dbo.Bowling_DRI_SuccessiveOvers_Even
with schemabinding
as
select
((OverNumber+1)/2) as EvenON,
BowlerID
from
dbo.Bowling
go
create unique clustered index UQ_Bowling_DRI_SuccessiveOvers_Even on dbo.Bowling_DRI_SuccessiveOvers_Even (EvenON,BowlerID)
go
insert into dbo.Bowling(ID,OverNumber,BowlerID,InningsID) values
(1,1,150,1),
(2,4,160,1),
(3,3,165,1)
go
insert into dbo.Bowling(ID,OverNumber,BowlerID,InningsID) values
(4,2,150,1)
This final insert generates the error:
Msg 2601, Level 14, State 1, Line 37 Cannot insert duplicate key row
in object 'dbo.Bowling_DRI_SuccessiveOvers_Even' with unique index
'UQ_Bowling_DRI_SuccessiveOvers_Even'. The duplicate key value is (1,
150). The statement has been terminated.
Hopefully, you can see the trick I'm employing to make these views check your desired constraint - it's set up so that rows are paired with either their (logical, based on OrderNumber) successor or predecessor based on dividing the OrderNumber by two using integer maths.
We then apply unique constraints on these pairs and including the BowlerID. Only if the same bowler bowls two successive overs will we generate more than one row with the same (OddON/EvenON) and BowlerID values.
Maybe this one?
create function dbo.chk_fnk (#OverNumber int, #BowlerID int, #InningsID int)
returns int
as
begin
return
case when
exists (select *
from dbo.OverDetails
where BowlerID = #BowlerID and abs(OverNumber - #OverNumber) = 1 and InningsID = #InningsID)
then 1
else 0
end;
end;
go
ALTER TABLE dbo.OverDetails ADD CONSTRAINT UniqueConsecutiveBowlerInOneInning
CHECK (dbo.chk_fnk(OverNumber, BowlerID, InningsID) = 0);

Ensure foreign key of a foreign key matches a base foreign key

Basically let's say I have a "Business" that owns postal codes that it services. Let's also suppose I have another relational table that sets up fees.
CREATE TABLE [dbo].[BusinessPostalCodes]
(
[BusinessPostalCodeId] INT IDENTITY (1, 1) NOT NULL,
[BusinessId] INT NOT NULL,
[PostalCode] VARCHAR (10) NOT NULL
)
CREATE TABLE [dbo].[BusinessPostalCodeFees]
(
[BusinessId] INT NOT NULL,
[BusinessProfileFeeTypeId] INT NOT NULL,
[BusinessPostalCodeId] INT NOT NULL,
[Fee] SMALLMONEY NULL
)
I want to know if it's possible to set up a foreign key (or something) on BusinessPostalCodeFees that ensures that the related BusinessId of BusinessPostalCodes is the same as the BusinessId of BusinessPostalCodeFees.
I realize that I can remove BusinessId entirely, but I would much rather keep this column and have a way of guaranteeing they will be the same. Is there anything I can do?
It sounds like (and correct me if I'm wrong) that you're trying to make sure that any entry into BusinessPostalCodeFees' BusinessId and BusinessPostalCodeId columns match an entry in the BusinessPostalCodes table. If that's the case, then yes, you can definitely have a foreign key that references a compound primary key.
However, if you need to keep the BusinessId, I'd recommend normalizing your tables a step further than you have. You'll end up with duplicate data as-is.
On a side note, I would recommend you don't use the money data types in SQL: See here.
In the end, Jeffrey's solution didn't quite work for my particular situation. Both columns in the relation have to be unique (like a composite key). Turns out the answer here (for me) is a Checked Constraint.
Create a function that you want to have the constraint pass or fail:
CREATE FUNCTION [dbo].[MatchingBusinessIdPostalCodeAndProfileFeeType]
(
#BusinessId int,
#BusinessPostalCodeId int,
#BusinessProfileFeeTypeId int
)
RETURNS BIT
AS
BEGIN
-- This works because BusinessPostalCodeId is a unique Id.
-- If businessId doesn't match, its filtered out.
DECLARE #pcCount AS INT
SET #pcCount = (SELECT COUNT(*)
FROM BusinessPostalCodes
WHERE BusinessPostalCodeId = #BusinessPostalCodeId AND
BusinessId = #BusinessId)
-- This works because BusinessProfileFeeTypeId is a unique Id.
-- If businessId doesn't match, its filtered out.
DECLARE #ftCount AS INT
SET #ftCount = (SELECT COUNT(*)
FROM BusinessProfileFeeTypes
WHERE BusinessProfileFeeTypeId = #BusinessProfileFeeTypeId AND
BusinessId = #BusinessId)
-- Both should have only one record
BEGIN IF (#pcCount = 1 AND #ftCount = 1)
RETURN 1
END
RETURN 0
END
Then just add it to your table:
CONSTRAINT [CK_BusinessPostalCodeFees_MatchingBusinessIdPostalCodeAndProfileFeeType]
CHECK (dbo.MatchingBusinessIdPostalCodeAndProfileFeeType(
BusinessId,
BusinessPostalCodeId,
BusinessProfileFeeTypeId) = 1)

Sql Server string interning

We have a table where we store all the exceptions (message, stackTrace, etc..), the table is getting big and we would like to reduce it.
There are plenty of repeated StackTraces, Messages, etc, but enabling compression produces a modest size reduction (10%) while I think much bigger benefits could come if somehow Sql Server will intern the strings in some per-column hash-table.
I could get some of the benefits if I normalize the table and extract StackTraces to another one, but exception messages, exception types, etc.. are also repeated.
Is there a way to enable string interning for some column in Sql Server?
There is no built-in way to do this. You could easily do something like:
SELECT MessageID = IDENTITY(INT, 1, 1), Message
INTO dbo.Messages
FROM dbo.HugeTable GROUP BY Message;
ALTER TABLE dbo.HugeTable ADD MessageID INT;
UPDATE h
SET h.MessageID = m.MessageID
FROM dbo.HugeTable AS h
INNER JOIN dbo.Messages AS m
ON h.Message = m.Message;
ALTER TABLE dbo.HugeTable DROP COLUMN Message;
Now you'll need to do a few things:
Change your logging procedure to perform an upsert to the Messages table
Add proper indexes to the messages table (wasn't sure of Message data type) and PK
Add FK to MessageID column
Rebuild indexes on HugeTable to reclaim space
Do this in a test environment first!
Aaron's posting answers the questions of adding interning to a table, but afterwards you will need to modify your application code and stored-procedures to work with the new schema.
...or so you might think. You can actually create a VIEW that returns data matching the old schema, and you can also support INSERT operations on the view too, which are translated into child operations on the Messages and HugeTable tables. For readability I'll use the names InternedStrings and ExceptionLogs for the tables.
So if the old table was this:
CREATE TABLE ExceptionLogs (
LogId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Message nvarchar(1024) NOT NULL,
ExceptionType nvarchar(512) NOT NULL,
StackTrace nvarchar(4096) NOT NULL
)
And the new tables are:
CREATE TABLE InternedStrings (
StringId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Value nvarchar(max) NOT NULL
)
CREATE TABLE ExceptionLogs2 ( -- note the new name
LogId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
Message int NOT NULL,
ExceptionType int NOT NULL,
StackTrace int NOT NULL
)
Add an index to InternedStrings to make the value lookups faster:
CREATE UNIQUE NONCLUSTERED INDEX IX_U_InternedStrings_Value ON InternedStrings ( Value ASC )
Then you would also have a VIEW:
CREATE VIEW ExeptionLogs AS
SELECT
LogId,
MessageStrings .Value AS Message,
ExceptionTypeStrings.Value AS ExceptionType,
StackTraceStrings .Value AS StackTrace
FROM
ExceptionLogs2
INNER JOIN InternedStrings AS MessageStrings ON
MessageStrings.StringId = ExceptionLogs2.Message
INNER JOIN InternedStrings AS ExceptionTypeStrings ON
ExceptionTypeStrings.StringId = ExceptionLogs2.ExceptionType
INNER JOIN InternedStrings AS StackTraceStrings ON
StackTraceStrings.StringId = ExceptionLogs2.StackTrace
And to handle INSERT operations from unmodified clients:
CREATE TRIGGER ExceptionLogsInsertHandler
ON ExceptionLogs INSTEAD OF INSERT AS
DECLARE #messageId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.Message
IF #messageId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.Message )
SET #messageId = SCOPE_IDENTITY()
END
DECLARE #exceptionTypeId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.ExceptionType
IF #exceptionTypeId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.ExceptionType )
SET #exceptionTypeId = SCOPE_IDENTITY()
END
DECLARE #stackTraceId int = SELECT StringId FROM InternedStrings WHERE Value = inserted.StackTrace
IF #stackTraceId IS NULL
BEGIN
INSERT INTO InternedStrings ( Text ) VALUES ( inserted.StackTrace )
SET #stackTraceId = SCOPE_IDENTITY()
END
INSERT INTO ExceptionLogs2 ( Message, ExceptionType, StackTrace )
VALUES ( #messageId, #exceptionTypeId, #stackTraceId )
Note this TRIGGER can be improved: it only supports single-row insertions, and is not entirely concurrency-safe, though because previous data won't be mutated it means that there's a slight risk of data duplication in the InternedStrings table - and because of a UNIQUE index the insert will fail. There are different possible ways to handle this, such as using a TRANSACTION and changing the queries to use holdlock and updlock.

Resources