I need to use the rows of one column to select another column - tsql

I have as selection of Columns called Parameter(1 to 10) and I need the row select to join on the column of another table, depending on the Matrix selected. This needs to be driven by the matrix selected. The rows to columns heading is a 1-1 mapping, but the values are 1 to many. Like below:
Matrix 1 - Name, Parameter1 = First Name, Parameter2 = Last Name
Matrix 2 - Location, Parameter1 = City, Parameter2 = State, Parameter3 = Country
All the data a held in the table client detail table. The base table is over 50 columns long and there are over 100 different parameters, along with 60+ matrix possibilities.
Example - Base Table
First Name
Last Name
City
State
Country
Mary
Smith
Austin
Texas
USA
Steven
Johnson
Toronto
Ontario
Canada
Matrix
Parameter1
Parameter2
Parameter3
1
City
State
Country
2
First Name
Last Name
I need the outputs to be like :
Output A
When Matrix 1 is selected
City
State
Country
Austin
Texas
USA
Toronto
Ontario
Canada
Output B
When Matrix 2 selected
First Name
Last Name
Mary
Smith
Steven
Johnson

I believe you can do what you want using dynamic SQL that builds up the query and the variable conditions into a SQL string and then executes the query using sp_ExecuteSql.
Something like:
DECLARE #ConditionTemplate VARCHAR(MAX) = 'B.<ColumnName> = #Parameter<ParameterId>'
DECLARE #ConditionSeparator VARCHAR(50) = '
AND '
DECLARE #Conditions NVARCHAR(MAX) = (
SELECT STRING_AGG(REPLACE(REPLACE(
#ConditionTemplate
, '<ColumnName>', QUOTENAME(P.ColumnName))
, '<ParameterId>', CAST(P.ParameterId AS VARCHAR))
, #ConditionSeparator)
FROM Matrix M
CROSS APPLY (
VALUES
(1, M.Parameter1),
(2, M.Parameter2),
(3, M.Parameter3)
) P(ParameterId, ColumnName)
WHERE M.MatrixId = #MatrixId
AND P.ColumnName > ''
)
DECLARE #Sql NVARCHAR(MAX) = '
SELECT *
FROM BaseTable B
WHERE '
+ #Conditions
DECLARE #Params NVARCHAR(MAX) = N'#Parameter1 VARCHAR(100), #Parameter2 VARCHAR(100), #Parameter3 VARCHAR(100)'
PRINT #Sql
EXEC sp_ExecuteSql #Sql, #Params, #Parameter1, #Parameter2, #Parameter3
Most of the work is done in the #Conditions calculation. That calculation selects the proper matrix row, flatens out the data by mapping each parameter column into a numbered row, formats each into a "column = #Parameter" equality comparison, and then uses STRING_AGG() to join the conditions together. That list of conditions is then combined with the rest of the query and executed. Because the executed dynamic SQL cannot access the parameters from the calling sql, the parameters must be explicitly passed in via the sp_ExecuteSql call.
Given the following parameters:
DECLARE #MatrixId INT = 2
DECLARE #Parameter1 VARCHAR(100) = 'Steven'
DECLARE #Parameter2 VARCHAR(100) = 'Johnson'
DECLARE #Parameter3 VARCHAR(100) = NULL
The generated SQL would be as follows:
SELECT *
FROM BaseTable B
WHERE B.[First Name] = #Parameter1
AND B.[Last Name] = #Parameter2
Which will yield the following result:
First Name
Last Name
City
State
Country
Steven
Johnson
Toronto
Ontario
Canada
See this db<>fiddle.

I tried to repro this. Below is the SQL script.
Input Tables
1. base_table
First Name
Last Name
City
State
Country
Mary
Smith
Austin
Texas
USA
Steven
Johnson
Toronto
Ontario
Canada
2. Matrix table
Matrix
Parameter1
Parameter2
Parameter3
1
City
State
Country
2
[First Name]
[Last Name]
null
SQL script:
DECLARE #StgTable TABLE ([ColName] varchar(256), Done bit default(0));
Insert into #StgTable( [ColName] ) select concat_ws(',',parameter1,parameter2,parameter3) from matrix_table
DECLARE #SQLstmnt nvarchar(max), #colnames varchar(200);
WHILE EXISTS (SELECT 1 FROM #StgTable WHERE Done = 0) BEGIN
SELECT TOP 1 #colnames = [ColName] FROM #StgTable WHERE Done = 0;
SET #SQLstmnt = 'SELECT '+ #colnames +' FROM base_table ;';
EXECUTE sp_executesql #SQLstmnt;
UPDATE #StgTable SET Done = 1 WHERE [ColName] = #colnames;
END;
See this db<>fiddle
Output:
City
State
Country
Austin
Texas
USA
Toronto
Ontario
Canada
First Name
Last Name
Mary
Smith
Steven
Johnson

Related

Unpivot Columns with Most Recent Record

Student Records are updated for subject and update date. Student can be enrolled in one or multiple subjects. I would like to get each student record with most subject update date and status.
CREATE TABLE Student
(
StudentID int,
FirstName varchar(100),
LastName varchar(100),
FullAddress varchar(100),
CityState varchar(100),
MathStatus varchar(100),
MUpdateDate datetime2,
ScienceStatus varchar(100),
SUpdateDate datetime2,
EnglishStatus varchar(100),
EUpdateDate datetime2
);
Desired query output, I am using CTE method but trying to find alternative and better way.
SELECT StudentID, FirstName, LastName, FullAddress, CityState, [SubjectStatus], UpdateDate
FROM Student
;WITH orginal AS
(SELECT * FROM Student)
,Math as
(
SELECT DISTINCT StudentID, FirstName, LastName, FullAddress, CityState,
ROW_NUMBER OVER (PARTITION BY StudentID, MathStatus ORDER BY MUpdateDate DESC) as rn
, _o.MathStatus as SubjectStatus, _o.MupdateDate as UpdateDate
FROM original as o
left join orignal as _o on o.StudentID = _o.StudentID
where _o.MathStatus is not null and _o.MUpdateDate is not null
)
,Science AS
(
...--Same as Math
)
,English AS
(
...--Same As Math
)
SELECT * FROM Math WHERE rn = 1
UNION
SELECT * FROM Science WHERE rn = 1
UNION
SELECT * FROM English WHERE rn = 1
First: storing data in a denormalized form is not recommended. Some data model redesign might be in order. There are multiple resources about data normalization available on the web, like this one.
Now then, I made some guesses about how your source table is populated based on the query you wrote. I generated some sample data that could show how the source data is created. Besides that I also reduced the number of columns to reduce my typing efforts. The general approach should still be valid.
Sample data
create table Student
(
StudentId int,
StudentName varchar(15),
MathStat varchar(5),
MathDate date,
ScienceStat varchar(5),
ScienceDate date
);
insert into Student (StudentID, StudentName, MathStat, MathDate, ScienceStat, ScienceDate) values
(1, 'John Smith', 'A', '2020-01-01', 'B', '2020-05-01'),
(1, 'John Smith', 'A', '2020-01-01', 'B+', '2020-06-01'), -- B for Science was updated to B+ month later
(2, 'Peter Parker', 'F', '2020-01-01', 'A', '2020-05-01'),
(2, 'Peter Parker', 'A+', '2020-03-01', 'A', '2020-05-01'), -- Spider-Man would never fail Math, fixed...
(3, 'Tom Holland', null, null, 'A', '2020-05-01'),
(3, 'Tom Holland', 'A-', '2020-07-01', 'A', '2020-05-01'); -- Tom was sick for Math, but got a second chance
Solution
Your question title already contains the word unpivot. That word actually exists in T-SQL as a keyword. You can learn about the unpivot keyword in the documentation. Your own solution already contains common table expression, these constructions should look familiar.
Steps:
cte_unpivot = unpivot all rows, create a Subject column and place the corresponding values (SubjectStat, Date) next to it with a case expression.
cte_recent = number the rows to find the most recent row per student and subject.
Select only those most recent rows.
This gives:
with cte_unpivot as
(
select up.StudentId,
up.StudentName,
case up.[Subject]
when 'MathStat' then 'Math'
when 'ScienceStat' then 'Science'
end as [Subject],
up.SubjectStat,
case up.[Subject]
when 'MathStat' then up.MathDate
when 'ScienceStat' then up.ScienceDate
end as [Date]
from Student s
unpivot ([SubjectStat] for [Subject] in ([MathStat], [ScienceStat])) up
),
cte_recent as
(
select cu.StudentId, cu.StudentName, cu.[Subject], cu.SubjectStat, cu.[Date],
row_number() over (partition by cu.StudentId, cu.[Subject] order by cu.[Date] desc) as [RowNum]
from cte_unpivot cu
)
select cr.StudentId, cr.StudentName, cr.[Subject], cr.SubjectStat, cr.[Date]
from cte_recent cr
where cr.RowNum = 1;
Result
StudentId StudentName Subject SubjectStat Date
----------- --------------- ------- ----------- ----------
1 John Smith Math A 2020-01-01
1 John Smith Science B+ 2020-06-01
2 Peter Parker Math A+ 2020-03-01
2 Peter Parker Science A 2020-05-01
3 Tom Holland Math A- 2020-07-01
3 Tom Holland Science A 2020-05-01

Extract digits after a word occurrence

I have a table with a Name column and a Log column.
Name Log
Michelle Bad news travels id 54585 fast.
Lucy Barking dogs id 545584 seldom bite.
Green Beauty is in the id 85955 eyes of the beholder.
Gail Beggars 123 can't be ID 4658 choosers.
I want to extract only the ID digits from log column. The output should be like this:
Name ID
Michelle 54585
Lucy 545584
Green 85955
Gail 4658
I tried to use the following query:
select name
, substring(log from E'^(.*?)[id< ]') as id
from mytable;
However, I cannot have the output I need. Note that the word ID could be capitalized or not.
You might be able to do this with fancy regular expressions. Here is one way that I do this with nested calls to translate():
select translate(col,
'0123456789' || translate(col, '0123456789', ''),
'0123456789')
from (select cast('asdg sgafadsf 123123 .,sdfa' as varchar(255)) as col) t;
The SQL Fiddle is here.
SELECT m.name
, regexp_replace (m.log, e'.* [iI][Dd] ([0-9]*).*', '\1', '') AS id
FROM meuk m
;
Result:
INSERT 0 4
name | id
----------+--------
Michelle | 54585
Lucy | 545584
Green | 85955
Gail | 4658
(4 rows)

Find all records NOT in any blocked range where blocked ranges are in a table

I have a table TaggedData with the following fields and data
ID GroupID Tag MyData
** ******* *** ******
1 Texas AA01 Peanut Butter
2 Texas AA15 Cereal
3 Ohio AA05 Potato Chips
4 Texas AA08 Bread
I have a second table of BlockedTags as follows:
ID StartTag EndTag
** ******** ******
1 AA00 AA04
2 AA15 AA15
How do I select from this to return all data matching a given GroupId but NOT in any blocked range (inclusive)? For the data given if the GroupId is Texas, I don't want to return Cereal because it matches the second range. It should only return Bread.
I did try left joins based queries but I'm not even that close.
Thanks
create table TaggedData (
ID int,
GroupID varchar(16),
Tag char(4),
MyData varchar(50))
create table BlockedTags (
ID int,
StartTag char(4),
EndTag char(4)
)
insert into TaggedData(ID, GroupID, Tag, MyData)
values (1, 'Texas', 'AA01', 'Peanut Butter')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (2, 'Texas' , 'AA15', 'Cereal')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (3, 'Ohio ', 'AA05', 'Potato Chips')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (4, 'Texas', 'AA08', 'Bread')
insert into BlockedTags(ID, StartTag, EndTag)
values (1, 'AA00', 'AA04')
insert into BlockedTags(ID, StartTag, EndTag)
values (2, 'AA15', 'AA15')
select t.* from TaggedData t
left join BlockedTags b on t.Tag between b.StartTag and b.EndTag
where b.ID is null
Returns:
ID GroupID Tag MyData
----------- ---------------- ---- --------------------------------------------------
3 Ohio AA05 Potato Chips
4 Texas AA08 Bread
(2 row(s) affected)
So, to match on given GroupID you change the query like that:
select t.* from TaggedData t
left join BlockedTags b on t.Tag between b.StartTag and b.EndTag
where b.ID is null and t.GroupID=#GivenGroupID
I Prefer the NOT EXISTS simply because it gives you more readability, usability and better performance usually in large data (several cases get better execution plans):
would be like this:
SELECT * from TaggedData
WHERE GroupID=#GivenGroupID
AND NOT EXISTS(SELECT 1 FROM BlockedTags WHERE Tag BETWEEN StartTag ANDEndTag)

T-SQL query, multiple values in a field

I have two tables in a database. The first table tblTracker contains many columns, but the column of particular interest is called siteAdmin and each row in that column can contain multiple loginIDs of 5 digits like 21457, 21456 or just one like 21444. The next table users contains columns like LoginID, fname, and lname.
What I would like to be able to do is take the loginIDs contained in tblTracker.siteAdmin and return fname + lname from users. I can successfully do this when there is only one loginID in the row such as 21444 but I cannot figure out how to do this when there is more than one like 21457, 21456.
Here is the SQL statement I use for when there is one loginID in that column
SELECT b.FName + '' '' + b.LName AS siteAdminName,
FROM tblTracker a
LEFT OUTER JOIN users b ON a.siteAdmin= b.Login_Id
However this doesn't work when it tries to join a siteAdmin with more than one LoginID in it
Thanks!
I prefer the number table approach to split a string in TSQL
For this method to work, you need to do this one time table setup:
SELECT TOP 10000 IDENTITY(int,1,1) AS Number
INTO Numbers
FROM sys.objects s1
CROSS JOIN sys.objects s2
ALTER TABLE Numbers ADD CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number)
Once the Numbers table is set up, create this split function:
CREATE FUNCTION [dbo].[FN_ListToTable]
(
#SplitOn char(1) --REQUIRED, the character to split the #List string on
,#List varchar(8000)--REQUIRED, the list to split apart
)
RETURNS TABLE
AS
RETURN
(
----------------
--SINGLE QUERY-- --this will not return empty rows
----------------
SELECT
ListValue
FROM (SELECT
LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(#SplitOn, List2, number+1)-number - 1))) AS ListValue
FROM (
SELECT #SplitOn + #List + #SplitOn AS List2
) AS dt
INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
WHERE SUBSTRING(List2, number, 1) = #SplitOn
) dt2
WHERE ListValue IS NOT NULL AND ListValue!=''
);
GO
You can now easily split a CSV string into a table and join on it:
select * from dbo.FN_ListToTable(',','1,2,3,,,4,5,6777,,,')
OUTPUT:
ListValue
-----------------------
1
2
3
4
5
6777
(6 row(s) affected)
Your can now use a CROSS APPLY to split every row in your table like:
DECLARE #users table (LoginID int, fname varchar(5), lname varchar(5))
INSERT INTO #users VALUES (1, 'Sam', 'Jones')
INSERT INTO #users VALUES (2, 'Don', 'Smith')
INSERT INTO #users VALUES (3, 'Joe', 'Doe')
INSERT INTO #users VALUES (4, 'Tim', 'White')
INSERT INTO #users VALUES (5, 'Matt', 'Davis')
INSERT INTO #users VALUES (15,'Sue', 'Me')
DECLARE #tblTracker table (RowID int, siteAdmin varchar(50))
INSERT INTO #tblTracker VALUES (1,'1,2,3')
INSERT INTO #tblTracker VALUES (2,'2,3,4')
INSERT INTO #tblTracker VALUES (3,'1,5')
INSERT INTO #tblTracker VALUES (4,'1')
INSERT INTO #tblTracker VALUES (5,'5')
INSERT INTO #tblTracker VALUES (6,'')
INSERT INTO #tblTracker VALUES (7,'8,9,10')
INSERT INTO #tblTracker VALUES (8,'1,15,3,4,5')
SELECT
t.RowID, u.LoginID, u.fname+' '+u.lname AS YourAdmin
FROM #tblTracker t
CROSS APPLY dbo.FN_ListToTable(',',t.siteAdmin) st
LEFT OUTER JOIN #users u ON st.ListValue=u.LoginID --to get all rows even if missing siteAdmin
--INNER JOIN #users u ON st.ListValue=u.LoginID --to remove rows without any siteAdmin
ORDER BY t.RowID,u.fname,u.lname
OUTPUT:
RowID LoginID YourAdmin
----------- ----------- -----------
1 2 Don Smith
1 3 Joe Doe
1 1 Sam Jones
2 2 Don Smith
2 3 Joe Doe
2 4 Tim White
3 5 Matt Davis
3 1 Sam Jones
4 1 Sam Jones
5 5 Matt Davis
7 NULL NULL
7 NULL NULL
7 NULL NULL
8 3 Joe Doe
8 5 Matt Davis
8 1 Sam Jones
8 15 Sue Me
8 4 Tim White
(18 row(s) affected)

Update table lookup

I have a temp table (#WorkTable) which looks like the following:
InstrID CountryName CreditRating
1 UK AA
2 UK Unclassified
3 South Africa A
4 South Africa A
5 South Africa Unclassified
What I want to be able to do is update this table where column CreditRating is 'Unclassified' with its actual credit rating. So in the example above the UK unclassified would become 'AA' and the South African one would become 'A'.
I'm unsure of the coding for this, any help would be greatly appreciated.
The sql script below will update any unclassified to the 'actual' credit ratings. I hope this helps.
CREATE TABLE #WorkTable
(
InstrID INT,
CountryName VARCHAR(50),
CreditRating VARCHAR(20)
)
INSERT INTO #WorkTable VALUES ( 1, 'UK','AA');
INSERT INTO #WorkTable VALUES ( 2, 'UK','Unclassified');
INSERT INTO #WorkTable VALUES ( 3, 'South Africa','A');
INSERT INTO #WorkTable VALUES ( 4, 'South Africa','A');
INSERT INTO #WorkTable VALUES ( 5, 'South Africa','Unclassified');
WITH cteUnclassified
AS
(
SELECT InstrID,
CountryName,
CeditRating
FROM #WorkTable
WHERE CreditRating != 'Unclassified'
)
UPDATE #WorkTable
SET CreditRating = u.CreditRating
FROM #WorkTable wt
INNER JOIN cteUnclassified u
ON wt.CountryName = u.CountryName
WHERE wt.CreditRating = 'Unclassified'
SELECT *
FROM #WorkTable
Result of the query below:
InstrID CountryName CreditRating
1 UK AA
2 UK AA
3 South Africa A
4 South Africa A
5 South Africa A
I think This example would help you.
String sql = "update table_name set CreditRating = (select unique CreditRating from table_name where CountryName = ? and CreditRating != 'classifie...') where CountryName = ? and CreditRating = 'classifie...'";
Here you can pass the countryName as a paramaeter.
You seem new to SQL. What you need to do is "join" the table to the lookup values. You can find a lot of help on basics of SQL in a lot of places, but if you need a quick solution, try the following:
If you havea seperate table with countries and credit ratings, (I have assumed the name of that table is Rating, and the country names and rating match.)
update #WorkTable
Set w.Creditrating = r.CreditRating
from #WorkTable w join Rating r on w.CountryName=r.CountryName
where w.CreditRating='Unclassified'
On the other hand, if no seperate table exists, and you want to update the unclassified values based on the same temp table, then you need to assume that there is some entry with a CreditRating for that country in the table already (which, in the case you posted, there is.) It also needs for there to be only one CreditRating per country. In this case, the following should work:
update #WorkTable
Set w.Creditrating = w2.CreditRating
from #WorkTable w join
(Select distinct CountryName, CreditRating from #WorkTable where CreditRating<>'Unclassified') w2
on w.CountryName=w2.CountryName
where w.CreditRating='Unclassified'
This uses the table itself to find the values, by looking at the table twice - once as the table to update, where the last line restricts what is updated to only the unclassified items, and once as a source table, where only classified ratings are used. Please leave a comment with more details if this doesn't work for you.