Select multiple values from same column based on 2 columns - sql

Best explained using an example. My trials and results are bellow the example.
There are two tables (in reality I have multiple tables)
TABLE: Products
ID name
-----------
1 apple
2 orange
3 pear
TABLE: ATTRIBUTES
ID prod_ID attr_id value
----------------------------
1 1 101 20
2 1 102 red
3 1 103 sweet
4 2 101 30
5 2 102 orange
6 2 103 sour
6 3 101 40
7 3 102 green
8 3 103 sweet
DESIRED OUTPUT
name attr_id 101 AS 'price' attr_id 102 AS 'taste'
------------------------------------------------------
apple 20 sweet
orange 30 sour
pear 40 sweet
I have managed SQL till now but recently I have had to call 3 tables and combine column values like shown above. I just can't get my head wrapped around this. Help would be greatly appreciated.

Since you have not mentioned any RDBMS in your question, the query below will work on most RDBMS.
SELECT a.Name,
MAX(CASE WHEN b.attr_ID = 101 THEN b.value END) Price,
MAX(CASE WHEN b.attr_ID = 103 THEN b.value END) Taste
FROM Products a
INNER JOIN Attributes b
ON a.ID = b.prod_ID
GROUP BY a.Name
SQLFiddle Demo

You can use CASE statement for that: (This will work in MySQL)
SELECT p.name
,GROUP_CONCAT(CASE WHEN attr_id = 101 THEN value else NULL END) AS price
,GROUP_CONCAT(CASE WHEN attr_id = 102 THEN value else NULL END) AS color
,GROUP_CONCAT(CASE WHEN attr_id = 103 THEN value else NULL END) AS taste
FROM Products p JOIN Attributes a
ON p.id = a.prod_id
GROUP BY p.name;
Or you can also do the same with join: (This will work in both MySQL and SQL Server)
SELECT Name
,CASE WHEN a.attr_id = 101 THEN a.value ELSE NULL END AS price
,CASE WHEN b.attr_id = 102 THEN b.value else NULL END AS color
,CASE WHEN c.attr_id = 103 THEN c.value else NULL END AS taste
FROM Products p
LEFT JOIN Attributes a ON p.id = a.prod_id
LEFT JOIN Attributes b ON p.id = b.prod_id AND a.attr_id = 101 AND b.attr_id = 102
LEFT JOIN Attributes c ON p.id = c.prod_id AND a.attr_id = 101 AND c.attr_id = 103
WHERE a.attr_id IS NOT NULL and b.attr_id IS NOT NULL AND c.attr_id IS NOT NULL
I have also added a column for color.
Output
| NAME | PRICE | COLOR | TASTE |
-----------------------------------
| apple | 20 | red | sweet |
| orange | 30 | orange | sour |
| pear | 40 | green | sweet |
See this SQLFiddle
Demo for SQL Server

You have to query for each column separately...
SELECT
p.name,
(SELECT value FROM Attributes a WHERE attr_id=101 AND a.prod_ID=p.ID) AS price,
(SELECT value FROM Attributes a WHERE attr_id=102 AND a.prod_ID=p.ID) AS taste
FROM Products p
... or ...
If you are using MSSQL 2008 R2 you can use PIVOT:
http://msdn.microsoft.com/en-us/library/ms177410(v=sql.105).aspx

Using PIVOT, easier to adjust for extra attributes.
SELECT NAME, [101],[102],[103]
FROM (
SELECT P.NAME as NAME, A.ATTR_ID as ATTR_ID, A.VALUE as VALUE
FROM PRODUCTS as P, ATTRIBUTES as A
WHERE A.PROD_ID = P.ID
) SrcTable
PIVOT
(
MAX(VALUE)
for ATTR_ID in ([101],[102],[103])
) PivotTable;
Or a dynamic statement could be generated to adjust for a variable number of attributes (since this is the only thing that changes in the above statement)
The column string could be obtained with somethings like:
DECLARE #columns AS NVARCHAR(MAX);
set #columns = STUFF((SELECT distinct ',' + QUOTENAME(ATTR_ID)
FROM ATTRIBUTES
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'),1,1,'');
Next, this could variable could be used to generate a string like the static PIVOT
DECALARE #stmt AS NVARCHAR(MAX);
set #stmt ='SELECT NAME, ' + #columns + '
FROM (
SELECT P.NAME as NAME, A.ATTR_ID as ATTR_ID, A.VALUE as VALUE
FROM PRODUCTS as P, ATTRIBUTES as A
WHERE A.PROD_ID = P.ID
) SourceTable
PIVOT (
MAX(VALUE)
for ATTR_ID in (' + #columns + ')
) PivotTable';
EXECUTE(#stmt);

Related

join two SQL rows in a single one

I have three tables in Postgresql, for a biological classification system.
table lang (languages)
id name
1 português
2 english
-------------------------------
table taxon (biological groups)
id name
...
101 Mammalia
-------------------------------
table pop (popular names)
id tax lang pop
...
94 101 1 mamíferos
95 101 2 mammals
I want to get
id name namePT nameEN
101 Mammalia mamíferos mammals
but my join is giving me
id name pop
101 Mammalia mamíferos
101 Mammalia mammals
select t.id,name,pop from taxon t
left join pop p on p.tax = t.id
where t.id = 101
How can I get the desired result in a single row?
If you are happy to change query every time you add a new language then this query will do the trick:
select t.id,name,pe.pop as eng_pop, pp.pop as port_pop
from taxon t
left join pop pe on pe.tax = t.id and pe.lang = 1
left join pop pp on pp.tax = t.id and pp.lang = 2
where t.id = 101
You could use this
SELECT t.id, t.name,
MAX(CASE WHEN p.lang = 1 THEN p.pop END) AS namePT,
MAX(CASE WHEN p.lang = 2 THEN p.pop END) AS nameEN
FROM taxon t
LEFT JOIN pop p
ON p.tax = t.id
GROUP BY t.id, t.name;
Here's how I got the results:
with base as (
select t.id, t.name,
case when lang = 1 then 'mamiferos' else null end as namePT,
case when lang = 2 then 'mamals' else null end as nameEN
from taxon t
left join pop p on t.id = p.tax
group by 1,2,3, p.lang
)
select
distinct id,
name,
coalesce(namept,'mamiferos',null) as namept,
coalesce(nameen,'mamals',null) as nameen
from base
where id = 101
group by id, name, namept, nameen;
id | name | namept | nameen
-----+----------+-----------+--------
101 | Mammalia | mamiferos | mamals
(1 row)

SQL WHERE clause same column multiple times?

My table looks like this, let's call it Table1:
ID | value | formID
----------------------
25 Business 1001
16 John 1001
5 2/20/17 1001
30 FormXYZ 1001
25 Nursing 2345
16 Sam 2345
5 1/15/17 2345
30 FormXYZ 2345
25 Tech 4500
16 Sam 4500
5 2/1/17 4500
30 FormC 4500
The ID is the unique identifier of that field:
25 = Department
16 = Name
5 = Date
30 = Form Name, we have multiple different Forms, and I just need FormXYZ data.
FormID is a unique ID for each form submitted, the form contains 3 fields.
I have been trying to write a single query that retrieves all data looking something like this if possible:
Department | Name | Date
Business John 2/20/17
Nursing Sam 1/15/17
Here is what I have been trying, nesting and CASE didn't seem to work right for me, so I am posting here and I am right back where I started at.
SELECT value
FROM Table1
WHERE ID = '25'
UNION ALL
SELECT value
FROM Table1
WHERE ID = '16'
UNION ALL
SELECT value
FROM Table1
WHERE ID = '5'
UNION ALL
SELECT value
FROM Table1
WHERE ID = '30' and value = 'FormXYZ'
One way to transpose data in SQL is to use case statements and roll up the data using a group by:
select
formID,
max(case when ID=25 then value else null end) as Department,
max(case when ID=16 then value else null end) as Name,
max(case when ID=5 then value else null end) as Date
from Table1
group by formID
This produces:
formid Department Name Date
1001 Business John 2/20/17
2345 Nursing Sam 1/15/17
4500 Tech Sam 2/1/17
You can add a where clause as needed. This should get the data in a single scan.
select dp.value as department,
n.value as name,
dt.value as date
from
(select formID, value from table1 where id = 25) as dp
inner join (select formID, value from table1 where id = 16) as n
on dp.formID = n.formID
inner join (select formID, value from table1 where id = 5) as dt
on dp.formID = dt.formID
inner join (select formID, value from table1 where id = 30) as f
on dp.formID = f.formID
where f.value = 'FormXYZ';
OR
select
case when id = 25 then value end as department,
case when id = 5 then value end as date,
case when id = 16 then value end as name
from table1
where formId in (select formID from table1
where id = 30 and value = 'FormXYZ')
and id in (5,16,25);

SQL Join w/ some Math thrown in

Heck, maybe 'joining' isn't even involved. I'm way out of my sql league here. Could someone please help me out w/ the following:
Table A
ItemId ItemLookup Price
------- ---------- -----
1 123456 10.00
2 234567 7.00
3 345678 6.00
Table B
ItemId Location Qty QtyOnHold
------- ---------- ----- ---------
1 1 26 20
2 1 0 0
3 1 12 6
1 2 4 0
2 2 2 1
3 2 16 8
What I'm hoping to get is something that looks like
ItemLookup, Price, (qty minus qtyonhold for loc1), (qty minus qtyonhold for loc2)
or 123456, 10.00, 6, 4
Thank you very much for any direction you can provide.
You can use conditional aggregation and a join:
select a.ItemLookup,
sum(case when Location = 1 then Qty - QtyOnHold end) as Location1,
sum(case when Location = 2 then Qty - QtyOnHold end) as Location2
from tableb b join
tablea a
on b.ItemId = a.ItemId
group by a.ItemLookup;
Somthing like this
select tablea.* ,
(select (qty- QtyOnHold) as qty from tableb where ItemId = tablea.ItemId ans Location = 1 ) as qtyl1,
(select (qty- QtyOnHold) as qty from tableb where ItemId = tablea.ItemId ans Location = 2) as qtyl2
from tablea
This assumes that there's only one row in TableB for each ItemID + Location combination. This is basically just a "pivot", you can learn various ways to do this in MySQL here.
SELECT ItemLookup, Price,
MAX(IF(Location = 1, Qty-QtyOnHold, 0)) avail1,
MAX(IF(Location = 2, Qty-QtyOnHold, 0)) avail2
FROM TableA AS a
JOIN TableB AS b ON a.ItemId = b.ItemId
GROUP BY a.ItemId
It seems to me that it may be possible to have a variable number of locations for each item. If this is the case, you need an aggregate function to convert/concatenate multiple rows into a column.
Here's an example with MySQL's group_concat function:
select a.itemlookup,a.price,group_concat('loc ',location,'=',b.x order by location) as qty_minus_qtyonhold
from tablea a,(select itemid,location,qty-qty_onhold x from tableb
group by itemid,location) as b
where a.itemid = b.itemid
group by 1
You'll get a result like this:
itemlookup price qty_minus_qtyonhold
---------- ------ ------------------
123456 10.00 loc 1=6,loc 2=4
234567 7.00 loc 1=0,loc 2=1
345678 6.00 loc 1=6,loc 2=8
Not sure what DBMS you're using but there are similar alternatives for Oracle and SQL Server

How do I collapse a result set in SQLite?

Let's say I have table
id name
1 nora
2 mars
3 ven
and I left join on id with this table,
id type value
1 clothing shirt
1 clothing pants
1 toys abacus
1 toys legos
...
how do I produce something that looks like,
id name clothing toys
1 nora shirt, pants abacus, legos
A simple join does not help if you want to put different values into different result columns.
You need to use correlated subqueries:
SELECT id,
name,
(SELECT group_concat(value, ', ')
FROM Table2
WHERE id = Table1.id
AND type = 'clothing'
) AS clothing,
(SELECT group_concat(value, ', ')
FROM Table2
WHERE id = Table1.id
AND type = 'toys'
) AS toys
FROM Table1

Very challenging SQL interview (can't use stored procedure)

You have a SQL table table with two columns: name and pen. Both columns are text strings.
name | pen
---------------
mike | red
mike | red
mike | blue
mike | green
steve | red
steve | yellow
anton | red
anton | blue
anton | green
anton | black
alex | black
alex | green
alex | yellow
alex | red
Person's name is given as the input argument.
Please write a SQL statement (not a stored procedure) which returns names of persons having unique set of pens which is equivalent or wider/bigger than the set of pens of given person.
Examples:
input: mike
output: anton
Mike has (red, blue, green).
Anton has more gadgets (red, blue, green) + black.
input: steve
output: alex
Steve has (red, yellow).
Alex has (red, yellow) + green+ black.
Mike, Anton aren't printed - they do not have yellow.
input: alex
output:
Here's one way (Online Demo), assuming input name "steve".
This can be rephrased as "Looking for all users for which there does not exist a pen owned by steve that they do not own"
SELECT DISTINCT name
FROM table t1
WHERE NOT EXISTS (SELECT *
FROM table t2
WHERE name = 'steve'
AND NOT EXISTS (SELECT *
FROM table t3
WHERE t2.pen = t3.pen
AND t1.name = t3.name))
AND t1.name <> 'steve' /*Exclude input name from results*/
See this article for other techniques
with test1 as
(select a.name nm, count(distinct a.pen) ct
from table a, table b
where b.pen = a.pen
and b.name = 'anton'
group by a.name
order by 2 desc),
test2 as
(select nm name1
from test1
where ct = (select max(ct) from test1))
select distinct c.name
from table c
where c.name in (select name1
from test2
where name1 not in (case
when (select count(distinct name1) from test2) > 1 then
'anton'
else
' '
end))
Table Format:
E_NAME E_PEN
----------------
mike green
mike blue
mike red
mike red
steve red
steve yellow
anton red
anton blue
anton green
anton black
alex black
alex green
alex yellow
alex red
Query:
SELECT A.E_NAME, A.E_PEN
FROM V_NAME A, V_NAME B
WHERE TRIM(UPPER(A.E_NAME)) = TRIM(UPPER(B.E_NAME))
AND TRIM(UPPER(A.E_NAME)) != 'MIKE'
AND TRIM(UPPER(A.E_PEN)) = TRIM(UPPER(B.E_PEN(+)))
Procedure to take Input from user:
CREATE OR REPLACE PROCEDURE E_TEST(I_NAME VARCHAR2) AS
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE E_TABLE AS
SELECT A.E_NAME,A.E_PEN FROM V_NAME A,V_NAME B
WHERE TRIM(UPPER(A.E_NAME)) = TRIM(UPPER(B.E_NAME))
AND TRIM(UPPER(A.E_NAME)) != ''' ||
I_NAME || '''
AND TRIM(UPPER(A.E_PEN))= TRIM(UPPER(B.E_PEN(+)))';
END;
Name: Nikhil Shinde
E-Mail: nikhilshinde3jun#gmail.com
declare #t table (name nvarchar(20), pen nvarchar(20))
insert into #t (name,pen)
values('mike','red'),
('mike','red'),
('mike','blue'),
('mike','green'),
('steve','red'),
('steve','yellow'),
('anton','red'),
('anton','blue'),
('anton','green'),
('anton','black'),
('alex','black'),
('alex','green'),
('alex','yellow'),
('alex','red')
declare #input nvarchar(20) = 'mike';
with cte_input (name, pen)
as
(
select distinct name, pen
from #t
where name = #input
)
, cte_colors (name, matches)
as
(
select name, matches = count(distinct pen)
from #t
where name != #input
and pen in (select pen from cte_input)
group by name
having count(distinct pen) = (select count(1) from cte_input)
)
select t.name
from #t t
join cte_colors m on m.name = t.name
group by t.name, m.matches
having count(distinct t.pen) > m.matches
We can use LEAD analytical function to achieve this solution.
The query would be
select next_name from
(select name, count(pen) CNT, LEAD(name,1) over (order by count(pen)) next_name
from table
group by name order by CNT
)
where name=input_value;
the sub query will give the results as below
name | CNT | next_name
mike | 3 | Steve
steve | 2 | anton
anton | 4 | alex
alex | 4 | (null)
Then the out query filter the row required and gives the next_name which is what we are looking for.
create table practise
(
name varchar (10),pens varchar (10)
)
insert practise (name,pens)
select 'mike','red' union
select 'mike','red' union
select 'mike','blue' union
select 'mike','green' union
select 'steve','red' union
select 'steve','yellow' union
select 'anton','blue' union
select 'anton','green' union
select 'anton','black' union
select 'anton','red' union
select 'alex','black' union
select 'alex','green' union
select 'alex','yellow' union
select 'alex','red'
select * into #t from practise
update #t
set Colourcode = 1 where pens = 'black'
go
update #t
set Colourcode = 2 where pens = 'blue'
go
update #t
set Colourcode = 3 where pens = 'green'
go
update #t
set Colourcode = 4 where pens = 'red'
go
update #t
set Colourcode = 5 where pens = 'yellow'
select distinct colourcode from #t
/* IMP Code /
update #t
set Number =
(
select count() from #t
where name = 'steve' and colourcode = 1
)
where name = 'steve' and colourcode = 1
/* once the above update statement is exected change colourcode = 2 and again run the statement
change colourcode = 3 and again run the statement
change colourcode = 4 and again run the statement
change colourcode = 5 and again run the statement*/
/* Follow the same process for name 'alex','anton' and 'mike' */
/* Below is the Solution*/
select name from #t
where number =
(
select distinct number from #t
where colourcode = 3 and Number =1)
Declare #name nvarchar(20)
set #name = 'steve';
with cte_in
as
(
select distinct Pen from tblNamePen where Name = #name
)
select Name from
(
select t1.name, count(distinct t1.Pen) ct from tblNamePen t1
join cte_in inp on t1.Name <> #name
where t1.pen = inp.Pen
group by t1.Name
) a
where a.ct >= (select count(Pen) from cte_in)