SQL Server : replace UDF with a joined view - sql-server

I'd would like to ask your help with a gordian knot in my head with regards to SQL Server. I'm trying to replace an UDF with a joined view, but I'm struggling to get the view to return what I need. A bit of clever ordering or so may well do the trick, I'm stuck.
Unfortunately I can't sign up to SQL fiddle at the moment so I have to present the test data here:
CREATE TABLE #Contacts
(
ID INT NOT NULL IDENTITY,
Firstname VARCHAR(50) NULL,
Lastname VARCHAR(50) NULL
)
GO
CREATE TABLE #Cars (ID INT NOT NULL IDENTITY, CarModel VARCHAR(50) NULL)
GO
CREATE TABLE #Ownership
(
Contacts_ID INT NOT NULL,
cars_id INT NOT NULL,
ownership_type TINYINT NOT NULL,
DisplayName VARCHAR(50) NULL
)
GO
CREATE TABLE #Races(ID INT NOT NULL IDENTITY, RaceName VARCHAR(50) NOT NULL)
GO
CREATE TABLE #RaceEntries
(
ID INT NOT NULL IDENTITY,
Races_ID INT NOT NULL,
Contacts_ID INT NOT NULL,
cars_id INT NOT NULL
)
INSERT [#Contacts] ([Firstname], [Lastname])
SELECT
'Justin', 'Case' UNION ALL SELECT
'Gladys', 'Friday' UNION ALL SELECT
'Mandy', 'Lifeboats'
GO
INSERT [#Cars] ([CarModel])
VALUES ('Great Car')
GO
INSERT [#Races] ([RaceName])
VALUES ('A Car Race')
INSERT [#Ownership] ([Contacts_ID], [cars_id], [ownership_type], [DisplayName]) SELECT
1, 1, 0, NULL UNION ALL SELECT
2, 1, 1, NULL UNION ALL SELECT
3, 1, 1, 'Mandy Lifeboats Racing Team'
INSERT [#RaceEntries] ([Races_ID], [Contacts_ID], [cars_id]) SELECT
1, 1, 1 UNION ALL SELECT
1, 3, 1 UNION ALL SELECT
1, 2, 1
What I'd like:
SELECT
[cars_id], mvo.Ownername
FROM
[#RaceEntries] -- join a view that returns the ownername
LEFT OUTER JOIN
#myViewOwnername AS mvo ON mvo.Contacts_ID = [#RaceEntries].[Contacts_ID] AND mvo.Cars_ID = [#RaceEntries].[cars_id]
The issue here is that every car only has one owner (type 0 in ownership). It can have other contacts as representatives.
Usually on lists for #RaceEntries, the owner's name is displayed, unless the representative has an agreed "an override" (so that his or a company name is displayed).
In the above example, for Justin Case's entry it's straight-forward. He is the owner (type 0), end of story.
When Gladys Friday enters (she doesn't have an override "DisplayName") the system should again return Justin Case's name as the owner.
In the last example, Mandy Lifeboats has a DisplayName and therefore that should be returned.
Ideally, I would end up with a view or similar that does the heavy lifting, so that I can join it to x000 records from #RaceEntries (joined on car and contact ID) to get the correct owner name back.
I hope I've simplified the example as much as possible, the real thing is a bit more complex... Please let me know if I should prepare anything else to make helping a bit easier. Many thanks!

I think this might be what you are looking for:
select [#Ownership].Contacts_ID,
[#Ownership].Cars_ID,
coalesce([DisplayName], CarOwners.ContactName) Ownername
into #myViewOwnername
from [#Ownership]
join
(
select [cars_id], Contacts_ID, [#Contacts].Firstname + ' ' + [#Contacts].Lastname ContactName
from [#Ownership]
join [#Contacts]
on [#Ownership].Contacts_ID = [#Contacts].ID
where ownership_type = 0
) CarOwners
on [#Ownership].cars_id = CarOwners.cars_id
A car has one direct owner (type 0), so the sub-query will get you all the owner names of each car. Then you just join that to your ownership table on the cars_id fields. If there is a display name in the ownership table you display that, otherwise you show the car owner's name.

Related

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

Create table from query result

I do have a problem with creating proper SQL statement.
Scenario, i do have a table "Excel" which is populated by SQLBulkCopy few times, after that table contains duplicates which I want to sum by "Buildneed", I have figured out way to return "consolidated" result with below query :
SELECT GBC, Description, sum(Buildneed) as Buildneed, Replaced
FROM Excel
GROUP BY GBC, Description, Replaced
ORDER BY GBC ASC
Now I would like to drop NewTable if exists, create NewTable again and populate this table from mentioned above result.
So I wrote this statement :
DROP TABLE IF EXISTS dbo.NewTable
CREATE TABLE [dbo].[NewTable]
(
[GBC] INT NULL,
[Description] VARCHAR (80) NULL,
[Buildneed] INT NULL,
[Replaced] VARCHAR (80) NULL
);
SELECT Excel2.GBC, Excel2.Description, Excel2.Buildneed, Excel2.Replaced
INTO NewTable
FROM (SELECT GBC, Description, sum(Buildneed) as Buildneed, Replaced
FROM Excel
GROUP BY GBC, Description, Replaced
ORDER BY GBC ASC) AS Excel2
I'm not receiving any error information, table is not created after running above query.
Every single time new data will come to table "Excel" I want to "consolidate -> drop NewTable -> create NewTable with new data"
Sample data :
CREATE TABLE [dbo].[Excel]
(
[GBC] INT NULL,
[Description] VARCHAR (80) NULL,
[Buildneed] INT NULL,
[Replaced] VARCHAR (80) NULL
);
INSERT INTO #Excel (GBC, [Description], Buildneed, Replaced)
SELECT 71744, 'RES_TF,10k,0402,1%,0,1W,100PPM/C', 2000
UNION ALL
SELECT 71744, 'RES_TF,10k,0402,1%,0,1W,100PPM/C', 1000
UNION ALL
SELECT 76527, 'CAP_CER,10nF,0402,10%,50V,X7R', 288
UNION ALL
SELECT 86911, 'CAP_CER,10nF,0603,10%,100V,X7R', 1464
Expected result -> New table created with name "NewTable"
GBC / Description / Buildneed / Replaced
71744 / RES_TF,10k,0402,1%,0,1W,100PPM/C / **3000** / null
76527 / CAP_CER,10nF,0402,10%,50V,X7R / 288 / null
86911 / CAP_CER,10nF,0603,10%,100V,X7R / 1463 / null
First of all, I suggest to create the new table with another name, drop the old table after the new table has successfully been created and then rename the new table to the desired name - so you prevent data loss in case anything doesn't work during table creation. Furthermore the SELECT ... INTO should create a new table without the need of creating it first. Another possibility would be INSERT INTO... SELECT...
Here an example:
CREATE TABLE [dbo].[Excel] (
[GBC] INT NULL,
[Description] VARCHAR (80) NULL,
[Buildneed] INT NULL,
[Replaced] VARCHAR (80) NULL
);
insert into [Excel] (GBC, [Description], Buildneed, Replaced)
select 71744, 'RES_TF,10k,0402,1%,0,1W,100PPM/C' ,2000, NULL
union all
select 71744, 'RES_TF,10k,0402,1%,0,1W,100PPM/C' ,1000, NULL
union all
select 76527, 'CAP_CER,10nF,0402,10%,50V,X7R' ,288, NULL
union all
select 86911, 'CAP_CER,10nF,0603,10%,100V,X7R' ,1464, NULL;
SELECT Excel2.GBC, Excel2.Description, Excel2.Buildneed, Excel2.Replaced
INTO NewTable
FROM (SELECT GBC, Description, sum(Buildneed) as Buildneed, Replaced FROM Excel GROUP BY GBC, Description, Replaced) as Excel2;
SELECT *
FROM NewTable
See SQL Fiddle for an example: sqlfiddle.com/#!18/d568e/5/2

How to find specific characters in a string and replace them with values fetched from a table in SQL Server

I have text stored in the table "StructureStrings"
Create Table StructureStrings(Id INT Primary Key,String nvarchar(4000))
Sample Data:
Id String
1 Select * from Employee where Id BETWEEN ### and ### and Customer Id> ###
2 Select * from Customer where Id BETWEEN ### and ###
3 Select * from Department where Id=###
and I want to replace the "###" word with a values fetched from another table
named "StructureValues"
Create Table StructureValues (Id INT Primary Key,Value nvarcrhar(255))
Id Value
1 33
2 20
3 44
I want to replace the "###" token present in the strings like
Select * from Employee where Id BETWEEN 33 and 20 and Customer Id> 44
Select * from Customer where Id BETWEEN 33 and 20
Select * from Department where Id=33
PS: 1. Here an assumption is that the values will be replaced with the tokens in the same order i.e first occurence of "###" will be replaced by first value of
"StructureValues.Value" column and so on.
Posting this as a new answer, rather than editting my previous.
This uses Jeff Moden's DelimitedSplit8K; it does not use the built in splitter available in SQL Server 2016 onwards, as it does not provide an item number (thus no join criteria).
You'll need to firstly put the function on your server, then you'll be able to use this. DO NOT expect it to perform well. There's a lot of REPLACE in this, which will hinder performance.
SELECT (SELECT REPLACE(DS.Item, '###', CONVERT(nvarchar(100), SV.[Value]))
FROM StructureStrings sq
CROSS APPLY DelimitedSplit8K (REPLACE(sq.String,'###','###|'), '|') DS --NOTE this uses a varchar, not an nvarchar, you may need to change this if you really have Unicode characters
JOIN StructureValues SV ON DS.ItemNumber = SV.Id
WHERE SS.Id = sq.id
FOR XML PATH ('')) AS NewString
FROM StructureStrings SS;
If you have any question, please place the comments on this answer; do not put them under the question which has already become quite a long discussion.
Maybe this is what you are looking for.
DECLARE #Employee TABLE (Id int)
DECLARE #StructureValues TABLE (Id int, Value int)
INSERT INTO #Employee
VALUES (1), (2), (3), (10), (15), (20), (21)
INSERT INTO #StructureValues
VALUES (1, 10), (2, 20)
SELECT *
FROM #Employee
WHERE Id BETWEEN (SELECT MIN(Value) FROM #StructureValues) AND (SELECT MAX(Value) FROM #StructureValues)
Very different take here:
CREATE TABLE StructureStrings(Id int PRIMARY KEY,String nvarchar(4000));
INSERT INTO StructureStrings
VALUES (1,'SELECT * FROM Employee WHERE Id BETWEEN ### AND ###'),
(2,'SELECT * FROM Customer WHERE Id BETWEEN ### AND ###');
CREATE TABLE StructureValues (Id int, [Value] int);
INSERT INTO StructureValues
VALUES (1,10),
(2,20);
GO
DECLARE #SQL nvarchar(4000);
--I'm asuming that as you gave one output you are supplying an ID or something?
DECLARE #Id int = 1;
WITH CTE AS(
SELECT SS.Id,
SS.String,
SV.[Value],
LEAD([Value]) OVER (ORDER BY SV.Id) AS NextValue,
STUFF(SS.String,PATINDEX('%###%',SS.String),3,CONVERT(varchar(10),[Value])) AS ReplacedString
FROM StructureStrings SS
JOIN StructureValues SV ON SS.Id = SV.Id)
SELECT #SQL = STUFF(ReplacedString,PATINDEX('%###%',ReplacedString),3,CONVERT(varchar(10),NextValue))
FROM CTE
WHERE Id = #Id;
PRINT #SQL;
--EXEC (#SQL); --yes, I should really be using sp_executesql
GO
DROP TABLE StructureValues;
DROP TABLE StructureStrings;
Edit: Note that Id 2 will return NULL, as there isn't a value to LEAD to. If this needs to change, we'll need more logic on what the value should be if there is not value to LEAD to.
Edit 2: This was based on the OP's original post, not what he puts it as later. As it currently stands, it's impossible.

Return Data and Update Row without Multiple Lookups?

I have a stored procedure that looks up an article based on the article's title. But I also need to increment a column in the same table that counts the number of times the article is viewed.
Trying to be as efficient as possible, I see two possible ways to approach this:
Perform one SELECT to obtain the PK on the target row. Then use that PK to increment the number of views and, finally, another SELECT using the PK to return the article data.
Perform one SELECT to return the article data to my application, and then use the returned PK to make another round trip to the database to increment the number of views.
I know #1 would be pretty fast, but it's three lookups. And #2 requires two round trips to the database. Is there no way to optimize this task?
EDIT Based on feedback, I came up with the following. Thanks for any comments or constructive criticism.
DECLARE #Slug VARCHAR(250) -- Stored procedure argument
-- declare #UpdatedArticle table variable
DECLARE #UpdatedArticle TABLE
(
ArtID INT,
ArtUserID UNIQUEIDENTIFIER,
ArtSubcategoryID INT,
ArtTitle VARCHAR(250),
ArtHtml VARCHAR(MAX),
ArtDescription VARCHAR(350),
ArtKeywords VARCHAR(250),
ArtLicenseID VARCHAR(10),
ArtViews BIGINT,
ArtCreated DATETIME2(7),
ArtUpdated DATETIME2(7)
);
UPDATE Article
SET ArtViews = ArtViews + 1
OUTPUT
INSERTED.ArtID,
INSERTED.ArtUserID,
inserted.ArtSubcategoryID,
INSERTED.ArtTitle,
INSERTED.ArtHtml,
INSERTED.ArtDescription,
INSERTED.ArtKeywords,
INSERTED.ArtLicenseID,
INSERTED.ArtViews,
INSERTED.ArtUpdated,
INSERTED.ArtCreated
INTO #UpdatedArticle
WHERE ArtSlugHash = CHECKSUM(#Slug) AND ArtSlug = #Slug AND ArtApproved = 1
SELECT a.ArtID, a.ArtUserID, a.ArtTitle, a.ArtHtml, a.ArtDescription, a.ArtKeywords, a.ArtLicenseID,
l.licTitle, a.ArtViews, a.ArtCreated, a.ArtUpdated, s.SubID, s.SubTitle, c.CatID, c.CatTitle,
sec.SecID, sec.SecTitle, u.UsrDisplayName AS UserName
FROM #UpdatedArticle a
INNER JOIN Subcategory s ON a.ArtSubcategoryID = s.SubID
INNER JOIN Category c ON s.SubCatID = c.CatID
INNER JOIN [Section] sec ON c.CatSectionID = sec.SecID
INNER JOIN [User] u ON a.ArtUserID = u.UsrID
INNER JOIN License l ON a.ArtLicenseID = l.LicID
Here is a way using the OUTPUT statement (SQL Server 2005 onwards), in a single update statement:
IF OBJECT_ID ('Books', 'U') IS NOT NULL
DROP TABLE dbo.Books;
CREATE TABLE dbo.Books
(
BookID int NOT NULL PRIMARY KEY,
BookTitle nvarchar(50) NOT NULL,
ModifiedDate datetime NOT NULL,
NumViews int not null CONSTRAINT DF_Numviews DEFAULT (0)
);
INSERT INTO dbo.Books
(BookID, BookTitle, ModifiedDate)
VALUES
(106, 'abc', GETDATE()),
(107, 'Great Expectations', GETDATE());
-- declare #UpdateOutput1 table variable
DECLARE #UpdateOutput1 table
(
BookID int,
BookTitle nvarchar(50),
ModifiedDate datetime,
NumViews int
);
-- >>>> here is the update of Numviews and the Fetch
-- update Numviews in Books table, and retrive the row
UPDATE Books
SET
NumViews = NumViews + 1
OUTPUT
INSERTED.BookID,
INSERTED.BookTitle,
INSERTED.ModifiedDate,
INSERTED.NumViews
INTO #UpdateOutput1
WHERE BookID = 106
-- view updated row in Books table
SELECT * FROM Books;
-- view output row in #UpdateOutput1 variable
SELECT * FROM #UpdateOutput1;

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