Unexpected result in SELECT CASE WHEN NULL - sql

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]

Related

Nested while loop in SQL Server is not showing the expected result

I am trying to connect records from two different tables so I can display the data in a tabular format in an SSRS tablix.
The code below does not return the expected results.
As is, for each item in Temp_A the loop updates everything with the last item in Temp_C. Here is the code:
CREATE TABLE #Temp_A
(
[ID] INT,
[Name] VARCHAR(255)
)
INSERT INTO #Temp_A ([ID], [Name])
VALUES (1, 'A'), (2, 'B')
CREATE TABLE #Temp_C
(
[ID] INT,
[Name] VARCHAR(255)
)
INSERT INTO #Temp_C ([ID], [Name])
VALUES (1, 'C'), (2, 'D')
CREATE TABLE #Temp_Main
(
[Temp_A_ID] INT,
[Temp_A_Name] VARCHAR(255),
[Temp_C_ID] INT,
[Temp_C_Name] VARCHAR(255),
)
DECLARE #MIN_AID int = (SELECT MIN(ID) FROM #Temp_A)
DECLARE #MAX_AID int = (SELECT MAX(ID) FROM #Temp_A)
DECLARE #MIN_DID int = (SELECT MIN(ID) FROM #Temp_C)
DECLARE #MAX_DID int = (SELECT MAX(ID) FROM #Temp_C)
WHILE #MIN_AID <= #MAX_AID
BEGIN
WHILE #MIN_DID <= #MAX_DID
BEGIN
INSERT INTO #Temp_Main([Temp_A_ID], [Temp_A_Name])
SELECT ID, [Name]
FROM #Temp_A
WHERE ID = #MIN_AID
UPDATE #Temp_Main
SET [Temp_C_ID] = ID, [Temp_C_Name] = [Name]
FROM #Temp_C
WHERE ID = #MIN_DID
SET #MIN_DID = #MIN_DID + 1
END
SET #MIN_AID = #MIN_AID + 1
SET #MIN_DID = 1
END
SELECT * FROM #Temp_Main
DROP TABLE #Temp_A
DROP TABLE #Temp_C
DROP TABLE #Temp_Main
Incorrect result:
Temp_A_ID | Temp_A_Name | Temp_C_ID | Temp_C_Name
----------+-------------+-----------+---------------
1 A 2 D
1 A 2 D
2 B 2 D
2 B 2 D
Expected results:
Temp_A_ID | Temp_A_Name | Temp_C_ID | Temp_C_Name
----------+-------------+-----------+---------------
1 A 1 C
1 A 2 D
2 B 1 C
2 B 2 D
What am I missing?
You seem to want a cross join:
select a.*, c.*
from #Temp_A a cross join
#Temp_C c
order by a.id, c.id;
Here is a db<>fiddle.
There is no need to write a WHILE loop to do this.
You can use insert to insert this into #TempMain, but I don't se a need to have a temporary table for storing the results of this query.

SQL insert data dynamic column name from another table

i'm trying to insert data from one table to another with dynamic column name from #array to #array2
error
The multi-part identifier "s.id" could not be bound.
SQL CODE:
DECLARE #Array TABLE
(
id int not null,
dt varchar(12) not null,
ld varchar(16) not null,
val varchar(12) not null,
ty varchar(4) not null,
PRIMARY KEY CLUSTERED (id,dt)
)
DECLARE #Array2 TABLE
(
id int not null,
dt varchar(12) not null,
ld varchar(16) not null,
min varchar(12) null,
mout varchar(4) null,
PRIMARY KEY CLUSTERED (id,dt)
)
INSERT INTO #Array VALUES
('1','2015-11-11','2015-11-11','20:08','min')
,('2','2015-11-11','2015-11-11','20:08','mout')
,('3','2015-11-11','2015-11-11','20:08','min')
,('4','2015-11-11','2015-11-11','20:08','min')
Select * from #Array s
WHERE NOT EXISTS (select s.id,s.dt,s.ld,s.ty from #Array2
WHERE id != s.id AND dt != s.dt)
INSERT INTO #Array2 (id,dt,ld,s.ty) VALUES(s.id,s.dt,s.ld,s.val)
^
dynamic column name from #Array TABLE
here is SQL Fiddle link, thanks.
I would re-write your insert along the lines of:
INSERT INTO #Array2 (id,dt,ld,s.ty)
Select s.id,s.dt,s.ld,s.ty from #Array s
left join #Array2 a2 on a2.id = s.id
where a2.id is null
Your error is coming from the fact that Array2 doesn't have a ty column defined. The fix there is to either put it in there or re-evaluate what you are putting into it. Also, thumbs up for the fiddle link :)
EDIT:
On second reading of your question, do you want to dynamically add that column to array2? If so, then that would require quite a bit of mucking around, and I would try to find another solution. Changing your schema like that on the fly is ill-advised.
EDIT2:
INSERT INTO #Array2 (id,dt,ld,min,mout)
Select
s.id,
s.dt,
s.ld,
case s.ty when 'min' then s.val else '' end,
case s.ty when 'mout' then s.val else '' end
from #Array s
left join #Array2 a2 on a2.id = s.id
where a2.id is null
EDIT3
UPDATE a2
SET
a2.dt = s.dt,
a2.ld = s.ld,
a2.min = case s.ty when 'min' then s.val else '' end,
a2.mout = case s.ty when 'mout' then s.val else '' END
FROM #Array2 a2
LEFT JOIN #Array s ON a2.id = s.id
WHERE s.id IS NOT null

Insert - Select keeping identity mapping

I have 2 tables, and im trying to insert data from one to another and keepeng the mappings between ids.
I found here someone with the same problem, but the solution isnt good for me.
here is the example:
the two tables
CREATE TABLE [source] (i INT identity PRIMARY KEY, some_value VARCHAR(30))
CREATE TABLE [destination] (i INT identity PRIMARY KEY, some_value VARCHAR(30))
CREATE TABLE [mapping] (i_old INT, i_new INT) -- i_old is source.i value, i_new is the inserted destination.i column
some sample data
INSERT INTO [source] (some_value)
SELECT TOP 30 name
FROM sysobjects
INSERT INTO [destination] (some_value)
SELECT TOP 30 name
FROM sysobjects
Here, i want to transfer everything from source into destination, but be able to keep a mapping on the two tables:
I try to use OUTPUT clause, but i cannot refer to columns outside of the ones being inserted:
INSERT INTO [destination] (some_value)
--OUTPUT inserted.i, s.i INTO [mapping] (i_new, i_old) --s.i doesn't work
SELECT some_value
FROM [source] s
Anyone has a solution for this?
Not sure is it write way but it works :D
MERGE [#destination] AS D
USING [#source] AS s
ON s.i <> s.i
WHEN NOT MATCHED BY TARGET
THEN
INSERT (some_value) VALUES (some_value)
OUTPUT inserted.i, s.i INTO [#mapping] (i_new, i_old);
try this sql below if you don't have permission to modify the tables:
The idea is using a temp table to be a bridge between destination table and the mapping table.
SQL Query:
declare #source table (i INT identity PRIMARY KEY, some_value VARCHAR(30))
declare #destination table (i INT identity PRIMARY KEY, some_value VARCHAR(30))
declare #mapping table (i_old INT, i_new INT) -- i_old is source.i value, i_new is the inserted destination.i column
declare #tempSource table
(
id_source INT identity , source_value VARCHAR(30)
,Id_New int,source_new VARCHAR(30)
)
insert into #source
output inserted.i, inserted.some_value into #tempSource(id_source,source_value)
SELECT TOP 10 name
FROM sysobjects
--select * from #tempsource
insert into #destination
OUTPUT inserted.i, inserted.some_value INTO #tempSource (Id_New,source_new)
select source_value from #tempSource
insert into #mapping
select Id_source, Id_New from
(
select a.id_source, a.source_value
from
#tempSource a
where id_source is not null and source_value is not null
) aa
inner join
(
select a.Id_New, a.source_new
from
#tempSource a
where Id_New is not null and source_new is not null
) bb on aa.source_value = bb.source_new
select * from #mapping
The mapping table result:
i_old i_new
----------- -----------
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 10

SQL query to show repeating data from child records in columns

I have the following tables in a SQL Server 2000 database:
Master
MasterID | Details | [other fields]
=====================================
PK (int) | Free text | ...
LogTable
LogID | MasterID | UserID | LogDate | LogText
==========================================================
PK (int) | FK (int) | VarChar(2)| Date stamp | Free text
There may be many Log entries for each master record.
I have a query which extracts the most recent three associated Log entries for each Master row as shown below. Note that appropriate conversion and formatting is performed to achieve the LogData concatenation (omitted for clarity):
SELECT
M.MasterID, M.Details, L.LogDate + L.UserID + L.LogText AS LogData
FROM
MasterTable M
INNER JOIN
LogTable L ON M.MasterID = L.MasterID
AND L.LogID IN (SELECT TOP 3 LogID FROM LogTable
WHERE MasterID = M. MasterID ORDER BY LogDate DESC)
This produces output like this:
MasterID | Details | LogData
========================================================
1 | First | 05/11/2012 AB Called Client
2 | Second | 08/11/2012 CD Client Visit
2 | Second | 07/11/2012 CD Called Client
2 | Second | 05/11/2012 AB Called Client
What I need to achieve is showing the data from the second table as columns in the output, all reported against each single master record, thus avoiding repeated data. Like so:
MasterID | Details | LogData1 | LogData2 | LogData3
===========================================================================================================
1 | First | 05/11/2012 AB Called Client | (null) | (null)
2 | Second | 08/11/2012 CD Client Visit | 07/11/2012 CD Called Client | 05/11/2012 AB Called Client
Note that in the real world requirement, this solution will be part of flattening 5 tables with the output consisting of approx 20,000 rows and 90 columns of data.
Thanks in advance.
I'm going to post this, just to show it can be done, but HIGHLY SUGGEST, not do it through SQL. Should be done through the UI that's displaying to be more dynamic on your columns. Even then, I would design this differently.
-- create master table
DECLARE #MasterTable TABLE (
[MasterID] [int] IDENTITY (1, 1) NOT NULL ,
[Details] [varchar] (50) ,
[AdditionalField_1] [varchar] (50) ,
[AdditionalField_n] [varchar] (50)
)
-- create log table
DECLARE #LogTable TABLE (
[LogID] [int] IDENTITY (1, 1) NOT NULL ,
[MasterID] [int] NULL ,
[UserID] [varchar] (2) ,
[LogDate] [datetime] NULL ,
[LogText] [varchar] (50)
)
-- insert into master table
INSERT INTO #MasterTable (Details)
VALUES ('First')
INSERT INTO #MasterTable (Details)
VALUES ('Second')
-- insert into log table
INSERT INTO #LogTable (MasterID, UserID, LogDate, LogText)
VALUES (1, 'AB', '05/11/2012', 'Called Client')
INSERT INTO #LogTable (MasterID, UserID, LogDate, LogText)
VALUES (2, 'AB', '05/11/2012', 'Called Client')
INSERT INTO #LogTable (MasterID, UserID, LogDate, LogText)
VALUES (2, 'CD', '07/11/2012', 'Called Client')
INSERT INTO #LogTable (MasterID, UserID, LogDate, LogText)
VALUES (2, 'CD', '08/11/2012', 'Client Visit')
-- create table to display data
DECLARE #MyTemp TABLE (MasterID INT, Details VARCHAR(50), LogData1 VARCHAR(50), LogData2 VARCHAR(50), LogData3 VARCHAR(50))
INSERT INTO #MyTemp SELECT MasterID, Details, NULL, NULL, NULL FROM #MasterTable
-- create vars
DECLARE #ID INT, #NewID INT, #MasterID INT, #NewValue VARCHAR(100)
SET #ID = 0
-- loop through data
WHILE #ID >-1
BEGIN
-- clear vars
SELECT #NewID = NULL, #MasterID = NULL, #NewValue = NULL
-- get first record
SELECT TOP 1
#NewValue = CONVERT(VARCHAR(10), LogDate, 103)+ ' ' + UserID + ': ' + LogText
, #MasterID=MasterID
, #NewID=LogID
FROM #LogTable WHERE LogID>#ID
-- if no data, exit loop
IF #NewID IS NULL
BREAK
-- update record based on valuds in fields
UPDATE m
SET #ID = #NewID
, LogData1 = (CASE WHEN m.LogData1 IS NULL THEN #NewValue ELSE m.LogData1 END)
, LogData2 = (CASE WHEN m.LogData1 IS NOT NULL THEN
(CASE WHEN m.LogData2 IS NULL THEN #NewValue ELSE m.LogData2 END)
ELSE m.LogData2 END)
, LogData3 = (CASE WHEN m.LogData1 IS NOT NULL THEN
(CASE WHEN m.LogData2 IS NOT NULL THEN
(CASE WHEN m.LogData3 IS NULL THEN #NewValue ELSE m.LogData3 END)
ELSE m.LogData3 END)
ELSE m.LogData3 END)
FROM #MyTemp m
WHERE m.MasterID=#MasterID
END
--display all data
SELECT * FROM #MyTemp

TSQL CASE on Multiple columns

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;