t-sql insert statement where primary key is a decimal - sql-server

2 years ago I created a table that has 22 rows. Each row is a step/page in filing an application for hire. I realized back then I would most likely be asked to insert steps as the business grew. I was right. I need to insert a new step between step 21 & 22. So I want to create a new row in that table with stepId = 21.5. But the insert statement fails.
INSERT INTO frznStep (
stepId
,myField1
,myField2
,myField3
)
VALUES (
21.5
,'xxx'
,'yyy'
,'zzz'
)
the error msg is:
Violation of PRIMARY KEY constraint 'PK_frznStep'. Cannot insert
duplicate key in object 'dbo.frznStep'. The duplicate key value is
(22).

I suspect when you script out the table, you'll see that the precision of your decimal column is 0, so something like stepId decimal(9,0)
If you have a non-zero value for the decimal precision, the following repro works
USE tempdb
DROP TABLE IF EXISTS #frznStep;
CREATE TABLE #frznStep
(
stepId decimal(9, 1) NOT NULL
, myField1 varchar(255) NOT NULL
, myField2 varchar(255) NOT NULL
, myField3 varchar(255) NOT NULL
, CONSTRAINT PK_frznStep PRIMARY KEY (stepId)
);
insert into #frznStep (stepId, myField1, myField2, myField3) values (21, 'www', 'yyy', 'zzz');
insert into #frznStep (stepId, myField1, myField2, myField3) values (22, 'yyy', 'yyy', 'zzz');
insert into #frznStep (stepId, myField1, myField2, myField3) values (21.5, 'xxx', 'yyy', 'zzz');
GO
When you use a 0 scale, you'll get 21 and 22 into the table but 21.5 will be implicitly converted to decimal(x,0) which then violates the primary key constraint.
-- Redeclare as 0 precision
DROP TABLE IF EXISTS #frznStep;
CREATE TABLE #frznStep
(
stepId decimal(9, 0) NOT NULL
, myField1 varchar(255) NOT NULL
, myField2 varchar(255) NOT NULL
, myField3 varchar(255) NOT NULL
, CONSTRAINT PK_frznStep PRIMARY KEY (stepId)
);
insert into #frznStep (stepId, myField1, myField2, myField3) values (21, 'www', 'yyy', 'zzz');
insert into #frznStep (stepId, myField1, myField2, myField3) values (22, 'yyy', 'yyy', 'zzz');
--Msg 2627, Level 14, State 1, Line 36
--Violation of PRIMARY KEY constraint 'PK_frznStep'. Cannot insert duplicate key in object 'dbo.#frznStep'. The duplicate key value is (22).
--The statement has been terminated.
insert into #frznStep (stepId, myField1, myField2, myField3) values (21.5, 'xxx', 'yyy', 'zzz');
You options are either to change your data type to include the scale (which will require dropping and recreating the primary key as the column is part of it) Or scale everything up by a factor of 10 and then you can insert into the 215 nicely between 210 and 220. (A "trick" I learned the hard way programming Apple Basic ages ago)

The first intuitive answer off the bat would be to convert your primary key to a numeric type, such as decimal.
However, is there really a reason to think of the step as 21.5? Or are you just trying to fit it between 21 and 22? I say this because the more ideal situation would be to have a primary key that simply serves as an identity. Then have a separate column that identifies the step number. That way, instead of having the step be 21.5, you'll simply have it be step 22, and then you can change step 22 to step 23.
alter table frznStep add column stepOrd int null;
update frznStep set stepOrd = stepId;
update frznStep set stepOrd = 23 where stepOrd = 22;
insert frznStep (stepId, stepOrd, ...) values (100, 22, ...);
You could also convert stepId to autoincrement. Though I believe you'll have to drop the table and recreate it in that case.

You’re getting the error because your id column is effectively integer and your attempted insert value is being rounded to integer, thus colliding with an existing key value.
Rather than using the id column as both unique identifier and step order, which is a design flaw (overloading a column), specify the steps as a chain, like a linked list, by introducing a column, perhaps called nextStepId, that stores the id of the next step to run.
This would separate the concerns of primary key being the row identifier and step order, giving control of step order independent of id values being any particular value relative to each other.

Related

Conversion failed when converting the varchar value '______' to data type int

USE MASTER
GO
CREATE DATABASE db_movies;
GO
USE db_movies;
GO
CREATE TABLE tbl_movies
(
movie_id INT PRIMARY KEY NOT NULL IDENTITY (1,1),
movie_name VARCHAR(50) NOT NULL
);
INSERT INTO tbl_movies (movie_name)
VALUES ('Jurassic Park'), ('Star Wars'), ('Blade Runner');
CREATE TABLE tbl_genre
(
genre_id INT PRIMARY KEY NOT NULL IDENTITY (100,1),
genre_name VARCHAR(50) NOT NULL
);
INSERT INTO tbl_genre (genre_name)
VALUES ('Sci-Fi'), ('Thriller'), ('Horror');
CREATE TABLE tbl_movielist
(
MovieID INT PRIMARY KEY NOT NULL IDENTITY (1000,1),
MovieName VARCHAR (50) NOT NULL,
Movie_identification INT NOT NULL
CONSTRAINT fk_movie_id
FOREIGN KEY REFERENCES tbl_movies(movie_id)
ON UPDATE CASCADE ON DELETE CASCADE,
Genre_identification INT NOT NULL
CONSTRAINT fk_genre_id
FOREIGN KEY REFERENCES tbl_genre(genre_id)
ON UPDATE CASCADE ON DELETE CASCADE,
rating FLOAT(3) NOT NULL,
);
INSERT INTO tbl_movielist (MovieName, Movie_identification, Genre_identification, rating)
VALUES ('Sandlot', 10, 109, 7.80),
('Knives Out', 11, 110, 7.90),
('The Notebook', 12, 111, 7.80);
INSERT INTO tbl_genre (genre_name)
VALUES ('Comedy'), ('Mystery'), ('Drama');
INSERT INTO tbl_movies(movie_name)
VALUES ('Sandlot'), ('Knives Out'), ('The Notebook');
SELECT * FROM tbl_genre;
SELECT * FROM tbl_movies;
SELECT * FROM tbl_movielist;
SELECT *
FROM tbl_movies
INNER JOIN tbl_genre ON CONVERT(int, tbl_genre.genre_id) = tbl_movies.movie_name;
I have created a database and for some reason on my final two lines, where I am using the INNER JOIN statement it says "Conversion failed when converting the varchar value 'Jurassic Park' to data type int. Now I have tried using the CAST function and CONVERT function, as well as changing the tbl and attributes I wanted to INNER JOIN. It either says the error message above, or when I do get no error message and it prints a table, there is no data in the tables. Can not figure out why, I am pretty new to SQL.

T-SQL logic for roll up and group by

I have a question to collapse or roll up data based on the logic below.
How can I implement it?
The logic that allows episodes to be condensed into a single continuous care episode is a discharge code of 22 followed by an admission code of 4 on the same day.
continuous care implementation update
EPN--is a business_key.
episode_continuous_care_key is an artificial key that can be a row number function.
Below is the table structure.
drop table #source
CREATE TABLE #source(patidid varchar(20),epn int,preadmitdate datetime,adminttime varchar(10),
admitcode varchar(10),datedischarge datetime,disctime varchar(10),disccode varchar(10))
INSERT INTO #source VALUES
(1849,1,'4/23/2020','7:29',1,'7/31/2020','9:03',22)
,(1849,2,'7/31/2020','11:00',4,'7/31/2020','12:09',22)
,(1849,3,'7/31/2020','13:10',4,'8/24/2020','10:36',10)
,(1849,4,'8/26/2020','12:25',2,null,null,null)
,(1850,1,'4/23/2020','7:33',1,'6/29/2020','7:30',22)
,(1850,2,'6/29/2020','9:35',4,'7/8/2020','10:51',7)
,(1850,3,'7/10/2020','11:51',3,'7/29/2020','9:12',7)
,(1850,4,'7/31/2020','11:00',2,'8/6/2020','10:24',22)
,(1850,5,'8/6/2020','12:26',4,null,null,null)
,(1851,1,'4/23/2020','7:35',1,'6/24/2020','13:45',22)
,(1851,2,'6/24/2020','15:06',4,'9/24/2020','15:00',2)
,(1851,3,'12/4/2020','8:59',0,null,null,null)
,(1852,1,'4/23/2020','7:37',1,'7/6/2020','11:15',20)
,(1852,2,'7/8/2020','10:56',0,'7/10/2020','11:46',2)
,(1852,3,'7/10/2020','11:47',2,'7/28/2020','13:16',22)
,(1852,4,'7/28/2020','15:17',4,'8/4/2020','11:37',22)
,(1852,5,'8/4/2020','13:40',4,'11/18/2020','15:43',2)
,(1852,6,'12/2/2020','15:23',2,null,null,null)
,(1853,1,'4/23/2020','7:40',1,'7/1/2020','8:30',22)
,(1853,2,'7/1/2020','14:57',4,'12/4/2020','12:55',7)
,(1854,1,'4/23/2020','7:44',1,'7/31/2020','13:07',20)
,(1854,2,'8/3/2020','16:30',0,'8/5/2020','9:32',2)
,(1854,3,'8/5/2020','10:34',2,'8/24/2020','8:15',22)
,(1854,4,'8/24/2020','10:33',4,'12/4/2020','7:30',22)
,(1854,5,'12/4/2020','9:13',4,null,null,null)
That Excel sheet image says little about your database design so I invented my own version that more or less resembles your image. With a proper database design the first step of the solution should not be required...
Unpivot timestamp information so that admission timestamp and discharge timestamps become one column.
I used a common table expression Log1 for this action.
Use the codes to filter out the start of the continuous care periods. Those are the admissions, marked with Code.IsAdmission = 1 in my database design.
Also add the next period start as another column by using the lead() function.
These are all the actions from Log2.
Add a row number as continuous care key.
Using the next period start date, find the current continuous period end date with a cross apply.
Replace empty period end dates with the current date using the coalesce() function.
Calculate the difference as the continuous care period duration with the datediff() function.
Sample data
create table Codes
(
Code int,
Description nvarchar(50),
IsAdmission bit
);
insert into Codes (Code, Description, IsAdmission) values
( 1, 'First admission', 1),
( 2, 'Re-admission', 1),
( 4, 'Campus transfer IN', 0),
(10, 'Trial visit', 0),
(22, 'Campus transfer OUT', 0);
create table PatientLogs
(
PatientId int,
AdmitDateTime smalldatetime,
AdmitCode int,
DischargeDateTime smalldatetime,
DischargeCode int
);
insert into PatientLogs (PatientId, AdmitDateTime, AdmitCode, DischargeDateTime, DischargeCode) values
(1849, '2020-04-23 07:29', 1, '2020-07-31 09:03', 22),
(1849, '2020-07-31 11:00', 4, '2020-07-31 12:09', 22),
(1849, '2020-07-31 13:10', 4, '2020-08-24 10:36', 10),
(1849, '2020-08-26 12:25', 2, null, null);
Solution
with Log1 as
(
select updt.PatientId,
case updt.DateTimeType
when 'AdmitDateTime' then updt.AdmitCode
when 'DischargeDateTime' then updt.DischargeCode
end as Code,
updt.LogDateTime,
updt.DateTimeType
from PatientLogs pl
unpivot (LogDateTime for DateTimeType in (AdmitDateTime, DischargeDateTime)) updt
),
Log2 as (
select l.PatientId,
l.Code,
l.LogDateTime,
lead(l.LogDateTime) over(partition by l.PatientId order by l.LogDateTime) as LogDateTimeNext
from Log1 l
join Codes c
on c.Code = l.Code
where c.IsAdmission = 1
)
select la.PatientId,
row_number() over(partition by la.PatientId order by la.LogDateTime) as ContCareKey,
la.LogDateTime as AdmitDateTime,
coalesce(ld.LogDateTime, convert(smalldatetime, getdate())) as DischargeDateTime,
datediff(day, la.LogDateTime, coalesce(ld.LogDateTime, convert(smalldatetime, getdate()))) as ContStay
from Log2 la -- log admission
outer apply ( select top 1 l1.LogDateTime
from Log1 l1
where l1.PatientId = la.PatientId
and l1.LogDateTime < la.LogDateTimeNext
order by l1.LogDateTime desc ) ld -- log discharge
order by la.PatientId,
la.LogDateTime;
Result
PatientId ContCareKey AdmitDateTime DischargeDateTime ContStay
--------- ----------- ---------------- ----------------- --------
1849 1 2020-04-23 07:29 2020-08-24 10:36 123
1849 2 2020-08-26 12:25 2021-02-03 12:49 161
Fiddle to see things in action with intermediate results.
Here is a T-SQL solution that contains primary and foreign key relationships.
To make it a bit more realistic, I added a simple "Patient" table.
I put all your "codes" into a single table which should make it easier to manage the codes.
I do not understand the purpose of your concept of "continuous care" so I just added an "is first" binary column to the Admission table.
You might also consider adding something about the medical condition for which the patient is being treated.
CREATE SCHEMA Codes
GO
GO
CREATE TABLE dbo.Code
(
codeNr int NOT NULL,
description nvarchar(50),
CONSTRAINT Code_PK PRIMARY KEY(codeNr)
)
GO
CREATE TABLE dbo.Patient
(
patientNr int NOT NULL,
birthDate date NOT NULL,
firstName nvarchar(max) NOT NULL,
lastName nvarchar(max) NOT NULL,
CONSTRAINT Patient_PK PRIMARY KEY(patientNr)
)
GO
CREATE TABLE dbo.Admission
(
admitDateTime time NOT NULL,
patientNr int NOT NULL,
admitCode int,
isFirst bit,
CONSTRAINT Admission_PK PRIMARY KEY(patientNr, admitDateTime)
)
GO
CREATE TABLE dbo.Discharge
(
dischargeDateTime time NOT NULL,
patientNr int NOT NULL,
dischargeCode int NOT NULL,
CONSTRAINT Discharge_PK PRIMARY KEY(patientNr, dischargeDateTime)
)
GO
ALTER TABLE dbo.Admission ADD CONSTRAINT Admission_FK1 FOREIGN KEY (patientNr) REFERENCES dbo.Patient (patientNr) ON DELETE NO ACTION ON UPDATE NO ACTION
GO
ALTER TABLE dbo.Admission ADD CONSTRAINT Admission_FK2 FOREIGN KEY (admitCode) REFERENCES dbo.Code (codeNr) ON DELETE NO ACTION ON UPDATE NO ACTION
GO
ALTER TABLE dbo.Discharge ADD CONSTRAINT Discharge_FK1 FOREIGN KEY (patientNr) REFERENCES dbo.Patient (patientNr) ON DELETE NO ACTION ON UPDATE NO ACTION
GO
ALTER TABLE dbo.Discharge ADD CONSTRAINT Discharge_FK2 FOREIGN KEY (dischargeCode) REFERENCES dbo.Code (codeNr) ON DELETE NO ACTION ON UPDATE NO ACTION
GO
GO

Check constraint based on information in another table

Given two tables:
TableA
(
id : primary key,
type : tinyint,
...
)
TableB
(
id : primary key,
tableAId : foreign key to TableA.id,
...
)
There is a check constraint on TableA.type with permitted values of (0, 1, 2, 3). All other values are forbidden.
Due to the known limitations, records in TableB can exist only when TableB.TableAId references the record in TableA with TableA.type=0, 1 or 2 but not 3. The latter case is forbidden and leads the system into an invalid state.
How can I guarantee that in such case the insert to TableB will fail?
Cross-table constraint using an empty indexed view:
Tables
CREATE TABLE dbo.TableA
(
id integer NOT NULL PRIMARY KEY,
[type] tinyint NOT NULL
CHECK ([type] IN (0, 1, 2, 3))
);
CREATE TABLE dbo.TableB
(
id integer NOT NULL PRIMARY KEY,
tableAId integer NOT NULL
FOREIGN KEY
REFERENCES dbo.TableA
);
The 'constraint view'
-- This view is always empty (limited to error rows)
CREATE VIEW dbo.TableATableBConstraint
WITH SCHEMABINDING AS
SELECT
Error =
CASE
-- Error condition: type = 3 and rows join
WHEN TA.[type] = 3 AND TB.id = TA.id
-- For a more informative error
THEN CONVERT(bit, 'TableB cannot reference type 3 rows in TableA.')
ELSE NULL
END
FROM dbo.TableA AS TA
JOIN dbo.TableB AS TB
ON TB.id = TA.id
WHERE
TA.[type] = 3;
GO
CREATE UNIQUE CLUSTERED INDEX cuq
ON dbo.TableATableBConstraint (Error);
Online demo:
-- All succeed
INSERT dbo.TableA (id, [type]) VALUES (1, 1);
INSERT dbo.TableA (id, [type]) VALUES (2, 2);
INSERT dbo.TableA (id, [type]) VALUES (3, 3);
INSERT dbo.TableB
(id, tableAId)
VALUES
(1, 1),
(2, 2);
-- Fails
INSERT dbo.TableB (id, tableAId) VALUES (3, 3);
-- Fails
UPDATE dbo.TableA SET [type] = 3 WHERE id = 1;
This is similar in concept to the linked answer to Check constraints that ensures the values in a column of tableA is less the values in a column of tableB, but this solution is self-contained (does not require a separate table with more than one row at all times). It also produces a more informational error message, for example:
Msg 245, Level 16, State 1
Conversion failed when converting the varchar value 'TableB cannot reference type 3 rows in TableA.' to data type bit.
Important notes
The error condition must be completely specified in the CASE expression to ensure correct operation in all cases. Do not be tempted to omit conditions implied by the rest of the statement. In this example, it would be an error to omit TB.id = TA.id (implied by the join).
The SQL Server query optimizer is free to reorder predicates, and makes no general guarantees about the timing or number of evaluations of scalar expressions. In particular, scalar computations can be deferred.
Completely specifying the error condition(s) within a CASE expression ensures the complete set of tests is evaluated together, and no earlier than correctness requires. From an execution plan perspective, this means the Compute Scalar associated with the CASE tests will appear on the indexed view delta maintenance branch:
The light shaded area highlights the indexed view maintenance region; the Compute Scalar containing the CASE expression is dark-shaded.

SQL - How to INSERT a foreign key as a value for a column

I know this is rather basic, and i've searched for answers for quite some time, but I'm troubled.
I don't know how to make my coding readable on here but here it is.
Here's the query for making the table in question:
CREATE TABLE customer
( customer_id INT NOT NULL CONSTRAINT customer_pk PRIMARY KEY IDENTITY,
first_name VARCHAR(20) NOT NULL,
surname VARCHAR(20) NOT NULL,
dob DATETIME NOT NULL,
home_address VARCHAR(50) NOT NULL,
contact_number VARCHAR(10) NOT NULL,
referrer_id INT NULL FOREIGN KEY REFERENCES customer(customer_id),
);
And here's the problem code:
--fill customer table
INSERT INTO customer
VALUES ( 'Harold', 'Kumar', '2010-07-07 14:03:54', '3 Blue Ln, Perth', 0812391245, NULL )
INSERT INTO customer
VALUES ( 'Bingo', 'Washisnameoh', '2010-09-21 12:30:07', '3 Red St, Perth', 0858239471, NULL )
INSERT INTO customer
VALUES ( 'John', 'Green', '2010-11-07 14:13:34', '4 Blue St, Perth', 0423904823, NULL )
INSERT INTO customer
VALUES ( 'Amir', 'Blumenfeld', '2010-11-01 11:03:04', '166 Yellow Rd, Perth', 0432058323, NULL)
INSERT INTO customer
VALUES ( 'Hank', 'Green', '2010-07-07 16:04:24', '444 Orange Crs, Perth', 0898412429, 8)
(Specifically the line with the 8 value at the end.)
When executing the second query it responds with this:
Msg 547, Level 16, State 0, Line 1
The INSERT statement conflicted
with the FOREIGN KEY SAME TABLE constraint
"FK_customer_referr__5772F790". The conflict occurred in database
"master", table "dbo.customer", column 'customer_id'. The statement
has been terminated.
Appreciate your help with this.
1)
You have a primary key on customer_id - and your insert statements do not have value for customer id
2)
You have a self referencing foreign key in the form of referrer_id referring to customer_id.
When you are inserting a record with referrer_id which is not null, in your case which is '8', make sure you already inserted a record with customer_id '8'
How do you know that the referrer_id is supposed to be 8 ??
What you need to do is catch the value of the customer_id inserted, and then used that in your second query:
DECLARE #referToID INT
INSERT INTO dbo.Customer(first_name, surname, dob, home_address, contact_number, referrer_id)
VALUES ('Harold', 'Kumar', '2010-07-07 14:03:54', '3 Blue Ln, Perth', 0812391245, NULL)
SELECT #ReferToID = SCOPE_IDENTITY() ; -- catch the newly given IDENTITY ID
INSERT INTO dbo.Customer(first_name, surname, dob, home_address, contact_number, referrer_id)
VALUES ('Hank', 'Green', '2010-07-07 16:04:24', '444 Orange Crs, Perth', 0898412429, #ReferToID)
I don't know which row you want to refer to (you didn't specify) - but I hope you understand the mechanism:
insert the new row into your table
get the newly inserted ID by using SCOPE_IDENTITY
insert the next row which refers to that first row and use that value returned by SCOPE_IDENTITY
Update: if you really want to have a given row reference itself (strange concept.....), then you'd need to do it in two steps:
insert the new row into your table
get the newly inserted ID by using SCOPE_IDENTITY
update that row to set the referrer_id
Something like this:
DECLARE #NewCustomerID INT
INSERT INTO dbo.Customer(first_name, surname, dob, home_address, contact_number)
VALUES ('Hank', 'Green', '2010-07-07 16:04:24', '444 Orange Crs, Perth', 0898412429)
SELECT #NewCustomerID = SCOPE_IDENTITY() ; -- catch the newly given IDENTITY ID
UPDATE dbo.Customer
SET referrer_id = #NewCustomerID
WHERE customer_id = #NewCustomerID
The only problem you have here is the identity must have a seed value which can be like Identity(1,1) where the first 1 is the starting point and the send 1 is the auto seed number...the re run your insert statement

Is it possible to a db constraint in for this rule?

I wish to make sure that my data has a constraint the following check (constraint?) in place
This table can only have one BorderColour per hub/category. (eg. #FFAABB)
But it can have multiple nulls. (all the other rows are nulls, for this field)
Table Schema
ArticleId INT PRIMARY KEY NOT NULL IDENTITY
HubId TINYINT NOT NULL
CategoryId INT NOT NULL
Title NVARCHAR(100) NOT NULL
Content NVARCHAR(MAX) NOT NULL
BorderColour VARCHAR(7) -- Can be nullable.
I'm gussing I would have to make a check constraint? But i'm not sure how, etc.
sample data.
1, 1, 1, 'test', 'blah...', '#FFAACC'
1, 1, 1, 'test2', 'sfsd', NULL
1, 1, 2, 'Test3', 'sdfsd dsf s', NULL
1, 1, 2, 'Test4', 'sfsdsss', '#AABBCC'
now .. if i add the following line, i should get some sql error....
INSERT INTO tblArticle VALUES (1, 2, 'aaa', 'bbb', '#ABABAB')
any ideas?
CHECK constraints are ordinarily applied to a single row, however, you can cheat using a UDF:
CREATE FUNCTION dbo.CheckSingleBorderColorPerHubCategory
(
#HubID tinyint,
#CategoryID int
)
RETURNS BIT
AS BEGIN
RETURN CASE
WHEN EXISTS
(
SELECT HubID, CategoryID, COUNT(*) AS BorderColorCount
FROM Articles
WHERE HubID = #HubID
AND CategoryID = #CategoryID
AND BorderColor IS NOT NULL
GROUP BY HubID, CategoryID
HAVING COUNT(*) > 1
) THEN 1
ELSE 0
END
END
Then create the constraint and reference the UDF:
ALTER TABLE Articles
ADD CONSTRAINT CK_Articles_SingleBorderColorPerHubCategory
CHECK (dbo.CheckSingleBorderColorPerHubCategory(HubID, CategoryID) = 1)
Another option that is available is available if you are running SQL2008. This version of SQL has a feature called filtered indexes.
Using this feature you can create a unique index that includes all rows except those where BorderColour is null.
CREATE TABLE [dbo].[UniqueExceptNulls](
[HubId] [tinyint] NOT NULL,
[CategoryId] [int] NOT NULL,
[BorderColour] [varchar](7) NULL,
)
GO
CREATE UNIQUE NONCLUSTERED INDEX UI_UniqueExceptNulls
ON [UniqueExceptNulls] (HubID,CategoryID)
WHERE BorderColour IS NOT NULL
This approach is cleaner than the approach in my other answer because it doesn't require creating extra computed columns. It also doesn't require you to have a unique column in the table, although you should have that anyway.
Finally, it will also be much faster than the UDF/Check Constraint solutions.
You can also do a trigger with something like this (this is actually overkill - you can make it cleaner by assuming the database is already in a valid state - i.e. UNION instead of UNION all etc):
IF EXISTS (
SELECT COUNT(BorderColour)
FROM (
SELECT INSERTED.HubId, INSERTED.CategoryId, INSERTED.BorderColour
UNION ALL
SELECT HubId, CategoryId, BorderColour
FROM tblArticle
WHERE EXISTS (
SELECT *
FROM INSERTED
WHERE tblArticle.HubId = INSERTED.HubId
AND tblArticle.CategoryId = INSERTED.CategoryId
)
) AS X
GROUP BY HubId, CategoryId
HAVING COUNT(BorderColour) > 1
)
RAISEERROR
If you have a unique column in your table, then you can accomplish this by creating a unique constraint on a computer column.
The following sample created a table that behaved as you described in your requirements and should perform better than a UDF based check constraint. You might also be able to improve the performance further by making the computed column persisted.
CREATE TABLE [dbo].[UQTest](
[Id] INT IDENTITY(1,1) NOT NULL,
[HubId] TINYINT NOT NULL,
[CategoryId] INT NOT NULL,
[BorderColour] varchar(7) NULL,
[BorderColourUNQ] AS (CASE WHEN [BorderColour] IS NULL
THEN cast([ID] as varchar(50))
ELSE cast([HuBID] as varchar(3)) + '_' +
cast([CategoryID] as varchar(20)) END
),
CONSTRAINT [UQTest_Unique]
UNIQUE ([BorderColourUNQ])
)
The one possibly undesirable facet of the above implementation is that it allows a category/hub to have both a Null AND a color defined. If this is a problem, let me know and I'll tweak my answer to address that.
PS: Sorry about my previous (incorrect) answer. I didn't read the question closely enough.

Resources