Pivot group multi column SQL - sql

In SQL, how can I merge multiple columns into one column with multiple rows?
Example:
name | age | gender
------+-------+---------
John | 20 | M
Jill | 21 | F
Exam | 22 | M
I want to get this table:
Exam | John | Jill
------+-------+---------
22 | 21 | 20
M | F | M

You can do like this.
Use two PIVOT query with UNION ALL to combine them
SELECT CAST(Exam AS VARCHAR(10)) Exam,
CAST(Jill AS VARCHAR(10)) Jill,
CAST(John AS VARCHAR(10)) John
FROM
(
select age,name
from T
) as x
PIVOT
(
MAX(Age) FOR name IN ([Exam],[John],[Jill])
)AS P1
UNION ALL
SELECT Exam,Jill,John FROM
(
select name,gender
from T
) as x
PIVOT
(
MAX(gender) FOR name IN ([Exam],[John],[Jill])
)AS P1
sqlfiddle:http://sqlfiddle.com/#!18/a437d/6

You can do this using a single query -- basically unpivot and conditional aggregation:
select max(case when v.name = 'Exam' then v.val end) as exam,
max(case when v.name = 'John' then v.val end) as john,
max(case when v.name = 'Jill' then v.val end) as jill
from t cross apply
(values (t.name, cast(t.age as varchar(10)), 1),
(t.name, t.gender, 2)
) v(name, val, which)
group by which;
Here is the SQL Fiddle.
You can convert the values to whatever character type you like for compatibility among the values. You want to put numeric values and strings in the same column, so they have to have the same type.

Related

Convert column value to row value

In SQL Server, I am trying to convert the from table 1 to table 2. From reading other answers from stack overflow, I can do some sort of row_number(). But the problem is I need do some inner join after the conversion because the following script use max() aggregate function, it kind force other fields from other tables to have some sort of aggregate function as well. So I was wondering if there is an alternative approach to solve this problem? Or if there is a way to handle this aggregate function when do join with another table.
select max(case when key = 'ab' then Value end) as ab,
max(case when key = 'cd' then Value end) as cd
from (select t.*,
row_number() over (partition by key order by Value) as seq
from table t
) t
group by seq;
table 1
table 2
You can try with this below script-
SELECT id,
MAX(CASE WHEN name = 'car1' THEN name END) car1,
MAX(CASE WHEN name = 'car2' THEN name END) car2,
MAX(CASE WHEN name = 'car3' THEN name END) car3
FROM your_table
GROUP BY id
You can go for PIVOT feature.
;WITH src as
(
SELECT *
FROM
(
VALUES
(1, 'Car1', 'nissan'),
(1, 'Car2', 'audi'),
(1, 'Car3', 'toyota')
) as t (id, name, value)
)
SELECT *
FROM src
PIVOT
(
max(VALUE) FOR NAME IN ([Car1], [Car2], [Car3])
) as pvt
+----+--------+------+--------+
| id | Car1 | Car2 | Car3 |
+----+--------+------+--------+
| 1 | nissan | audi | toyota |
+----+--------+------+--------+

Ranking counts using an SQL query

I am using DB Browser for SQLite. I have a table in the following format:
+-----------+-------------------------------------+
| search_id | search_town |
+-----------+-------------------------------------+
| 1 | town1,town3 |
| 2 | town2,town4,town5 |
| 3 | town3,town5 |
| 4 | town2,town5 |
| 5 | town2,town3,town4 |
+-----------+-------------------------------------+
I would like to do a COUNT on the number of times town1 through town5 has appeared under search_town, and then rank in descending order the towns based on their respective counts. So far I have the following query:
SELECT SUM(CASE WHEN search_location LIKE '%town01%' THEN 1 ELSE 0 END) AS town01,
SUM(CASE WHEN search_location LIKE '%town02%' THEN 1 ELSE 0 END) AS town02,
SUM(CASE WHEN search_location LIKE '%town03%' THEN 1 ELSE 0 END) AS town03,
SUM(CASE WHEN search_location LIKE '%town04%' THEN 1 ELSE 0 END) AS town04,
SUM(CASE WHEN search_location LIKE '%town05%' THEN 1 ELSE 0 END) AS town05
FROM searches
...but am unable to do an ORDER BY as the towns and their counts are output as columns instead of rows in this format
+-------+-------+-------+-------+-------+
| town1 | town2 | town3 | town4 | town5 |
+-------+-------+-------+-------+-------+
| 12 | 31 | 12 | 24 | 12 |
+-------+-------+-------+-------+-------+
Is there another approach to this? Appreciate any comments.
You are turning your output in a single row using CASE WHEN, to convert it into multiple rows, you can try like following.
;WITH cte
AS (SELECT *
FROM (VALUES ('Town1'),
('Town2'),
('Town3'),
('Town4'),
('Town5')) T(town))
SELECT Count(*) [Count],
C.town
FROM [TABLE_NAME] T
INNER JOIN cte C
ON T.search_location LIKE '%' + C.town + '%'
GROUP BY C.town
ORDER BY Count(*) DESC
Online DEMO
Another approach can be using UNION ALL.
SELECT *
FROM (SELECT Count(*) s,
'Town1' AS Col
FROM tablename
WHERE search_location LIKE '%town1%'
UNION ALL
SELECT Count(*) s,
'Town2' AS Col
FROM tablename
WHERE search_location LIKE '%town2%'
UNION ALL
SELECT Count(*) s,
'Town3' AS Col
FROM tablename
WHERE search_location LIKE '%town3%'
UNION ALL
SELECT Count(*) s,
'Town4' AS Col
FROM tablename
WHERE search_location LIKE '%town4%'
UNION ALL
SELECT Count(*) s,
'Town5' AS Col
FROM tablename
WHERE search_location LIKE '%town5%') t
ORDER BY s DESC
You can use a recursive common-table expression (CTE) to turn the comma-separated list into a set of rows. When the table is normalized, you can group by town and sort by descending count:
WITH rec(town, remain)
AS (
SELECT SUBSTR(search_town, 0, INSTR(search_town, ',')) -- Before ,
, SUBSTR(search_town, INSTR(search_town, ',')+1) || ',' -- After ,
FROM t1
UNION ALL
SELECT SUBSTR(remain, 0, INSTR(remain, ',')) -- Before ,
, SUBSTR(remain, INSTR(remain, ',')+1) -- After ,
FROM rec
WHERE LENGTH(remain) > 0
)
SELECT town
, COUNT(*)
FROM rec
GROUP BY
town
ORDER BY
COUNT(*) DESC
Idea from this blog post. Working example at sqliteonline.

How to write a query to allow null in minimum function

I need to write a query to get minimum values for a column from a table and if the value is null then I want to include that row. I wrote following query but it ignores the null values. How I can modify this query to include null values in the result?
select * from TABLE where COLUMN = (select min(COLUMN) from TABLE );
If the table is like below
|ID | VALUE | NAME
101 1 John
101 null John
102 1 Bill
103 1 Tina
103 null Tina
104 null James
Result Should be
|ID | VALUE | NAME
101 1 John
102 1 Bill
103 1 Tina
104 null James
You need distinct on:
with my_table(id, value, name) as (
values
(101, 1, 'John'),
(101, null, 'John'),
(102, 1, 'Bill'),
(103, 1, 'Tina'),
(103, null, 'Tina'),
(104, null, 'James')
)
select distinct on (id) *
from my_table
order by id, value
id | value | name
-----+-------+-------
101 | 1 | John
102 | 1 | Bill
103 | 1 | Tina
104 | | James
(4 rows)
Distinct on is a fantastic feature specific for Postgres. An alternative in other RDBMS may be:
select t.id, t.value, t.name
from my_table t
join (
select id, min(value) as value
from my_table
group by id
) u on u.id = t.id and u.value is not distinct from t.value;
Note, you should use is not distinct from because value may be null.
SQL SERVER
select DISTINCT j.ID,j.VALUE,j.NAME from Table1 j
join (
select id, MIN(VALUE) VALUE from Table1
group by id
) as t
on t.ID = j.ID and (t.VALUE = j.VALUE or t.VALUE is null)
You cannot do an equals (=) for a null value, you have to check is null or so. So one simple solution is to default the null value to a number that would not otherwise be used:
select * from TABLE where coalesce(COLUMN, -9999) = (select min(coalesce(COLUMN,-9999)) from TABLE );
The coalesce function returns the first non-null value passed to it.
with c as (
select column as c
from table
order by column nulls first
limit 1
)
select *
from table cross join c
where column = c or column is null
If you want to user order by:
select t.*
from t
order by t.column asc nulls first
limit 1;
Alternatively, use rank():
select t.*
from (select t.*,
rank() over (order by col asc nulls first) as seqnum
from t
) t
where seqnum = 1;
I hope this solve your problem.
SELECT id,
CASE WHEN MIN(
CASE WHEN value IS NULL THEN 0 ELSE 1 END) = 0 THEN null
ELSE MIN(value) END
FROM tableName
GROUP BY id
or using COALESCE.
SELECT id,
CASE WHEN MIN(COALESCE(value, 0)) = 0 THEN null
ELSE MIN(value) END
FROM tableName
GROUP BY id
I am on mobile phone now, so I cannot test.

SQL query create cross column

I have this table
customer | product | quantity
-------------------------------
CLI01 | A | 10
CLI01 | B | 20
CLI02 | A | 31
CLI03 | A | 10
CLI03 | C | 12
and I want to create in SQL Server this output:
customer | crossProduct | quantity
-----------------------------------
CLI01 | A+B | 30
CLI02 | Only A | 31
CLI03 | B+C | 22
Thanks in advance
Niko
If you only care about two products, then this is simple aggregation:
select customer,
(case when count(distinct product) > 2 then 'Lots of Products'
when min(product) = max(product) then 'Only ' + min(product)
else min(product) + '+' + max(product)
end) as crossproduct,
sum(quantity)
from t
group by customer;
If you care about more than two products, then you'll need to do aggregation string concatenation. That is a bit painful in SQL Server. Start by Googling "sql server aggregate string concatenation".
This is s sample:
----- Test Data ----------
DECLARE #TestData TABLE (customer VARCHAR(10),product VARCHAR(10),quantity INT)
INSERT INTO #TestData
SELECT 'CLI01','A',10 UNION ALL
SELECT 'CLI01','B',20 UNION ALL
SELECT 'CLI02','A',31 UNION ALL
SELECT 'CLI03','A',10 UNION ALL
SELECT 'CLI03 ','C',12
----- Query -------------
SELECT customer,CASE WHEN COUNT( DISTINCT t.product)=1 THEN 'Only ' ELSE '' END + LEFT(c.product,LEN(c.product)-1) AS Product,SUM(quantity) AS quantity
FROM #TestData AS t
CROSS APPLY(SELECT a.product+'+' FROM #TestData AS a WHERE a.customer=t.customer FOR XML PATH('')) c(product)
GROUP BY customer,c.product
ORDER BY t.customer
customer Product quantity
CLI01 A+B 30
CLI02 Only A 31
CLI03 A+C 22
Have you tried using stuff? This will give you what you need. Works with as many products as necessary, from sql 2008 onwards.
CREATE TABLE x (customer VARCHAR (20), product CHAR(1), quantity INT )
INSERT INTO x
VALUES( 'CLI01', 'A', 10),
( 'CLI01', 'B', 20),
( 'CLI02', 'A', 31),
( 'CLI03', 'A', 10),
( 'CLI03', 'C', 12)
SELECT x1.customer, x3.Products, SUM(x1.quantity)
FROM x x1
CROSS APPLY ( SELECT Products = STUFF( (select '+' + product AS [text()]
FROM x x2
WHERE x2.customer = x1.customer
FOR XML PATH ('') ), 1, 1,'') ) x3
GROUP BY x1.customer, x3.Products

Display multiple rows and column values into a single row, multiple column values

I have to show multiple incomes, type of income and employer name values for a single individual in a single row. So, if 'A' has three different incomes from three different sources,
id | Name | Employer | IncomeType | Amount
123 | XYZ | ABC.Inc | EarningsformJob | $200.00
123 | XYZ | Self | Self Employment | $300.00
123 | XYZ. | ChildSupport| Support | $500.00
I need to show them as
id | Name | Employer1 | Incometype1| Amount1 | Employer2 | incometype2 | Amount2| Employer3 | Incometype3| Amount3.....
123 |XYZ | ABC.Inc |EarningsformJob | $200.00|Self | Self Employment | $300.00|ChildSupport| Support | $500.00.....
I need both 'fixed number of columns' (where we know how many times employer, incometype and amount colums are going to repeat)logic and 'dynamic display of columns' ( unknown number of times these columns are going to repeat)
Thanks.
Since you are using SQL Server there are several ways that you can transpose the rows of data into columns.
Aggregate Function / CASE: You can use an aggregate function with a CASE expression along with row_number(). This version would require that you have a known number of values to become columns:
select id,
name,
max(case when rn = 1 then employer end) employer1,
max(case when rn = 1 then IncomeType end) IncomeType1,
max(case when rn = 1 then Amount end) Amount1,
max(case when rn = 2 then employer end) employer2,
max(case when rn = 2 then IncomeType end) IncomeType2,
max(case when rn = 2 then Amount end) Amount2,
max(case when rn = 3 then employer end) employer3,
max(case when rn = 3 then IncomeType end) IncomeType3,
max(case when rn = 3 then Amount end) Amount3
from
(
select id, name, employer, incometype, amount,
row_number() over(partition by id order by employer) rn
from yourtable
) src
group by id, name;
See SQL Fiddle with Demo.
PIVOT/UNPIVOT: You could use the UNPIVOT and PIVOT functions to get the result. The UNPIVOT converts your multiple columns of Employer, IncomeType and Amount into multiples rows before applying the pivot. You did not specific what version of SQL Server, assuming you have a known number of values then you could use the following in SQL Server 2005+ which uses CROSS APPLY with UNION ALL to unpivot:
select id, name,
employer1, incometype1, amount1,
employer2, incometype2, amount2,
employer3, incometype3, amount3
from
(
select id, name, col+cast(rn as varchar(10)) col, value
from
(
select id, name, employer, incometype, amount,
row_number() over(partition by id order by employer) rn
from yourtable
) t
cross apply
(
select 'employer', employer union all
select 'incometype', incometype union all
select 'amount', cast(amount as varchar(50))
) c (col, value)
) src
pivot
(
max(value)
for col in (employer1, incometype1, amount1,
employer2, incometype2, amount2,
employer3, incometype3, amount3)
) piv;
See SQL Fiddle with Demo.
Dynamic Version: Lastly, if you have an unknown number of values then you will need to use dynamic SQL to generate the result.
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(col+cast(rn as varchar(10)))
from
(
select row_number() over(partition by id order by employer) rn
from yourtable
) d
cross apply
(
select 'employer', 1 union all
select 'incometype', 2 union all
select 'amount', 3
) c (col, so)
group by col, rn, so
order by rn, so
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT id, name,' + #cols + '
from
(
select id, name, col+cast(rn as varchar(10)) col, value
from
(
select id, name, employer, incometype, amount,
row_number() over(partition by id order by employer) rn
from yourtable
) t
cross apply
(
select ''employer'', employer union all
select ''incometype'', incometype union all
select ''amount'', cast(amount as varchar(50))
) c (col, value)
) x
pivot
(
max(value)
for col in (' + #cols + ')
) p '
execute(#query);
See SQL Fiddle with Demo. All versions give a result:
| ID | NAME | EMPLOYER1 | INCOMETYPE1 | AMOUNT1 | EMPLOYER2 | INCOMETYPE2 | AMOUNT2 | EMPLOYER3 | INCOMETYPE3 | AMOUNT3 |
-------------------------------------------------------------------------------------------------------------------------------------
| 123 | XYZ | ABC.Inc | EarningsformJob | 200 | ChildSupport | Support | 500 | Self | Self Employment | 300 |