Related
I have sample data set as follows,
| Customer | |Detail | |DataValues |
|----------| |-------| |-----------|
| ID | |ID | |CustomerID |
| Name | |Name | |DetailID |
|Values |
| Customer | |Detail | |DataValues |
|----------| |---------| |-----------|
| 1 | Jack | | 1 | sex | | 1 | 1 | M |
| 2 | Anne | | 2 | age | | 1 | 2 | 30|
| 2 | 1 | F |
| 2 | 2 | 28|
and my desired outcome is below,
Name
Sex
Age
Jack
M
30
Anne
F
28
I have failed to come up with a correct SQL Query that returns anything.
Thanks in advance.
select Customers.Name, Details.Name, DataValues.Value from Customers
inner join DataValues on DataValues.CustomersID = Customers.ID
inner join Details on DataValues.DetailsID = Details.ID
The static way, assuming you know you want exactly Sex and Age:
WITH cte AS
(
SELECT c.Name, Type = d.Name, dv.[Values]
FROM dbo.DataValues AS dv
INNER JOIN dbo.Detail AS d
ON dv.DetailID = d.ID
INNER JOIN dbo.Customer AS c
ON dv.CustomerID = c.ID
WHERE d.Name IN (N'Sex',N'Age')
)
SELECT Name, Sex, Age
FROM cte
PIVOT (MAX([Values]) FOR [Type] IN ([Sex],[Age])) AS p;
If you need to derive the query based on all of the possible attributes, then you'll need to use dynamic SQL. Here's one way:
DECLARE #in nvarchar(max),
#piv nvarchar(max),
#sql nvarchar(max);
SELECT #in = STRING_AGG(N'N' + QUOTENAME(Name, char(39)), ','),
#piv = STRING_AGG(QUOTENAME(Name), ',')
FROM (SELECT Name FROM dbo.Detail GROUP BY Name) AS src;
SET #sql = N'WITH cte AS
(
SELECT c.Name, Type = d.Name, dv.[Values]
FROM dbo.DataValues AS dv
INNER JOIN dbo.Detail AS d
ON dv.DetailID = d.ID
INNER JOIN dbo.Customer AS c
ON dv.CustomerID = c.ID
WHERE d.Name IN (' + #in + N')
)
SELECT Name, ' + #piv + N'
FROM cte
PIVOT (MAX([Values]) FOR [Type] IN (' + #piv + N')) AS p;';
EXECUTE sys.sp_executesql #sql;
Working examples in this fiddle.
There's a lot to unpack here. Let's start with how to present demo data:
If you provide the DDL and DML for your data it makes it much easier for folks to work with:
DECLARE #Customer TABLE (ID INT, Name NVARCHAR(100))
DECLARE #Detail TABLE (ID INT, Name NVARCHAR(20))
DECLARE #DataValues TABLE (CustomerID INT, DetailID INT, [Values] NVARCHAR(20))
INSERT INTO #Customer (ID, Name) VALUES
(1, 'Jack'),(2, 'Anne')
INSERT INTO #Detail (ID, Name) VALUES
(1, 'Sex'),(2, 'Age')
INSERT INTO #DataValues (CustomerID, DetailID, [Values]) VALUES
(1, 1, 'M'),(1, 2, '30'),(2, 1, 'F'),(2, 2, '28')
This sets up your tables (as variables) and populates them with the demo data.
Next let's talk about the horrible schema here.
You should always to try avoid reserved words as column names too. Values is a keyword.
This should probably be a single customers table:
DECLARE #Genders TABLE (ID INT IDENTITY, Name NVARCHAR(20))
DECLARE #Customer1 TABLE (CustomerID INT IDENTITY, Name NVARCHAR(100), BirthDate DATETIME, GenderID INT NULL, Age AS (DATEDIFF(YEAR, BirthDate, CURRENT_TIMESTAMP)))
Notice I used BirthDate instead of Age. This is because a persons age will change over time, but their birth date will not. Attributes that are calculated based on another attribute shouldn't be stored (but if you want you can used a calculated column, as we are here). You'll also note that instead of explicitly defining gender in the customers table we instead will reference it by Gender ID. This is a lookup table.
If you had used a normalized schema your query would then look like:
/* Demo Data */
DECLARE #Genders TABLE (ID INT IDENTITY, Name NVARCHAR(20));
INSERT INTO #Genders (Name) VALUES
('Male'),('Female'),('Non-Binary');
DECLARE #Customer1 TABLE (CustomerID INT IDENTITY, Name NVARCHAR(100), BirthDate DATETIME, GenderID INT NULL, Age AS (DATEDIFF(YEAR, BirthDate, CURRENT_TIMESTAMP)));
INSERT INTO #Customer1 (Name, BirthDate, GenderID) VALUES
('Jack', '2000-11-03', 1),('Anne', '2000-11-01', 2),('Chris', '2001-05-13', NULL);
/* Query */
SELECT *
FROM #Customer1 c
LEFT OUTER JOIN #Genders g
ON c.GenderID = g.ID;
Now on to how to get the data you want from the structure you have. Anyway you do this is going to be acrobatic because we have to work against the schema.
/* Demo Data */
DECLARE #Customer TABLE (ID INT, Name NVARCHAR(100));
DECLARE #Detail TABLE (ID INT, Name NVARCHAR(20));
DECLARE #DataValues TABLE (CustomerID INT, DetailID INT, [Values] NVARCHAR(20));
INSERT INTO #Customer (ID, Name) VALUES
(1, 'Jack'),(2, 'Anne');
INSERT INTO #Detail (ID, Name) VALUES
(1, 'Sex'),(2, 'Age');
INSERT INTO #DataValues (CustomerID, DetailID, [Values]) VALUES
(1, 1, 'M'),(1, 2, '30'),(2, 1, 'F'),(2, 2, '28');
/* Query */
SELECT *
FROM (
SELECT d.Name AS DetailName, c.Name AS CustomerName, DV.[Values]
FROM #DataValues dv
INNER JOIN #Detail d
ON dv.DetailID = d.ID
INNER JOIN #Customer c
ON dv.CustomerID = c.ID
) a
PIVOT (
MAX([Values]) FOR DetailName IN (Sex,Age)
) p;
CustomerName Sex Age
-----------------------
Anne F 28
Jack M 30
Let's suppose I have a temporary table which looks like this:
+----+------+
| Id | Value|
+----+------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
| 2 | 2 |
+----+------+
And I want my table to be like this:
+----+----------+
| Id | ValueList|
+----+----------+
| 1 | 1,2,3 |
| 2 | 1,2 |
+----+----------+
So basically I need to group my values as a comma separated list.
I already tried the following:
SELECT Id, STUFF((SELECT ',' + CAST(VALUE AS varchar) FROM #MyTable FOR XML PATH('')), 1 ,1, '') AS ValueList
FROM #MyTable
GROUP BY Id
But I get something like:
+----+---------------------+
| Id | ValueList |
+----+---------------------+
| 1 | 1,1,1,1,1,1,... |
+----+---------------------+
I cant find what I am doing wrong. Could someone help with this query? Or point me to a right direction?
Thank you.
You are missing the condition inside the sub query.
SELECT t2.Id, STUFF((SELECT ',' + CAST(VALUE AS varchar) FROM #MyTable t1 where t1.Id =t2.ID FOR XML PATH('')), 1 ,1, '') AS ValueList
FROM #MyTable t2
GROUP BY t2.Id
Demo
One alternative to using GROUP BY on the Id would be to use select distinct:
SELECT DISTINCT
Id,
STUFF((SELECT ',' + CAST(t2.VALUE AS varchar)
FROM #MyTable t2
WHERE t2.Id = t1.Id
FOR XML PATH('')), 1 ,1, '') AS ValueList
FROM #MyTable t1
Demo
One can also combine a FOR XML with a CROSS APPLY (or an OUTER APPLY) for this.
Example snippet:
declare #T table (id int, value int);
insert into #T values (1,1),(1,2),(1,3),(2,1),(2,2);
select id, stuff(x.list,1,1,'') as list
from (select distinct id from #T) as t
cross apply (
select concat(',',t2.value)
from #T t2
where t2.id = t.id
for xml path('')
) x(list)
order by id;
Result:
id list
-- -----
1 1,2,3
2 1,2
And starting from MS Sql Server 2017, STRING_AGG can be used instead.
select id, string_agg(value,',') as list
from Yourtable t
group by id;
Try this :
create table #t(id int, value int)
insert into #t values
(1,1),
(1,2),
(1,3),
(2,1),
(2,2)
SELECT t2.Id,
STUFF((SELECT ',' + CAST(VALUE AS varchar) FROM #t t1 where t1.Id =t2.ID FOR XML PATH('')), 1 ,1, '') AS list
FROM #t t2
GROUP BY t2.Id
output :
Id list
--- -------
1 1,2,3
2 1,2
Simple Solution
SELECT Id, GROUP_CONCAT(Value) as ValueList FROM MyTable GROUP BY Id;
add distinct to values if required
SELECT Id, GROUP_CONCAT(DISTINCT Value) as ValueList FROM MyTable GROUP BY Id;
Let's suppose I have a temporary table which looks like this:
+----+------+
| Id | Value|
+----+------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
| 2 | 2 |
+----+------+
And I want my table to be like this:
+----+----------+
| Id | ValueList|
+----+----------+
| 1 | 1,2,3 |
| 2 | 1,2 |
+----+----------+
So basically I need to group my values as a comma separated list.
I already tried the following:
SELECT Id, STUFF((SELECT ',' + CAST(VALUE AS varchar) FROM #MyTable FOR XML PATH('')), 1 ,1, '') AS ValueList
FROM #MyTable
GROUP BY Id
But I get something like:
+----+---------------------+
| Id | ValueList |
+----+---------------------+
| 1 | 1,1,1,1,1,1,... |
+----+---------------------+
I cant find what I am doing wrong. Could someone help with this query? Or point me to a right direction?
Thank you.
You are missing the condition inside the sub query.
SELECT t2.Id, STUFF((SELECT ',' + CAST(VALUE AS varchar) FROM #MyTable t1 where t1.Id =t2.ID FOR XML PATH('')), 1 ,1, '') AS ValueList
FROM #MyTable t2
GROUP BY t2.Id
Demo
One alternative to using GROUP BY on the Id would be to use select distinct:
SELECT DISTINCT
Id,
STUFF((SELECT ',' + CAST(t2.VALUE AS varchar)
FROM #MyTable t2
WHERE t2.Id = t1.Id
FOR XML PATH('')), 1 ,1, '') AS ValueList
FROM #MyTable t1
Demo
One can also combine a FOR XML with a CROSS APPLY (or an OUTER APPLY) for this.
Example snippet:
declare #T table (id int, value int);
insert into #T values (1,1),(1,2),(1,3),(2,1),(2,2);
select id, stuff(x.list,1,1,'') as list
from (select distinct id from #T) as t
cross apply (
select concat(',',t2.value)
from #T t2
where t2.id = t.id
for xml path('')
) x(list)
order by id;
Result:
id list
-- -----
1 1,2,3
2 1,2
And starting from MS Sql Server 2017, STRING_AGG can be used instead.
select id, string_agg(value,',') as list
from Yourtable t
group by id;
Try this :
create table #t(id int, value int)
insert into #t values
(1,1),
(1,2),
(1,3),
(2,1),
(2,2)
SELECT t2.Id,
STUFF((SELECT ',' + CAST(VALUE AS varchar) FROM #t t1 where t1.Id =t2.ID FOR XML PATH('')), 1 ,1, '') AS list
FROM #t t2
GROUP BY t2.Id
output :
Id list
--- -------
1 1,2,3
2 1,2
Simple Solution
SELECT Id, GROUP_CONCAT(Value) as ValueList FROM MyTable GROUP BY Id;
add distinct to values if required
SELECT Id, GROUP_CONCAT(DISTINCT Value) as ValueList FROM MyTable GROUP BY Id;
I need a little help getting the results I need. I have 2 tables with a common field of id. I want to combine the values from table B that have the same id
Table A
id | name | somevalue1
1 | dud | 12345
2 | duda | 8908
Table B
id | somevalue2
1 | 56545
2 | 545665
1 | 89875
2 | 12524
Desired Result
id | somevalue2 combined
1 | 56545, 89875
2 | 545665, 12524
I've tried using a Join, but I'm a bit lost. I have been attempting to do a subselect, but the only way I can think of to make it work is to reference the main query from within the subselect, but that aint gettin' it either.
This query I have been trying just hangs:
select distinct a.id,(
select b.somevalue2 + ', ' as [text()] from tableB b
where b.id = a.id and b.somevalue2 is not null for xml path(''))
as [ColumnName]
from tableA a
order by a.id asc
EDIT
I think I need to mention that TableB has over 400,000 rows.
TableA has only about 1,500 rows.
This works:
SELECT A.*,
STUFF((
SELECT ', ' + CAST([somevalue2] AS VARCHAR(20))
FROM TableB B
WHERE A.id = B.id
FOR XML PATH ('')), 1, 1, '')
FROM TableA A
Here is an sqlfiddle for you to try.
UNPIVOT will not return NULLs, but I need them in a comparison query. I am trying to avoid using ISNULL the following example (Because in the real sql there are over 100 fields):
Select ID, theValue, column_name
From
(select ID,
ISNULL(CAST([TheColumnToCompare] AS VarChar(1000)), '') as TheColumnToCompare
from MyView
where The_Date = '04/30/2009'
) MA
UNPIVOT
(theValue FOR column_name IN
([TheColumnToCompare])
) AS unpvt
Any alternatives?
To preserve NULLs, use CROSS JOIN ... CASE:
select a.ID, b.column_name
, column_value =
case b.column_name
when 'col1' then a.col1
when 'col2' then a.col2
when 'col3' then a.col3
when 'col4' then a.col4
end
from (
select ID, col1, col2, col3, col4
from table1
) a
cross join (
select 'col1' union all
select 'col2' union all
select 'col3' union all
select 'col4'
) b (column_name)
Instead of:
select ID, column_name, column_value
From (
select ID, col1, col2, col3, col4
from table1
) a
unpivot (
column_value FOR column_name IN (
col1, col2, col3, col4)
) b
A text editor with column mode makes such queries easier to write. UltraEdit has it, so does Emacs. In Emacs it's called rectangular edit.
You might need to script it for 100 columns.
It's a real pain. You have to switch them out before the UNPIVOT, because there is no row produced for ISNULL() to operate on - code generation is your friend here.
I have the problem on PIVOT as well. Missing rows turn into NULL, which you have to wrap in ISNULL() all the way across the row if missing values are the same as 0.0 for example.
I ran into the same problem. Using CROSS APPLY (SQL Server 2005 and later) instead of Unpivot solved the problem. I found the solution based on this article An Alternative (Better?) Method to UNPIVOT
and I made the following example to demonstrate that CROSS APPLY will NOT Ignore NULLs like Unpivot.
create table #Orders (OrderDate datetime, product nvarchar(100), ItemsCount float, GrossAmount float, employee nvarchar(100))
insert into #Orders
select getutcdate(),'Windows',10,10.32,'Me'
union
select getutcdate(),'Office',31,21.23,'you'
union
select getutcdate(),'Office',31,55.45,'me'
union
select getutcdate(),'Windows',10,null,'You'
SELECT OrderDate, product,employee,Measure,MeasureType
from #Orders orders
CROSS APPLY (
VALUES ('ItemsCount',ItemsCount),('GrossAmount',GrossAmount)
)
x(MeasureType, Measure)
SELECT OrderDate, product,employee,Measure,MeasureType
from #Orders orders
UNPIVOT
(Measure FOR MeasureType IN
(ItemsCount,GrossAmount)
)AS unpvt;
drop table #Orders
or, in SQLServer 2008 in shorter way:
...
cross join
(values('col1'), ('col2'), ('col3'), ('col4')) column_names(column_name)
Using dynamic SQL and COALESCE, I solved the problem like this:
DECLARE #SQL NVARCHAR(MAX)
DECLARE #cols NVARCHAR(MAX)
DECLARE #dataCols NVARCHAR(MAX)
SELECT
#dataCols = COALESCE(#dataCols + ', ' + 'ISNULL(' + Name + ',0) ' + Name , 'ISNULL(' + Name + ',0) ' + Name )
FROM Metric WITH (NOLOCK)
ORDER BY ID
SELECT
#cols = COALESCE(#cols + ', ' + Name , Name )
FROM Metric WITH (NOLOCK)
ORDER BY ID
SET #SQL = 'SELECT ArchiveID, MetricDate, BoxID, GroupID, ID MetricID, MetricName, Value
FROM
(SELECT ArchiveID, [Date] MetricDate, BoxID, GroupID, ' + #dataCols + '
FROM MetricData WITH (NOLOCK)
INNER JOIN Archive WITH (NOLOCK)
ON ArchiveID = ID
WHERE BoxID = ' + CONVERT(VARCHAR(40), #BoxID) + '
AND GroupID = ' + CONVERT(VARCHAR(40), #GroupID) + ') p
UNPIVOT
(Value FOR MetricName IN
(' + #cols + ')
)AS unpvt
INNER JOIN Metric WITH (NOLOCK)
ON MetricName = Name
ORDER BY MetricID, MetricDate'
EXECUTE( #SQL )
I've found left outer joining the UNPIVOT result to the full list of fields, conveniently pulled from INFORMATION_SCHEMA, to be a practical answer to this problem in some contexts.
-- test data
CREATE TABLE _t1(name varchar(20),object_id varchar(20),principal_id varchar(20),schema_id varchar(20),parent_object_id varchar(20),type varchar(20),type_desc varchar(20),create_date varchar(20),modify_date varchar(20),is_ms_shipped varchar(20),is_published varchar(20),is_schema_published varchar(20))
INSERT INTO _t1 SELECT 'blah1', 3, NULL, 4, 0, 'blah2', 'blah3', '20100402 16:59:23.267', NULL, 1, 0, 0
-- example
select c.COLUMN_NAME, Value
from INFORMATION_SCHEMA.COLUMNS c
left join (
select * from _t1
) q1
unpivot (Value for COLUMN_NAME in (name,object_id,principal_id,schema_id,parent_object_id,type,type_desc,create_date,modify_date,is_ms_shipped,is_published,is_schema_published)
) t on t.COLUMN_NAME = c.COLUMN_NAME
where c.TABLE_NAME = '_t1'
</pre>
output looks like:
+----------------------+-----------------------+
| COLUMN_NAME | Value |
+----------------------+-----------------------+
| name | blah1 |
| object_id | 3 |
| principal_id | NULL | <======
| schema_id | 4 |
| parent_object_id | 0 |
| type | blah2 |
| type_desc | blah3 |
| create_date | 20100402 16:59:23.26 |
| modify_date | NULL | <======
| is_ms_shipped | 1 |
| is_published | 0 |
| is_schema_published | 0 |
+----------------------+-----------------------+
Writing in May'22 with testing it on AWS Redshift.
You can use a with clause where you can coalesce the columns where nulls are expected. Alternatively, you can use coalesce in the select statement prior to the UNPIVOT block.
And don't forget to alias with the original column name (Not following won't break or violate the rule but would save some time for coffee).
Select ID, theValue, column_name
From
(select ID,
coalesce(CAST([TheColumnToCompare] AS VarChar(1000)), '') as TheColumnToCompare
from MyView
where The_Date = '04/30/2009'
) MA
UNPIVOT
(theValue FOR column_name IN
([TheColumnToCompare])
) AS unpvt
OR
WITH TEMP1 as (
select ID,
coalesce(CAST([TheColumnToCompare] AS VarChar(1000)), '') as TheColumnToCompare
from MyView
where The_Date = '04/30/2009'
)
Select ID, theValue, column_name
From
(select ID, TheColumnToCompare
from MyView
where The_Date = '04/30/2009'
) MA
UNPIVOT
(theValue FOR column_name IN
([TheColumnToCompare])
) AS unpvt
I had your same problem and this is
my quick and dirty solution :
your query :
select
Month,Name,value
from TableName
unpivot
(
Value for Name in (Col_1,Col_2,Col_3,Col_4,Col_5
)
) u
replace with :
select Month,Name,value from
( select
isnull(Month,'no-data') as Month,
isnull(Name,'no-data') as Name,
isnull(value,'no-data') as value from TableName
) as T1
unpivot
(
Value
for Name in (Col_1,Col_2,Col_3,Col_4,Col_5)
) u
ok the null value is replaced with a string, but all rows will be returned !!
ISNULL is half the answer. Use NULLIF to translate back to NULL. E.g.
DECLARE #temp TABLE(
Foo varchar(50),
Bar varchar(50) NULL
);
INSERT INTO #temp( Foo,Bar )VALUES( 'licious',NULL );
SELECT * FROM #temp;
SELECT
Col,
NULLIF( Val,'0Null' ) AS Val
FROM(
SELECT
Foo,
ISNULL( Bar,'0Null' ) AS Bar
FROM
#temp
) AS t
UNPIVOT(
Val FOR Col IN(
Foo,
Bar
)
) up;
Here I use "0Null" as my intermediate value. You can use anything you like. However, you risk collision with user input if you choose something real-world like "Null". Garbage works fine "!##34())0" but may be more confusing to future coders. I am sure you get the picture.