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

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.

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

How to use MERGE-statement with VARBINARY data

I'm stuck trying to figure out how to get one of the MERGE statements to work. See below code snippet:
DECLARE #PipelineRunID VARCHAR(100) = 'testestestestest'
MERGE [TGT].[AW_Production_Culture] as [Target]
USING [SRC].[AW_Production_Culture] as [Source]
ON [Target].[MD5Key] = [Source].[MD5Key]
WHEN MATCHED AND [Target].[MD5Others] != [Source].[MD5Others]
THEN UPDATE SET
[Target].[CultureID] = [Source].[CultureID]
,[Target].[ModifiedDate] = [Source].[ModifiedDate]
,[Target].[Name] = [Source].[Name]
,[Target].[MD5Others] = [Source].[MD5Others]
,[Target].[PipelineRunID] = #PipelineRunID
WHEN NOT MATCHED BY TARGET THEN
INSERT VALUES (
[Source].[AW_Production_CultureKey]
,[Source].[CultureID]
,[Source].[ModifiedDate]
,[Source].[Name]
,#PipelineRunID
,[Source].[MD5Key]
,[Source].[MD5Others]);
When I try and run this query I receive the following error:
Msg 257, Level 16, State 3, Line 16
Implicit conversion from data type varchar to varbinary is not allowed. Use the CONVERT function to run this query.
The only VARBINARY column types are MD5Key and MD5Others. As they are both linked to their corresponding columns I don't understand why my error message indicates there is a VARCHAR problem involved. Does anybody understand how and why I should use a CONVERT() function here?
Thanks!
--EDIT: Schema definitions
CREATE VIEW [SRC].[AW_Production_Culture]
WITH SCHEMABINDING
as
SELECT
CAST(CONCAT('',[CultureID]) as VARCHAR(100)) as [AW_Production_CultureKey]
,CAST(HASHBYTES('MD5',CONCAT('',[CultureID])) as VARBINARY(16)) as [MD5Key]
,CAST(HASHBYTES('MD5',CONCAT([ModifiedDate],'|',[Name])) as VARBINARY(16)) as [MD5Others]
,[CultureID],[ModifiedDate],[Name]
FROM
[SRC].[tbl_AW_Production_Culture]
CREATE TABLE [TGT].[AW_Production_Culture](
[AW_Production_CultureKey] [varchar](100) NOT NULL,
[CultureID] [nchar](6) NULL,
[ModifiedDate] [datetime] NULL,
[Name] [nvarchar](50) NULL,
[MD5Key] [varbinary](16) NOT NULL,
[MD5Others] [varbinary](16) NOT NULL,
[RecordValidFrom] [datetime2](7) GENERATED ALWAYS AS ROW START NOT NULL,
[RecordValidUntil] [datetime2](7) GENERATED ALWAYS AS ROW END NOT NULL,
[PipelineRunID] [varchar](36) NOT NULL,
PRIMARY KEY CLUSTERED
(
[MD5Key] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY],
PERIOD FOR SYSTEM_TIME ([RecordValidFrom], [RecordValidUntil])
) ON [PRIMARY]
WITH
(
SYSTEM_VERSIONING = ON ( HISTORY_TABLE = [TGT].[AW_Production_Culture_History] )
)
Reposting my comment as an answer for the sweet, sweet, internet points:
You're getting that error because your varbinary value is being inserted into a varchar column. As your columns have the correct types already then it means your INSERT clause has mismatched columns.
As it is, your MERGE statement is not explicitly listing the destination columns - you should always explicitly list columns in production code so that your DML queries won't break if columns are added or reordered or marked HIDDEN.
So to fix this, change your INSERT clause to explicitly list destination column names.
Also, when using MERGE you should use HOLDLOCK (Or a more suitable lock, if applicable) - otherwise you’ll run into concurrency issues. MERGE is not concurrency-safe by default!
Minor nit-picks that are largely subjective:
I personally prefer avoiding [escapedName] wherever possible and prefer using short table aliases.
e.g. use s and t instead of [Source] and [Target].
"Id" (for "identity" or "identifier") is an abbreviation, not an acronym - so it should be cased as Id and not ID.
Consider using an OUTPUT clause to help diagnose/debug issues too.
So I'd write it like so:
DECLARE #PipelineRunId VARCHAR(100) = 'testestestestest'
MERGE INTO
tgt.AW_Production_Culture WITH (HOLDLOCK) AS t
USING
src.AW_Production_Culture AS s ON t.MD5Key = s.MD5Key
WHEN MATCHED AND t.MD5Others != s.MD5Others THEN UPDATE SET
t.CultureId = s.CultureId,
t.ModifiedDate = s.ModifiedDate,
t.Name = s.Name,
t.MD5Others = s.MD5Others,
t.PipelineRunID = #PipelineRunId
WHEN NOT MATCHED BY TARGET THEN INSERT
(
AW_Production_CultureKey,
CultureId,
ModifiedDate,
[Name],
PipelineRunId,
MD5Key,
MD5Others
)
VALUES
(
s.AW_Production_CultureKey,
s.CultureId,
s.ModifiedDate,
s.[Name],
#PipelineRunId,
s.MD5Key,
s.MD5Others
)
OUTPUT
$action AS [Action],
inserted.*,
deleted.*;

Temp tables, Column name or number of supplied values does not match table definition

Even though this tends to look as a duplicate, I had to post it as I can't seem to spot the error.
I don't know if I am mad or what but I can't seem to spot why there is a mismatch in the number of supplied values.
Here are they:
CREATE TABLE #TIPSTOPE_TS
(
TIP INT NULL,
SIFVAL VARCHAR(5),
GRUPA INT NULL,
DATUMOD VARCHAR(15),
PASIVNA DECIMAL(15,4) NULL DEFAULT(0),
REDOVNA DECIMAL(15,4) NULL DEFAULT(0),
ZATEZNA DECIMAL(15,4) NULL DEFAULT(0),
STOPA DECIMAL(15,4) NULL DEFAULT(0),
DATUMDO VARCHAR(15),
KONTO VARCHAR(15),
)
INSERT INTO #TIPSTOPE_TS
SELECT TS.TIP,
TS.SIFVAL,
TS.GRUPA,
CASE WHEN ISDATE(MAX(TS.DATUMOD)) = 0 THEN '2017.12.31' ELSE MAX(TS.DATUMOD) END AS DATUMOD,
CAST (2 AS DECIMAL(10,4)) AS PASIVNA,
CAST (1 AS DECIMAL(10,4)) AS REDOVNA,
CAST (3 AS DECIMAL(10,4)) AS ZATEZNA,
TS.REDOVNA,
TS.DATUMDO,
TP.M1 AS KONTO
FROM TIPSTOPE TS WITH(NOLOCK)
JOIN TIPPART TP WITH(NOLOCK) ON TP.TIP = TS.TIP
WHERE TS.DATUMOD <= '2017.12.31'
GROUP BY TS.TIP,TS.SIFVAL,TS.GRUPA,TP.M1,TS.DATUMDO,TS.REDOVNA
CREATE NONCLUSTERED INDEX IX_TIPSTOPE_TS ON #TIPSTOPE_TS (TIP, GRUPA, SIFVAL)
INCLUDE (DATUMOD)
And the second one...
CREATE TABLE #UNPVT_TIPSTOPE_TS
(
TIP INT NULL,
SIFVAL VARCHAR(5) NULL,
GRUPA INT NULL,
DATUMOD VARCHAR(10) NULL,
TIP_KS VARCHAR(15) NULL,
KAMATNA_STOPA DECIMAL(15,4) NULL DEFAULT(0),
DATUMDO VARCHAR(10) NULL,
)
INSERT INTO #UNPVT_TIPSOPE_TS
SELECT TIP, SIFVAL, GRUPA, DATUMOD, TIP_KS, KAMATNA_STOPA,DATUMDO
FROM
(
SELECT TIP, SIFVAL, GRUPA, DATUMOD, ISNULL(REDOVNA,0) AS REDOVNA, ISNULL(PASIVNA,0) AS PASIVNA, ISNULL(ZATEZNA,0) AS ZATEZNA,STOPA,DATUMDO
FROM #TIPSTOPE_TS
) P
UNPIVOT (KAMATNA_STOPA FOR TIP_KS IN (REDOVNA, PASIVNA, ZATEZNA)) AS UNPVT
The second temp tables is taking data from the first one.
When I try to create the second one error is thrown:
Insert error: Column name or number of supplied values does not match table definition
You are specifying the exact number of values that are needed. If you copy the whole code in new query window and execute it, it will work. Or in your current window drop the table table:
DROP TABLE #TIPSTOPE_TS;
DROP TABLE #UNPVT_TIPSTOPE_TS;
I mean execute only the above statements, and the execute the rest of the code. It should work again.
Sometime, when are debugging we forgot that the temporary table meta data is cached. For example, you can have the following code:
DROP TABLE IF EXISTS #TEST;
CREATE TABLE #TEST
(
[A] INT
);
INSERT INTO #TEST ([A])
SELECT 1;
And its valid. If we change it to this:
DROP TABLE IF EXISTS #TEST;
CREATE TABLE #TEST
(
[A] INT
,[B] INT
);
INSERT INTO #TEST ([A], [B])
SELECT 1, 2;
We will get:
Msg 207, Level 16, State 1, Line 9 Invalid column name 'B'.
Because, in the current session the #TEST table already exists and the engine is able to check that the B column does not exists. So, we need to drop the table manually, after the columns are changed, or we need to drop the tables at the end of our code statements.

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)

MSSQL aggregate ignoring where clause

I have a strange problem that when performing an aggregate function on a type cast varchar column I receive an "Msg 8114, Level 16, State 5, Line 1. Error converting data type nvarchar to bigint." The queries where clause should filter out the non-numeric values.
Table structure is similar to this:
IF EXISTS (SELECT * FROM sys.all_objects ao WHERE ao.name = 'Identifier' AND ao.type = 'U') BEGIN DROP TABLE Identifier END
IF EXISTS (SELECT * FROM sys.all_objects ao WHERE ao.name = 'IdentifierType' AND ao.type = 'U') BEGIN DROP TABLE IdentifierType END
CREATE TABLE IdentifierType
(
[ID] [int] IDENTITY(1,1) NOT NULL,
[Style] [int] NULL,
CONSTRAINT [PK_IdentifierType_ID] PRIMARY KEY CLUSTERED ([ID] ASC)
) ON [PRIMARY]
CREATE TABLE Identifier
(
[ID] [int] IDENTITY(1,1) NOT NULL,
[IdentifierTypeID] [int] NOT NULL,
[Value] [nvarchar](4000) NOT NULL,
CONSTRAINT [PK_Identifier_ID] PRIMARY KEY CLUSTERED ([ID] ASC)
) ON [PRIMARY]
ALTER TABLE Identifier WITH CHECK ADD CONSTRAINT [FK_Identifier_IdentifierTypeID] FOREIGN KEY([IdentifierTypeID]) REFERENCES IdentifierType ([ID])
GO
Identifier.Value is a VARCHAR column, it can and does contain non-numeric data. Filtering the query to IdentifierType.Style = 0 should mean that 'Value' only returns string representations of integers. The query below fails with "Msg 8114, Level 16, State 5, Line 1. Error converting data type nvarchar to bigint."
SELECT
MAX(CAST(Value AS BIGINT))
FROM
Identifier i,
IdentifierType it
WHERE
i.IdentifierTypeID = it.ID AND
it.Style = 0
If i extend the WHERE clause to include a 'AND ISNUMERIC(i.Value) = 1' it will return the maximum integer value. That to me implies that there is a non-numeric string in my result set. Yet i get no rows returned from this:
SELECT
*
FROM
Identifier i,
IdentifierType it
WHERE
i.IdentifierTypeID = it.ID AND
it.Style = 0 AND
ISNUMERIC(i.Value) <> 1
I've been unable to identity the row(s) that are tripping the type cast. The above query should have exposed the exceptional rows. In addition, there are no empty or extremely long strings either (the largest string is 6 character long)
Is it possible that MSSQL is attempting to do the CAST on all rows rather than filtering via the WHERE clause first?
Or has anyone else seen anything similar?
There is a second work around which is instantiating the component of the query into a temp table, and then selecting the MAX value from that.
SELECT
Value
INTO
IdentifierClone
FROM
Identifier i,
IdentifierType it
WHERE
i.IdentifierTypeID = it.ID AND
it.Style = 0
SELECT MAX(CAST(Value as BIGINT)) FROM IdentifierClone
A subquery doesn't work however.
Any help or thoughts would be appreciated.
Try using a REGEX expression to find the problem record. Here's an example where ISNUMERIC does not detect the problem but the regex expression does
CREATE TABLE tst (value nvarchar(4000))
INSERT INTO tst select '£'
-- Record found ...
SELECT * FROM tst WHERE value NOT LIKE '%[0-9]%'
-- No record found ...
SELECT * from tst where isnumeric(value) <> 1

Resources