merging and transposing tables - sql-server

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

Related

How to get data as a list in SQL server [duplicate]

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;

JOIN SUM with WHERE

I couldn't figure this out from existing kinda similar threads. I've tried left join and different sort of subqueries but no luck.
I got left join work with group by but couldn't figure out how to add where clauses, then wen't to subqueries and broke everything.
I have two tables storage and orders.
Storage has list of unique id and name
id | name
1234 | product1
1235 | product2
A123 | product3
Orders have multiple instances code, quantity and type
code | qty| type
1234 | 10 | order
1234 | 10 | quote
1234 | 10 | order
A123 | 15 | order
1235 | 13 | order
I wan't to join these tables so that I get filtered (with where) results from storage and join with summed qty where type is order.
For example filter storage where id in (1234, A123) should result:
id | name | sum qty
1234 | product1 | 20
A123 | product3 | 15
Any help appreciated!
--
Going forward, storage has products and cols. There is table prod_to_col that has productid and col_id to tie them together.
I would need to grab product codes from table prod_to_col and show total quantity for cols according to order quantity.
I tried this according to #iSR5 example:
SELECT st.id, st.name, SUM(order.qty) AS SumQty
FROM storage
JOIN prod_to_col ON st.id=prod_to_col.col_id
JOIN orders ON order.id IN (SELECT prod_id FROM prod_to_col WHERE col_id=st.id) AND type='order'
WHERE id IN (1234, A123)
GROUP BY st.id, st.name
This almost works but quantities are multiplied in some rows some are fine, can someone point where it goes wrong?
In addition to tables storage and orders above, here's example of prod_to_col and cols:
Prod_to_col
prod_id | col_id | col_qty (per product)
1235 | C101 | 2
1236 | C102 | 1
Cols
col_id | name | other data
C101 | cname1 | --
C102 | cname2 | --
Orders
prod_id | qty | type
1235 | 10 | order
1235 | 10 | order
1236 | 2 | quote
1236 | 5 | order
Storage
st.id | st.name| SumQty
C101 | cname1 | 40
C102 | cname2 | 5
I understand I need to use two different sentence to populate storage list, one for products and one for cols. The one for products works fine.
Is this is what you need ?
DECLARE #Storage TABLE(ID VARCHAR(50), name VARCHAR(250) )
DECLARE #Orders TABLE(code VARCHAR(50), qty INT, type VARCHAR(50))
INSERT INTO #Storage VALUES
('1234','product1')
, ('1235','product2')
, ('A123','product3')
INSERT INTO #Orders VALUES
('1234',10,'order')
, ('1234',10,'quote')
, ('1234',10,'order')
, ('A123',15,'order')
, ('1235',13,'order')
SELECT
s.ID
, s.name
, SUM(o.qty) TotalQty
FROM
#Storage s
JOIN #Orders o ON o.code = s.ID AND o.type = 'order'
WHERE
s.ID IN('1234','A123')
GROUP BY
s.ID
, s.name
UPDATE
You've updated your post with more logic to cover, which wasn't provided before, however, I've update it the query for you ..
DECLARE #Storage TABLE(ID VARCHAR(50), name VARCHAR(250) )
DECLARE #Orders TABLE(code VARCHAR(50), qty INT, type VARCHAR(50))
DECLARE #Prod_to_col TABLE(prod_id VARCHAR(50), col_id VARCHAR(50), col_qty INT)
DECLARE #Cols TABLE(col_id VARCHAR(50), name VARCHAR(250))
INSERT INTO #Storage VALUES
('1234','product1')
, ('1235','product2')
, ('A123','product3')
, ('1236','product3')
INSERT INTO #Orders VALUES
('1234',10,'order')
, ('1234',10,'quote')
, ('1234',10,'order')
, ('A123',15,'order')
, ('1235',10,'order')
, ('1235',10,'order')
, ('1236',2,'quote')
, ('1236',5,'order')
INSERT INTO #Prod_to_col VALUES
('1235','C101',2)
, ('1236','C102',1)
INSERT INTO #Cols VALUES
('C101','cname1')
, ('C102','cname2')
SELECT
c.col_id
, c.name
, SUM(o.qty) * MAX(ptc.col_qty) TotalQty
FROM
#Storage s
JOIN #Orders o ON o.code = s.ID AND o.type = 'order'
JOIN #Prod_to_col ptc ON ptc.prod_id = o.code
JOIN #Cols c ON c.col_id = ptc.col_id
--WHERE
-- s.ID IN('1234','A123')
GROUP BY
c.col_id
, c.name
Try to use Left Join combined with a group by
SELECT OD.CODE
,ST.PRODUCT
,SUM(quantity) as qnt
FROM ORDERS OD
LEFT JOIN STORAGE ST ON(OD.CODE = SG.CODE)
WHERE OD.type like 'order'
GROUP BY
OD.CODE
,ST.PRODUCT
You can use Having to filter
Having id in (1234, A123)
Greetings

SQL Update Manager ID in the Employee Table

I have Employee Temp Table with the following table structure:
**|EmpId | EmpName | ManagerId | ManagerName|**
|------|---------|-----------|------------|
|[113] |[Test] | |[A] |
|[111] |[A] | |[B] |
I need an update query to populate the Manager Id in the employee temp table by getting the values from the EmpId of the same
update B
set B.ManagerCode=A.EmpID
from EmployeeTemp A INNER JOIN EmployeeTemp B
ON A.ManagerName=B.EmpName
This query doesn't work for me. Could anyone suggest a modification to this.
It is updated successfully
DECLARE #Table TABLE (
EmpId INT,
EmpName VARCHAR(100),
ManagerId INT,
ManagerName VARCHAR(100)
)
INSERT INTO #Table
SELECT 113 ,'Test', NULL, 'A' UNION ALL
SELECT 111 ,'A' , NULL, 'B'
SELECT * FROM #Table
UPDATE I
SET ManagerId = O.EmpId
FROM #Table I
INNER JOIN #Table o
ON I.ManagerName = o.EmpName
SELECT * FROM #Table

Referencing outer table in an aggregate function in a subquery

I'm looking for a solution to particular query problem. I have a table Departments and table Employees designed like that:
Departments Employees
===================== ============================
ID | Name ID | Name | Surname | DeptID
--------------------- ----------------------------
1 | ADMINISTRATION 1 | X | Y | 2
2 | IT 2 | Z | Z | 1
3 | ADVERTISEMENT 3 | O | O | 1
4 | A | B | 3
I'd like to get list of all departments whose number of employees is smaller than number of employees working in Administration.
That was one of my ideas, but it did not work:
select * from Departments as Depts where Depts.ID in
(select Employees.ID from Employees group by Employees.ID
having count(Employees.ID) < count(case when Depts.Name='ADMINISTRATION' then 1 end));
Using GROUP BY and HAVING:
SELECT
d.ID, d.Name
FROM Departments d
LEFT JOIN Employees e
ON e.DeptID = d.ID
GROUP BY d.ID, d.Name
HAVING
COUNT(e.ID) < (SELECT COUNT(*) FROM Employees WHERE DeptID = 1)
Try this,
declare #Departments table (ID int, Name varchar(50))
insert into #Departments
values
(1 ,'ADMINISTRATION')
,(2 ,'IT')
,(3 ,'ADVERTISEMENT')
declare #Employees table (ID int, Name varchar(50)
,Surname varchar(50),DeptID int)
insert into #Employees
values
(1 ,'X','Y',2)
,(2 ,'Z','Z',1)
,(3 ,'O','O',1)
,(4 ,'A','B',3)
;
WITH CTE
AS (
SELECT *
,row_number() OVER (
PARTITION BY deptid ORDER BY id
) rn
FROM #Employees
WHERE deptid <> 1
)
SELECT *
FROM cte
WHERE rn < (
SELECT count(id) admincount
FROM #Employees
WHERE DeptID = 1
)

Concatenating columns using CTE in SQL Server 2008

I have a table like
TABLEX -
+------+------------+
| NAME | TABLE_NAME |
+------+------------+
| X1 | X001 |
| X2 | X002 |
+------+------------+
This table contains a name column which is nothing but description and a table_name column which is actually a table already present in the database.
X001 Table has columns like X1_A, X1_B
X002 Table has columns like X2_A, X2_B
Now I want to concatenate all columns in the actual table present in the TABLE_NAME column in a comma separated string and display that as a column.
+------+------------+------------+
| NAME | TABLE_NAME | COLUMNS |
+------+------------+------------+
| X1 | X001 | X1_A, X1_B |
| X2 | X002 | X2_A, X2_B |
+------+------------+------------+
Now can this be achieved using CTE. I've already successfully created the query using STUFF with XML PATH, but I'm having performance issues because there are like 200 odd rows in the table that I've show above and each subsequent tables linked have like 100 columns each.
EDIT -
SELECT
P.NAME,
P.TABLE_NAME,
[COLUMNS]=(SELECT STUFF((SELECT ',' + NAME FROM sys.syscolumns WHERE ID = OBJECT_ID(P.TABLE_NAME) ORDER BY colorder FOR XML PATH('') ), 1, 1,''))
FROM TABLEX P
Where TABLEX is the table posted above.
Try this one -
DDL:
IF OBJECT_ID (N'dbo.TABLEX') IS NOT NULL
DROP TABLE TABLEX
IF OBJECT_ID (N'dbo.X001') IS NOT NULL
DROP TABLE X001
IF OBJECT_ID (N'dbo.X002') IS NOT NULL
DROP TABLE X002
CREATE TABLE dbo.TABLEX (NAME VARCHAR(50), TABLE_NAME VARCHAR(50))
INSERT INTO dbo.TABLEX (NAME, TABLE_NAME)
VALUES ('X1', 'X001'), ('X2', 'X002')
CREATE TABLE dbo.X001 (X1_A VARCHAR(50), X1_B VARCHAR(50))
CREATE TABLE dbo.X002 (X2_A VARCHAR(50), X2_B VARCHAR(50))
Query:
;WITH cte AS
(
SELECT
NAME
, TABLE_NAME
, [COLUMN] = CAST('' AS VARCHAR(1024))
, POS = 1
FROM TABLEX t
UNION ALL
SELECT
t.NAME
, t.TABLE_NAME
, CAST([COLUMN] + ', ' + c.name AS VARCHAR(1024))
, POS + 1
FROM cte t
JOIN sys.columns c ON
OBJECT_ID('dbo.' + t.TABLE_NAME) = c.[object_id]
AND
t.POS = c.column_id
)
SELECT
NAME
, TABLE_NAME
, [COLUMNS] = STUFF([COLUMN], 1, 2, '')
FROM (
SELECT *, rn = ROW_NUMBER() OVER (PARTITION BY NAME ORDER BY POS DESC)
FROM cte
) t
WHERE t.rn = 1
Results:
NAME TABLE_NAME COLUMNS
------ ------------- -------------
X1 X001 X1_A, X1_B
X2 X002 X2_A, X2_B
Query cost:
Statistic:
Query Presenter Scans Logical Reads
------------------- ----- -------------
XML 5 9
CTE 3 48

Resources