SQL Query Dynamically get columns depending on mapping - sql

I am stuck on an issue. I am trying to create a select which returns information depending on the Rules Mapping table. The Rules table tells us where (TableName.ColumnName) value should come from. I have simplified the problem by just creating table variables to find a solution.
I have 3 tables set up.
#TableA which contains the Main_ID. First Table.
#TableB which contains more information for the Main_ID. Second table.
#Rules is the mapping table. It has headers for what the final select should have and also tells us where the the data should come from (What table and column).
The final expected result I am trying to achieve is this with the above rules is:
As you can see, I will have 3 columns in total with 3 rows (as I have 3 Main_ID). Data differs depending on the #Rules table. For example, Header C for Main_ID 3 is showing "Address C" as the rule mapping for Header C Main_ID 3 was "#TableB.Address" which means get the information from table #TableB and column Address.
Here is my code for the table set up:
DECLARE #TableA AS TABLE
(
Main_ID INT IDENTITY(1, 1) ,
Name VARCHAR(100)
)
INSERT INTO #TableA
VALUES ('Name A'), ('Name B'), ('Name C')
DECLARE #TableB AS TABLE
(
Secondary_ID INT IDENTITY(1, 1) ,
Main_ID INT ,
Address VARCHAR(100)
)
INSERT INTO #TableB
VALUES (1, 'Address A'), (2, 'Address B'), (3, 'Address C')
DECLARE #Rules AS TABLE
(
Rule_ID INT IDENTITY(1, 1) ,
Main_ID INT ,
Header_Name VARCHAR(100) ,
Obtain_From VARCHAR(100)
)
INSERT INTO #Rules
VALUES
-- Main_ID 1 AND 2 set up same but third one wants data from a different place.
(1, 'Header A', '#TableA.Main_ID'), (1, 'Header B', '#TableB.Address'), (1, 'Header C', '#TableA.Name'),
(2, 'Header A', '#TableA.Main_ID'), (2, 'Header B', '#TableB.Address'), (2, 'Header C', '#TableA.Name'),
(3, 'Header A', '#TableA.Main_ID'), (3, 'Header B', '#TableA.Name'), (3, 'Header C', '#TableB.Address')
SELECT * FROM #TableA
SELECT * FROM #TableB
SELECT * FROM #Rules
/*
Final result set should be:
Header A Header B Header C
1 Address A Name A
2 Address B Name B
3 Name C Address C
*/
What I have tried so far is joining the table together but there are too many rows.
Any ideas how I can achieve the final expected result?

There is a two way to do it.
The first way is static and easy.
You can use CASE expression for your mapping like that:
SELECT
[Header A] = CASE r_headera.Obtain_From WHEN '#TableA.Main_ID' THEN CAST(a.Main_ID AS NVARCHAR(100)) WHEN '#TableA.Name' THEN CAST(a.Name AS NVARCHAR(100)) WHEN '#TableB.Address' THEN CAST(b.Address AS NVARCHAR(100)) END,
[Header B] = CASE r_headerb.Obtain_From WHEN '#TableA.Main_ID' THEN CAST(a.Main_ID AS NVARCHAR(100)) WHEN '#TableA.Name' THEN CAST(a.Name AS NVARCHAR(100)) WHEN '#TableB.Address' THEN CAST(b.Address AS NVARCHAR(100)) END,
[Header C] = CASE r_headerc.Obtain_From WHEN '#TableA.Main_ID' THEN CAST(a.Main_ID AS NVARCHAR(100)) WHEN '#TableA.Name' THEN CAST(a.Name AS NVARCHAR(100)) WHEN '#TableB.Address' THEN CAST(b.Address AS NVARCHAR(100)) END
FROM
#TableA AS a
LEFT JOIN #TableB AS b ON b.Main_ID = a.Main_ID
LEFT JOIN #Rules AS r_headera ON r_headera.Main_ID = a.Main_ID AND r_headera.Header_Name = 'Header A'
LEFT JOIN #Rules AS r_headerb ON r_headerb.Main_ID = a.Main_ID AND r_headerb.Header_Name = 'Header B'
LEFT JOIN #Rules AS r_headerc ON r_headerc.Main_ID = a.Main_ID AND r_headerc.Header_Name = 'Header C'
Otherwise, you can use other structs such as Table Value Constructor, PIVOT, OUTER APPLY etc.
The result:
Header A
Header B
Header C
1
Address A
Name A
2
Address B
Name B
3
Name C
Address C
And the second way is Dynamic SQL with sp_executesql .
If you need help with that, I can prepare an example for you.

Related

Find data by multiple Lookup table clauses

declare #Character table (id int, [name] varchar(12));
insert into #Character (id, [name])
values
(1, 'tom'),
(2, 'jerry'),
(3, 'dog');
declare #NameToCharacter table (id int, nameId int, characterId int);
insert into #NameToCharacter (id, nameId, characterId)
values
(1, 1, 1),
(2, 1, 3),
(3, 1, 2),
(4, 2, 1);
The Name Table has more than just 1,2,3 and the list to parse on is dynamic
NameTable
id | name
----------
1 foo
2 bar
3 steak
CharacterTable
id | name
---------
1 tom
2 jerry
3 dog
NameToCharacterTable
id | nameId | characterId
1 1 1
2 1 3
3 1 2
4 2 1
I am looking for a query that will return a character that has two names. For example
With the above data only "tom" will be returned.
SELECT *
FROM nameToCharacterTable
WHERE nameId in (1,2)
The in clause will return every row that has a 1 or a 3. I want to only return the rows that have both a 1 and a 3.
I am stumped I have tried everything I know and do not want to resort to dynamic SQL. Any help would be great
The 1,3 in this example will be a dynamic list of integers. for example it could be 1,3,4,5,.....
Filter out a count of how many times the Character appears in the CharacterToName table matching the list you are providing (which I have assumed you can convert into a table variable or temp table) e.g.
declare #Character table (id int, [name] varchar(12));
insert into #Character (id, [name])
values
(1, 'tom'),
(2, 'jerry'),
(3, 'dog');
declare #NameToCharacter table (id int, nameId int, characterId int);
insert into #NameToCharacter (id, nameId, characterId)
values
(1, 1, 1),
(2, 1, 3),
(3, 1, 2),
(4, 2, 1);
declare #RequiredNames table (nameId int);
insert into #RequiredNames (nameId)
values
(1),
(2);
select *
from #Character C
where (
select count(*)
from #NameToCharacter NC
where NC.characterId = c.id
and NC.nameId in (select nameId from #RequiredNames)
) = 2;
Returns:
id
name
1
tom
Note: Providing DDL+DML as shown here makes it much easier for people to assist you.
This is classic Relational Division With Remainder.
There are a number of different solutions. #DaleK has given you an excellent one: inner-join everything, then check that each set has the right amount. This is normally the fastest solution.
If you want to ensure it works with a dynamic amount of rows, just change the last line to
) = (SELECT COUNT(*) FROM #RequiredNames);
Two other common solutions exist.
Left-join and check that all rows were joined
SELECT *
FROM #Character c
WHERE EXISTS (SELECT 1
FROM #RequiredNames rn
LEFT JOIN #NameToCharacter nc ON nc.nameId = rn.nameId AND nc.characterId = c.id
HAVING COUNT(*) = COUNT(nc.nameId) -- all rows are joined
);
Double anti-join, in other words: there are no "required" that are "not in the set"
SELECT *
FROM #Character c
WHERE NOT EXISTS (SELECT 1
FROM #RequiredNames rn
WHERE NOT EXISTS (SELECT 1
FROM #NameToCharacter nc
WHERE nc.nameId = rn.nameId AND nc.characterId = c.id
)
);
A variation on the one from the other answer uses a windowed aggregate instead of a subquery. I don't think this is performant, but it may have uses in certain cases.
SELECT *
FROM #Character c
WHERE EXISTS (SELECT 1
FROM (
SELECT *, COUNT(*) OVER () AS cnt
FROM #RequiredNames
) rn
JOIN #NameToCharacter nc ON nc.nameId = rn.nameId AND nc.characterId = c.id
HAVING COUNT(*) = MIN(rn.cnt)
);
db<>fiddle

SQL Query Get the Next ID

I am working on a problem where I need to get the next available Category for a gamer. If the gamer has reached the final Category (Shooter in below image) then I will need to go back to the start of the table and go to the next one available.
Below is a list of Category:
Below is a list of gamers and what their last Category_ID was:
Finally, below is the data I will be using to work with
Now I need to find the next Category_ID for each gamer using the data table. If a Gamer has reached the last Category_ID (ID 4 in this case and Gamer C) then it will reset back to top like 1 however Gamer C will pick Category 2 as there is no one for that Gamer.
How can I pick the next Category available using the data table for each gamer while keeping in mind of resetting back to top if reached end?
Below is a code example of the problem:
DECLARE #Category AS TABLE
(
Category_ID INT IDENTITY(1,1) ,
Description VARCHAR(255)
)
INSERT INTO #Category
VALUES('Sports'), ('Adventure'), ('Action'), ('Shooter')
DECLARE #Gamer AS TABLE
(
Username VARCHAR(255) ,
Last_Category_ID INT
)
INSERT INTO #Gamer
VALUES('Gamer A', 1), ('Gamer B', 2), ('Gamer C', 4), ('Gamer D', 3)
DECLARE #Data AS TABLE
(
Username VARCHAR(255) ,
Play_At DATETIME ,
Category_ID INT
)
INSERT INTO #Data
VALUES('Gamer A', GETDATE() -1, 1), ('Gamer B', GETDATE() -1, 2), ('Gamer A', GETDATE() -1, 3), ('Gamer D', GETDATE() -1, 3), ('Gamer C', GETDATE() -1, 2)
SELECT * FROM #Category
SELECT * FROM #Gamer
SELECT * FROM #Data
If I understand correctly, you want the next unused category id for each gamer. And then to start over when all are done.
Here is an approach:
For each gamer, get the maximum current category.
For each category, get the next category.
Get the first category as well.
When there is no next category use the first.
This is a good opportunity to use lateral joins:
select g.*, coalesce(cn.category_id, cf.category_id)
from #gamers g outer apply
(select max(category_id) as max_category_id
from #data d
where d.username = g.username
) c outer apply
(select top (1) cn.category_id
from #categories cn
where cn.categoryid > c.max_categoryid
) cn cross join
(select min(cf.category_id) as categoryi_d
from #categories cf
) cf;
Here is a db<>fiddle.
I'm not 100% clear on if your #Data table represents the starting category however this might be what you need, if not by all means please clarify with some clear expected results for the sample data.
Using an apply to determine the starting category for each user (if indeed that's what your description entails) and another to get the maximum possible category, select the username and, using a case expression, the corresponding next category name or the starting category if it's already the max category.
select
Username,
( select description
from #category c
where c.category_Id=case when Last_Category_ID<m.Id then Last_Category_ID + 1 else startcat.Id end
) NextCategory
from #gamer g
outer apply (
select min(category_id) Id
from #data d
where d.username=g.Username
)startcat
outer apply (
select Max(category_Id) Id
from #category
)m

SQL: JOIN with 'near' match

I need to do a JOIN with a 'near match'. The best way to explain this is with an example:
CREATE TABLE Car
(
Vin int,
Make nvarchar(50),
ColorID int,
)
CREATE TABLE Color
(
ColorID int,
ColorCode nvarchar(10)
)
CREATE TABLE ColorName
(
ColorID int,
Languagecode varchar(12),
ColorName nvarchar(50)
)
INSERT INTO Color Values (1, 'RED CODE')
INSERT INTO Color Values (2, 'GREEN CODE')
INSERT INTO Color Values (3, 'BLUE CODE')
INSERT INTO ColorName Values (1, 'en', 'Red')
INSERT INTO ColorName Values (1, 'en-US', 'Red, my friend')
INSERT INTO ColorName Values (1, 'en-GB', 'Red, my dear')
INSERT INTO ColorName Values (1, 'en-AU', 'Red, mate')
INSERT INTO ColorName Values (1, 'fr', 'Rouge')
INSERT INTO ColorName Values (1, 'fr-BE', 'Rouge, mon ami')
INSERT INTO ColorName Values (1, 'fr-CA', 'Rouge, mon chum')
INSERT INTO Car Values (123, 'Honda', 1)
The SPROC would look like this:
DECLARE #LanguageCode varchar(12) = 'en-US'
SELECT * FROM Car A
JOIN Color B ON (A.ColorID = B.ColorID)
LEFT JOIN ColorName C ON (B.ColorID = C.ColorID AND C.LanguageCode = #LanguageCode)
See http://sqlfiddle.com/#!6/ac24d/24 (thanks to Jake!)
Here is the challenge:
When the SPROC parameter #LanguageCode is an exact match, all is well.
I would like for it to also work for partial matches; more specifically: say for example that #LanguageCode would be 'en-NZ' then I would like the SPROC to return the value for language code 'en' (since there is no value for 'en-NZ').
As an extra challenge: if there is no match at all I would like to return the 'en' value; for example if #LanguageCode would be 'es' then the SPROC would return the 'en' value (since there is no value for 'es').
Try left(#LanguageCode, 2) + '%'
http://sqlfiddle.com/#!6/ac24d/26
About second part - you have to query table two times anyway (you can do it in one statement, but if will be like two statements in one). You also can insert data into temporary (or variable) table, check if there's no rows and then make another query
I've made a query with table function
http://sqlfiddle.com/#!6/b7be3/5
So you can write
DECLARE #LanguageCode varchar(12) = 'es'
if not exists (select * from sf_test(#LanguageCode))
select * from sf_test('en')
else
select * from sf_test(#LanguageCode)
you also can write
declare #temp table
(
Vin int,
Make nvarchar(50),
ColorCode nvarchar(10)
)
insert into #temp
select * from sf_test(#LanguageCode)
if not exists (select * from #temp)
select * from sf_test('en')
else
select * from #temp
As #Roman Pekar has said in his comment, this can indeed be done, including your additional request about falling back to en, in one statement with the help of a ranking function. Here's how you could go about it:
WITH FilteredAndRanked AS (
SELECT
*,
rnk = ROW_NUMBER() OVER (
PARTITION BY ColorID
ORDER BY CASE LanguageCode
WHEN #LanguageCode THEN 1
WHEN LEFT(#LanguageCode, 2) THEN 2
WHEN 'en' THEN 3
END
)
FROM ColorName
WHERE LanguageCode IN (
#LanguageCode,
LEFT(#LanguageCode, 2),
'en'
)
)
SELECT
...
FROM Car A
INNER JOIN Color B ON (A.ColorID = B.ColorID)
LEFT JOIN FilteredAndRanked C ON (B.ColorID = C.ColorID AND C.rnk = 1)
;
That is, the ColorName table is filtered and ranked before being used in the query, and then only the rows with the rankings of 1 are joined:
The filter for ColorName includes only rows with LanguageCode values of #LanguageCode, LEFT(#LanguageCode, 2) and 'en'.
The ranking values are assigned based on which language code each row contains: rows with LEFT(#LanguageCode, 2) are ranked after those with #LanguageCode but before the 'en' ones.

How do I join an unknown number of rows to another row?

I have this scenario:
Table A:
---------------
ID| SOME_VALUE|
---------------
1 | 123223 |
2 | 1232ff |
---------------
Table B:
------------------
ID | KEY | VALUE |
------------------
23 | 1 | 435 |
24 | 1 | 436 |
------------------
KEY is a reference to to Table A's ID. Can I somehow join these tables so that I get the following result:
Table C
-------------------------
ID| SOME_VALUE| | |
-------------------------
1 | 123223 |435 |436 |
2 | 1232ff | | |
-------------------------
Table C should be able to have any given number of columns depending on how many matching values that are found in Table B.
I hope this enough to explain what I'm after here.
Thanks.
You need to use a Dynamic PIVOT clause in order to do this.
EDIT:
Ok so I've done some playing around and based on the following sample data:
Create Table TableA
(
IDCol int,
SomeValue varchar(50)
)
Create Table TableB
(
IDCol int,
KEYCol int,
Value varchar(50)
)
Insert into TableA
Values (1, '123223')
Insert Into TableA
Values (2,'1232ff')
Insert into TableA
Values (3, '222222')
Insert Into TableB
Values( 23, 1, 435)
Insert Into TableB
Values( 24, 1, 436)
Insert Into TableB
Values( 25, 3, 45)
Insert Into TableB
Values( 26, 3, 46)
Insert Into TableB
Values( 27, 3, 435)
Insert Into TableB
Values( 28, 3, 437)
You can execute the following Dynamic SQL.
declare #sql varchar(max)
declare #pivot_list varchar(max)
declare #pivot_select varchar(max)
Select
#pivot_list = Coalesce(#Pivot_List + ', ','') + '[' + Value +']',
#Pivot_select = Coalesce(#pivot_Select, ', ','') +'IsNull([' + Value +'],'''') as [' + Value + '],'
From
(
Select distinct Value From dbo.TableB
)PivotCodes
Set #Sql = '
;With p as (
Select a.IdCol,
a.SomeValue,
b.Value
From dbo.TableA a
Left Join dbo.TableB b on a.IdCol = b.KeyCol
)
Select IdCol, SomeValue ' + Left(#pivot_select, Len(#Pivot_Select)-1) + '
From p
Pivot ( Max(Value) for Value in (' + #pivot_list + '
)
)as pvt
'
exec (#sql)
This gives you the following output:
Although this works at the moment it would be a nightmare to maintain. I'd recommend trying to achieve these results somewhere else. i.e not in SQL!
Good luck!
As Barry has amply illustrated, it's possible to get multiple columns using a dynamic pivot.
I've got a solution that might get you what you need, except that it puts all of the values into a single VARCHAR column. If you can split those results, then you can get what you need.
This method is a trick in SQL Server 2005 that you can use to form a string out of a column of values.
CREATE TABLE #TableA (
ID INT,
SomeValue VARCHAR(50)
);
CREATE TABLE #TableB (
ID INT,
TableAKEY INT,
BValue VARCHAR(50)
);
INSERT INTO #TableA VALUES (1, '123223');
INSERT INTO #TableA VALUES (2, '1232ff');
INSERT INTO #TableA VALUES (3, '222222');
INSERT INTO #TableB VALUES (23, 1, 435);
INSERT INTO #TableB VALUES (24, 1, 436);
INSERT INTO #TableB VALUES (25, 3, 45);
INSERT INTO #TableB VALUES (26, 3, 46);
INSERT INTO #TableB VALUES (27, 3, 435);
INSERT INTO #TableB VALUES (28, 3, 437);
SELECT
a.ID
,a.SomeValue
,RTRIM(bvals.BValues) AS ValueList
FROM #TableA AS a
OUTER APPLY (
-- This has the effect of concatenating all of
-- the BValues for the given value of a.ID.
SELECT b.BValue + ' ' AS [text()]
FROM #TableB AS b
WHERE a.ID = b.TableAKEY
ORDER BY b.ID
FOR XML PATH('')
) AS bvals (BValues)
ORDER BY a.ID
;
You'll get this as a result:
ID SomeValue ValueList
--- ---------- --------------
1 123223 435 436
2 1232ff NULL
3 222222 45 46 435 437
This looks like something a database shouldn't do. Firstly; a table cannot have arbitrary number of columns depending on whatever you'll store. So you will have to put up a maximum number of values anyway. You can get around this by using comma seperated values as value for that cell (or a similar pivot-like solution).
However; if you do have table A and B; i recommend keeping to those two tables; as they seem to be pretty normalised. Should you need a list of b.value given an input a.some_value, the following sql query gives that list.
select b.value from a,b where b.key=a.id a.some_value='INPUT_VALUE';

How to select a value in the same table as the value for an update for each row

I have a table structure with columns like this
[ID]
[Name]
[ParentId]
[ParentName]
The parents are contained in the same table, and i would like to populate the parent name column using a statement like:
UPDATE Table
SET ParentName = (select Name
from Table
where Id = ParentId)
When i do this, all the ParentNames are set to null. Thoughts?
I would go with the update from statement.
UPDATE tb
SET
tb.ParentName = parent.Name
FROM Table tb
INNER JOIN Table parent ON parent.Id = tb.ParentId
This is T-SQL specific, but it should work pretty well.
Here's another T-SQL syntax you can use :
(BTW, I agree with cletus about the denormalization concerns.)
-- create dummy table
create table test (id int, name varchar(20),
parentid int, parentname varchar(20))
go
-- add some rows
insert test values (1, 'parent A', null, null)
insert test values (2, 'parent B', null, null)
insert test values (3, 'parent C', null, null)
insert test values (11, 'child A 1', 1, null)
insert test values (12, 'child A 2', 1, null)
insert test values (33, 'child C 1', 3, null)
go
-- perform update
update c set parentname = p.name from test c join test p on c.parentid = p.id
go
-- check result
select * from test
Here is a solution that I have working
UPDATE TABLE
SET ParentName = b.Name from
(
select t.name as name, t.id as id
from TABLE t
) b
where b.id = parentid
Note I refuse to believe that it has to be this ugly, I'm sure that something very similar to what OMG Ponies posted should work but try as I might I couldn't make it happen.
Here , sub query returning null values, So that it is assigning null to ParentName
UPDATE
T
SET
parentname = PT.name
FROM
MyTable T
JOIN
MyTable PT ON t.parentid = PT.id
You error occurs becasue you have no correlation in the subquery. You get zero rows unless "Id = ParentId" in each row
select Name from Table where Id = ParentId -- = no rows
You can't use an alias like UPDATE TABLE T ... so push the JOIN/correlation into the FROM clause (or a CTE or derived table)