TSQL CASE on Multiple columns - sql

declare #T table
(
ID int identity primary key,
FBK_ID BIGINT null,
TWT_ID BIGINT null,
LNK_ID NVARCHAR(50) null
);
Each record can ONLY have either a FBK_ID or a TWT_ID or a LNK_ID. No records have multiple values on those fields.
So mainly some records will have FacebookID values, some others have TwitterID, some others have LinkedInID.
QUESTIONS:
what is the fastest and cleanest way to do this?
Select ID, Type from #T
....where Type is a nvarchar(10) equal to either 'Facebook' or 'Twitter' or 'LinkedIn' depending on who has a value?

You could do something like this:
select
ID ,
case when FBK_ID is not null then FBK_ID
when TWT_ID is not null then TWT_ID
else LNK_ID end as LinkID
from #t
where <rest of your conditions if any>
You will get back the ID and one of the link IDS for the specific social network. If you want to know, additionally, to what kind of social network does the LinkID returned belongs to, you can add an extra column as so:
select
ID ,
case when FBK_ID is not null then FBK_ID,
when TWT_ID is not null then TWT_ID
else LNK_ID end as LinkID,
case when FBK_ID is not null then 'F'
when TWT_ID is not null then 'T'
else 'L' end as LinkIDFrom
from #t
where <rest of your conditions if any>

Each record can ONLY have either a FBK_ID or a TWT_ID or a LNK_ID. No
records have multiple values on those fields.
First fix your table:
DECLARE #T TABLE
(
ID INT IDENTITY PRIMARY KEY,
FBK_ID BIGINT NULL,
TWT_ID BIGINT NULL,
LNK_ID NVARCHAR(50) NULL,
CHECK (
(FBK_ID IS NOT NULL AND TWT_ID IS NULL AND LNK_ID IS NULL)
OR (FBK_ID IS NULL AND TWT_ID IS NOT NULL AND LNK_ID IS NULL)
OR (FBK_ID IS NULL AND TWT_ID IS NULL AND LNK_ID IS NOT NULL)
)
);
what is the fastest and cleanest way to do this?
This was quite fast for me to write, employing copy+paste, and looks clean to my eye:
SELECT ID, CAST('Facebook' AS NVARCHAR(10)) AS Type
FROM #T
WHERE FBK_ID IS NOT NULL
UNION
SELECT ID, CAST('Twitter' AS NVARCHAR(10)) AS Type
FROM #T
WHERE TWT_ID IS NOT NULL
UNION
SELECT ID, CAST('LinkedIn' AS NVARCHAR(10)) AS Type
FROM #T
WHERE LNK_ID IS NOT NULL;

Related

WHERE ISNULL doesn't bring though records where target value IS NULL

The title only explains my first attempt, I couldn't think how to word the issue/my need exactly.
I have table job:
CREATE TABLE [dbo].[job]
(
[jobId] INT IDENTITY(1,1) PRIMARY KEY,
[contractId] INT NOT NULL,
[districtId] INT NULL,
[address] NVARCHAR (255) NULL
);
I'm trying to create a function where all of the column values can be optionally passed through to do a filter search.
So far I have:
DECLARE
#jobId INT,
#contracId INT,
#districtId INT,
#addressPart NVARCHAR(255)
SELECT
[jobId],
[contractId],
[districtId],
[address]
FROM
[dbo].job
WHERE
jobId = ISNULL(#jobId,jobId)
AND contractId = ISNULL(#contracId, contractId)
AND [address] LIKE '%' + ISNULL(#addressPart, [address]) + '%'
This works, however because the districtId can be null in the Job table doing the same ISNULL where clause as for contractId results in the query to only ever return records where districtId is not null, as the '=' condition doesn't work for null values.
I've tried using COALESCE
AND COALESCE(districtId, 0) = COALESCE(#districtId, 0)
But that results in the query only selecting records where districtId is null if no districtId is specified. It does select the the row with the districtId if a 'district' is set. But this is ultimately no good as if any other variable is set which would match a record, if that record has null districtId it won't be selected.
I've also tried using OR
AND (districtId = ISNULL(#districtId, districtId) OR districtId IS NULL)
But this returns everything when the districtId is null, but then if a districtId is set, it'll get that record, but also still return all records that have districtId null.
I can't even think how to do this with a dirty CASE statement, as I'd need to do districtId = and as soon as I do that '=' its not going to work as it need to on some level check for null is null which '=' can't do.
You must check separately if #districtId is null in which case the condition should return true:
AND (#districtId IS NULL OR districtId = #districtId)
You still need to include the column in the COALESCE to handle the param = column value
AND COALESCE(districtId ,0) = COALESCE(#districtId, districtId,0)

Cumulative query with 'group by' causing slowness

We have a table which will store the information of Users like deals performed, date etc. Same user can perform same deals multiple times. Now we want to display the cumulative sum of amount user performed totally if deal no is same
Query with GROUP BY Causing Slowness
Query to display cumulative sum joining the same table.
Table has 2.7 million records.
And this goes for proper index scan (Index structure).
INDEX `IDX_GRPBY2` (`DEAL`, `FIN`),
INDEX `Table1_TEMP` (`CREATE1`, `FEVSTATUS`, `EVE`)
Below is the expected result
Expected Result
Below is the actual query which is used to get deals with cumulative sum if it's same deal:
SELECT MAX(A.PKEY) PKEY,
A.ENT, A.DEAL, A.FIN, A.DATE1, A.TYPE1, A.STEPNO,
A.CREATE1, A.EVE, A.FEVSTATUS, A.STATUSDATE, A.INTAMT,
A.INTPAID, A.INT_TYPE, A.PENALTY_1, A.CONV_PAID_4,
A.INT_PAID_4, A.CONVFIN_4, A.INTTYPE,
B.DEAL AS DEAL_B, B.FIN AS FIN_B,
SUM(B.CONV_PAID_4) AS CONV_PAID_4_OUT
FROM Table1 A, Table1 B
WHERE A.CREATE1 = '0'
AND (A.FEVSTATUS = '1'
OR A.EVE IN ('E06', 'E07', 'E02', 'E15', 'E03', 'E04')
)
AND A.DEAL = B.DEAL
AND A.FIN = B.FIN
AND A.PKEY >= B.PKEY
GROUP BY A.ENT, A.DEAL, A.FIN, A.DATE1, A.TYPE1, A.STEPNO,
A.CREATE1, A.EVE, A.FEVSTATUS, A.STATUSDATE, A.INTAMT,
A.INTPAID, A.INT_TYPE, A.PENALTY_1, A.CONV_PAID_4,
A.INT_PAID_4, A.CONVFIN_4, A.INTTYPE,
B.DEAL, B.FIN;
I tried to change to below to sum up in sub query and then join, but still the result is taking very long
Changed Query:
SELECT MAX(A.PKEY) PKEY, A.ENT, A.DEAL, A.FIN, A.DATE1, A.TYPE1,
A.STEPNO, A.CREATE1, A.EVE, A.FEVSTATUS, A.STATUSDATE,
A.INTAMT, A.INTPAID, A.INT_TYPE, A.PENALTY_1, A.CONV_PAID_4,
A.INT_PAID_4, A.CONVFIN_4, A.INTTYPE, A.DEAL AS DEAL_B,
A.FIN AS FIN_B,
(
SELECT SUM(B.CONV_PAID_4)
FROM Table1 B
WHERE A.DEAL = B.DEAL
AND A.FIN = B.FIN
AND A.PKEY >= B.PKEY
) AS PRI_CONV_PAID_4_OUT
FROM Table1 A
WHERE A.CREATE1 = '0'
AND (A.FEVSTATUS = '1'
OR A.EVE IN ('E06', 'E07', 'E02', 'E15', 'E03', 'E04')
)
GROUP BY A.ENT, A.DEAL, A.FIN, A.DATE1, A.TYPE1, A.STEPNO,
A.CREATE1, A.EVE, A.FEVSTATUS, A.STATUSDATE, A.INTAMT,
A.INTPAID, A.INT_TYPE, A.PENALTY_1, A.CONV_PAID_4, A.INT_PAID_4,
A.CONVFIN_4, A.INTTYPE, B.DEAL, B.FIN;
Any help in re-framing the query faster?
Below the show create table
CREATE TABLE `Table1` (
`PKEY` DECIMAL(10,0) NOT NULL DEFAULT '0',
`ENT` CHAR(3) NOT NULL DEFAULT '',
`DEAL` CHAR(14) NOT NULL DEFAULT '',
`FIN` CHAR(3) NOT NULL DEFAULT '',
`DATE1` DATETIME NULL DEFAULT NULL,
`TYPE1` CHAR(3) NULL DEFAULT NULL,
`STEPNO` CHAR(3) NULL DEFAULT NULL,
`CREATE1` CHAR(1) NULL DEFAULT NULL,
`EVE` CHAR(3) NULL DEFAULT NULL,
`FEVSTATUS` CHAR(1) NULL DEFAULT NULL,
`STATUSDATE` DATETIME NULL DEFAULT NULL,
`INTAMT` DECIMAL(19,5) NULL DEFAULT NULL,
`INTPAID` DECIMAL(19,5) NULL DEFAULT NULL,
`INT_TYPE` CHAR(1) NULL DEFAULT NULL,
`PENALTY_1` DECIMAL(9,6) NULL DEFAULT NULL,
`CONV_PAID_4` DECIMAL(15,2) NULL DEFAULT NULL,
`CONV_PAID_4` DECIMAL(19,5) NULL DEFAULT NULL,
`CONV_FIN_4` CHAR(3) NULL DEFAULT NULL,
`INTTYPE` CHAR(1) NULL DEFAULT NULL,
PRIMARY KEY (`PKEY`),
UNIQUE INDEX `IXQDWFEV_PK` (`PKEY`),
UNIQUE INDEX `IXIDWFEV` (`ENT`, `DEAL`, `FIN`, `DATE1`),
INDEX `IXQDWFEV_GI1` (`ENT`, `DEAL`, `CONV_FIN_4`),
INDEX `IXQDWFEV_GI2` (`ENT`, `DEAL`, `TYPE1`, `STEPNO`),
INDEX `IDX_GRPBY2` (`DEAL`, `FIN`),
INDEX `IXQDWFEV1_TEMP` (`CREATE1`, `FEVSTATUS`, `EVE`)
)
COLLATE='utf8_general_ci'
;

Unexpected result in SELECT CASE WHEN NULL

Schema and data
I have two tables with the following schema and data:
#table1:
create table #table1(
PK int IDENTITY(1,1) NOT NULL,
[TEXT] nvarchar(50) NOT NULL
);
PK TEXT
1 a
2 b
3 c
4 d
5 e
#table2:
create table #table2(
PK int IDENTITY(1,1) NOT NULL,
FK int NOT NULL,
[TEXT] nvarchar(50) NOT NULL
);
PK FK TEXT
1 2 B
2 3 C
Problem
Now, if I select all from #table1 and left join #table2 like this:
select
#table1.PK,
(case #table2.[TEXT] when NULL then #table1.[TEXT] else #table2.[TEXT] end) as [TEXT]
from
#table1
left join
#table2 on #table2.FK = #table1.PK
;
the output are as following:
PK TEXT
1 NULL
2 B
3 C
4 NULL
5 NULL
Question
I expected the result to be:
PK TEXT
1 a <
2 B
3 C
4 d <
5 e <
So why does this happen (or what am I doing wrong) and how can I fix this?
Source code
if (OBJECT_ID('tempdb..#table1') is not null) drop table #table1;
if (OBJECT_ID('tempdb..#table2') is not null) drop table #table2;
create table #table1(PK int IDENTITY(1,1) NOT NULL, [TEXT] nvarchar(50) NOT NULL);
create table #table2(PK int IDENTITY(1,1) NOT NULL, FK int NOT NULL, [TEXT] nvarchar(50) NOT NULL);
insert into #table1 ([TEXT]) VALUES ('a'), ('b'), ('c'), ('d'), ('e');
insert into #table2 (FK, [TEXT]) VALUES (2, 'B'), (3, 'C');
select
#table1.PK,
(case #table2.[TEXT] when NULL then #table1.[TEXT] else #table2.[TEXT] end) as [TEXT]
from
#table1
left join
#table2 on #table2.FK = #table1.PK
;
drop table #table1;
drop table #table2;
From my perspective this is equivalent of
select isnull(table2.text, table1.text) as text from ...
You should check whether a field is null or not by is null, even though your case when is syntactically correct, you should use the other syntactically correct version.
case
when #table2.[TEXT] is null then #table1.[TEXT]
else #table2.[TEXT]
end
The problem is the way you have constructed your CASE statement. Any CASE statement of the form CASE x WHEN NULL THEN... is not going to behave as you might initially expect as you are effectively performing a comparison with NULL, which is always false, in your case resulting in always getting #table2.[TEXT].
I think you'd need to do:
(CASE WHEN #table2.[TEXT] IS NULL THEN #table1.[TEXT] ELSE #table2.[TEXT] END) AS [TEXT]
which is equivalent to COALESCE:
COALESCE(#table2.[TEXT], #table1.[TEXT]) AS [TEXT]

Digit restriction on SQL server numeric field

How can i restrict field in a table to 15 or 16 digits. I have this table:
create table Person(
,UserID varchar(30)
,Password varchar(30) not null
,CCtype varchar(8)
,CCNumber numeric
,primary key(UserID)
,constraint CK_CCvalidity check
(
(CCType is null or CCNumber is null)
or
(
(CCType = 'Amex' or CCType = 'Discover' or CCType = 'MC' or CCType = 'VISA')
and
(CCNumber >= 15 and CCNumber <= 16)
)
)
);
But this actually checks for the values 15 an 16, not for the number of digits. Also, we can assume that the numeric may hold 000... as the first digits.
Thanks for the help
CCNumber should never be numeric. That will lead to a world of pain.
It should be varchar(X) where X is 13 - 24 digits.
Credit card numbers are usually represented by groups of 4 or 5
digits separated by spaces or dashes or simply all together with no separators.
[note: American Express: 15 digits; Visa: 13 or 16 digits]
In response to your comment:
ALTER TABLE dbo.Person
ADD CONSTRAINT CK_Person_CCNumber
CHECK (LEN(CCNumber) = 16 OR LEN(CCNumber) = 15);
But probably better as:
ALTER TABLE dbo.Person
ADD CONSTRAINT CK_Person_CCNumber
CHECK (LEN(CCNumber) >= 13 AND LEN(CCNumber) <= 15);
AND add a constraint to ensure it is a valid credit card number perhaps (there are plenty of examples online).
Bank Card Number
You can create a function to remove the Non-Numeric characters from a varchar, like this one:
CREATE Function [fnRemoveNonNumericCharacters](#strText VARCHAR(1000))
RETURNS VARCHAR(1000)
AS
BEGIN
WHILE PATINDEX('%[^0-9]%', #strText) > 0
BEGIN
SET #strText = STUFF(#strText, PATINDEX('%[^0-9]%', #strText), 1, '')
END
RETURN #strText
END
Now, if you want to allow only digits and want to check the length, you could add two Check Constraints like this:
Create Table Person
(
Id int not null primary key,
CCNumber varchar(30),
CONSTRAINT CK_Person_CCNumber_Length CHECK (LEN(CCNumber) BETWEEN 15 AND 16),
CONSTRAINT CK_Person_CCNumber_IsNumeric CHECK (LEN(dbo.[fnRemoveNonNumericCharacters](CCNumber)) = LEN(CCNumber))
)
First Constraint will check the length of the field to be 15 or 16.
Second one will check that the field is numeric (length of field removing non-numeric is equal to length of the original field)
You can also do it in just one ANDed Check Constraint.
Storing a credit card number as a...number is guaranteed to shoot you in the foot some day. Such as the day you start encountering credit card numbers with leading zeroes. They may consist of decimal digits, but they're not numbers. They're text.
Plan for the future: what happens when somebody start issuing credit card numbers with letters?
So, try this:
create table dbo.some_table
(
...
credit_card_type varchar(8) null ,
credit_card_number varchar(32) null ,
constraint some_table_ck01 check (
( credit_card_type is not null
and credit_card_number is not null
)
OR ( credit_card_type is null
and credit_card_number is null
)
) ,
constraint some_table_ck02 check (
credit_card_type in ( 'amex' , 'discover' , 'mc' , 'visa' )
) ,
constraint some_table_ck03 check (
credit_card_number not like '%[^0-9]%'
) ,
constraint some_table_ck04 check (
len(credit_card_number) = case credit_card_type
when 'amex' then 15
when 'discover' then 16
when 'mc' then 16
when 'visa' then 16
else -1 -- coerce failure on invalid/unknown type
end
) ,
)
go
insert some_table values( null , null ) -- succeeds
insert some_table values( 'amex' , null ) -- violates check constraint #1
insert some_table values( null , '1' ) -- violates check constraint #1
insert some_table values( 'acme' , '1' ) -- violates check constraint #2
insert some_table values( 'amex' , 'A1B2' ) -- violates check constraint #3
insert some_table values( 'amex' , '12345' ) -- violates check constraint #4
insert some_table values( 'amex' , '123456789012345' ) -- success!
go
But as noted by others, you need to fix your data model. A credit card is a separate entity from a customer. It has a dependent relationship upon the customer (the card's existence is predicated upon the existence of the customer who own it). You can a data model like the following. This
create table credit_card_type
(
int id not null primary key clustered ,
description varchar(32) not null unique ,
... -- other columns describing validation rules here
)
create table credit_card
(
customer_id int not null ,
type int not null ,
number varchar(32) not null ,
expiry_date date not null ,
primary key ( customer_id , number , type , expiry_date ) ,
unique ( number , customer_id , type , expiry_date ) ,
foreign key customer references customer(id) ,
foreign key type references credit_card_type(id) ,
)
Further: you are encrypting card numbers using strong encryption, aren't you?

Are multiple stored procedures necessary here?

I've got a table with a structure like
create table DrugInteractions
(ndc_fk varchar(50) not null
,ndc_pk varchar(50) not null
,InteractionSeverity int not null,
primary key(ndc_fk,ndc_pk)
)
and another with a structure like'
create table DrugList
(char(11) not null
,drug_name varchar(50) not null
,drug_class char(3) not null,
primary key(ndc)
)
There is a 1-many relationship with drug_class and drug_name, but of course a 1-1 relationship between drug_name and drug_class. I currently have three stored procedures which add records to the DrugInteraction table with the following scenarios
All drugs of one class interact with another class
CREATE proc spInsertDrugInteractionsByDrugClass
#referenceClass char(3)
,#interactingClass char(3)
as
begin
;with x
as
(
select distinct
drug_name
from DrugList
where drug_class =#interactingClass
)
,y
as
(
select drug_name
from Druglist
where drug_class = #referenceClass
)
insert into DrugInteractions(ndc_pk,ndc_fk)
select distinct
upper(x.drug_name)
,upper(y.drug_name)
from y,x
end
Sample output from sproc
ndc_fk ncd_pk
drug1 drug2
drug1 drug3
drug1 drug4
Another situation is where there is one 'reference' drug class and other drug names entered as parameters to the sproc that that may or may not be the same drug_class
CREATE proc spInsertDrugInteractionsByClassAndName
#referenceClass char(3)
,#d1 varchar(50) = null
,#d2 varchar(50) = null
,#d3 varchar(50) = null
,#d4 varchar(50) = null
,#d5 varchar(50) = null
as begin
;with refClassDrugNames
as
(
select distinct upper(drug_name) as drug_name
from DrugList
where drug_class = #referenceClass
),
drugName
as
(
select distinct upper(drug_name) as drug_name
from DrugList
where drug_name in (#d1,#d2,#d3,#d4,#d5)
)
insert into DrugInteractions(ndc_pk,ndc_fk)
select * from refClassDrugNames,drugName
end
Sample output from this sproc would be:
ndc_pk ndc_fk
drug1 refDrug
drug2 refDrug
drug3 refDrug
drug4 refDrug
for situations such as this, is it good/bad design to have multiple stored procedures? I can't help but think there's got to be a way to make one sproc cover these two scenarios. Can something akin to this be done in SQL Server only?