Say I have two tables in a MySQL Database.
Table 1:
ID Name
1 Jim
2 Bob
3 John
Table 2:
ID key value
1 address "X Street"
1 city "NY"
1 region "NY"
1 country "USA"
1 postal_code ""
1 phone "123456789"
When selecting rows from the database, is there any way to join rows from the second table as columns to the first table?
The desired result right from the MySQL query is:
ID Name address city region country postal_code phone
1 Jim X Street NY NY USA NULL 123456789
2 Bob NULL NULL NULL NULL NULL NULL
3 John NULL NULL NULL NULL NULL NULL
Thanks for any help!
This type of data transformation is known as a PIVOT. MySQL doesn't have a pivot function but you can replicate it using an aggregate function with a CASE expression:
select t1.id,
t1.name,
max(case when t2.`key` = 'address' then t2.value end) address,
max(case when t2.`key` = 'city' then t2.value end) city,
max(case when t2.`key` = 'region' then t2.value end) region,
max(case when t2.`key` = 'country' then t2.value end) country,
max(case when t2.`key` = 'postal_code' then t2.value end) postal_code,
max(case when t2.`key` = 'phone' then t2.value end) phone
from table1 t1
left join table2 t2
on t1.id = t2.id
group by t1.id, t1.name
See SQL Fiddle with Demo.
This could also be written using multiple joins on your table2 and you would include a filter on the join for each key:
select t1.id,
t1.name,
t2a.value address,
t2c.value city,
t2r.value region,
t2y.value country,
t2pc.value postal_code,
t2p.value phone
from table1 t1
left join table2 t2a
on t1.id = t2a.id
and t2a.`key` = 'address'
left join table2 t2c
on t1.id = t2c.id
and t2c.`key` = 'city'
left join table2 t2r
on t1.id = t2r.id
and t2c.`key` = 'region'
left join table2 t2y
on t1.id = t2y.id
and t2c.`key` = 'country'
left join table2 t2pc
on t1.id = t2pc.id
and t2pc.`key` = 'postal_code'
left join table2 t2p
on t1.id = t2p.id
and t2p.`key` = 'phone';
See SQL Fiddle with Demo.
The above two versions will work great if you have a limited number of key values. If you have an unknown number of values, then you will want to look at using a prepared statement to generate dynamic SQL:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'max(case when t2.`key` = ''',
`key`,
''' then t2.value end) AS `',
`key`, '`'
)
) INTO #sql
from Table2;
SET #sql
= CONCAT('SELECT t1.id, t1.name, ', #sql, '
from table1 t1
left join table2 t2
on t1.id = t2.id
group by t1.id, t1.name;');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See SQL Fiddle with Demo
All versions will give a result:
| ID | NAME | ADDRESS | CITY | REGION | COUNTRY | POSTAL_CODE | PHONE |
|----|------|----------|--------|--------|---------|-------------|-----------|
| 1 | Jim | X Street | NY | (null) | (null) | (null) | 123456789 |
| 2 | Bob | (null) | (null) | (null) | (null) | (null) | (null) |
| 3 | John | (null) | (null) | (null) | (null) | (null) | (null) |
You have a structure called entity-attribute-value in the second table. There are two ways to do the combination. I think the aggregation method is the easier to express:
select t1.name,
max(case when `key` = 'address' then value end) as address,
max(case when `key` = 'city' then value end) as city,
max(case when `key` = 'region' then value end) as region,
max(case when `key` = 'country' then value end) as country,
max(case when `key` = 'postal_code' then value end) as postal_code,
max(case when `key` = 'phone' then value end) as phone
from table1 t1 left join
table2 t2
on t1.id = t2.id
group by t1.name;
The second method is to do separate joins for each value:
select t1.name, address.value, city.value, . . .
from table1 t1 left join
table2 address
on t1.id = address.id and address.`key` = 'Address' left join
table2 city
on t1.id = city.id and city.`key` = 'City' . . .
Depending on the structure of the data, the join method can actually be faster in MySQL when it uses appropriate indexing. (Other databases have been algorithms for aggregation, so the group by method often works well in other databases.)
Related
OK, so I'm using node and MySQL, having issues with a query.
I have three tables, we'll call them T1, T2, and T3. T1 has a primary key, we'll call it T1.id. T3 also has a primary key, T3.id.
T2 has a primary key, T2.id, and foreign keys which point to T1.id (FK1) and T3.id (FK3). So now let's look at a sample data set:
T1_____ T2________ T3_____
id -name | id -FK1 -FK3 | id -name
1 johnny | 1 .... 1 .... 1 .. | 1 .. MN
2 william | 2 .... 1 .... 2 .. | 2 .. FL
3 joseph | 3 .... 1 .... 3 .. | 3 .. CA
4 bobbie | 4 .... 2 .... 2
------------| 5 .... 2 .... 3
------------| 6 .... 3 .... 1
------------| 7 .... 3 .... 2
------------| 8 .... 3 .... 3
I want a query that will return columns from T1, and then join (unknown number of) columns of T2 for each match, and then for each T2 match, join columns from T3 for its corresponding match.
So, for example, the first row would be:
T1.name T2.1.id T2.1.T3.name T2.2.id T2.2.T3.name T2.3.id T2.3.T3.name
johnny 1 MN 2 FL 3 CA
*** Sorry for the formatting. Is this doable, or do I need to use a non-relational DB like Mongo or Couchbase?
Simplified solution: Merge all the data from T3 table into one column in the result set.
SELECT t1.name,
GROUP_CONCAT(t3.id,' ',t3.name SEPARATOR ', ') as data
FROM t1
JOIN t2
ON t2.fk1 = t1.id
JOIN t3
ON t3.id = t2.fk2
GROUP BY t1.id;
It would produce result
T1.name | data
johnny | 1 MN, 2 FL, 3 CA
with configurable delimiters.
Extended solution: Prepare dynamic query taking advantage of pivoting:
SELECT GROUP_CONCAT('MAX(CASE WHEN ind=', #i := #i+1, ' THEN x.t3id ELSE NULL END) AS col', #i, 'id,MAX(CASE WHEN ind=', #i, ' THEN x.name ELSE NULL END) AS col', #i, 'name')
FROM t2,
(SELECT #i := 0) init
WHERE t2.fk1 = (
SELECT fk1
FROM (
SELECT t2.fk1,
COUNT(t2.fk2) as cnt
FROM t2
GROUP BY t2.fk1
ORDER BY cnt DESC
LIMIT 1
) tmp
)
INTO #sql;
SET #sql = CONCAT('SELECT x.t1name,', #sql, ' FROM ( SELECT #i := IF(#id = tmp.id, #i+1, 1) as ind, #id := tmp.id as id, tmp.t1name, tmp.t3id, tmp.name FROM (SELECT t1.id, t1.name as t1name, t3.id as t3id, t3.name FROM t1 JOIN t2 ON t2.fk1 = t1.id JOIN t3 ON t3.id = t2.fk2,(SELECT #i := 1, #id := 0) init ORDER BY t1.id) tmp) x GROUP BY x.id');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
This approach will create and execute query looking like:
SELECT x.t1name,
MAX(CASE WHEN ind = 1 THEN x.t3id ELSE NULL END) AS col1id,
MAX(CASE WHEN ind = 1 THEN x.name ELSE NULL END) AS col1name,
MAX(CASE WHEN ind = 2 THEN x.t3id ELSE NULL END) AS col2id,
MAX(CASE WHEN ind = 2 THEN x.name ELSE NULL END) AS col2name,
MAX(CASE WHEN ind = 3 THEN x.t3id ELSE NULL END) AS col3id,
MAX(CASE WHEN ind = 3 THEN x.name ELSE NULL END) AS col3name
FROM (
SELECT #i := IF(#id = tmp.id, #i+1, 1) as ind,
#id := tmp.id as id,
tmp.t1name,
tmp.t3id,
tmp.name
FROM (
SELECT t1.id,
t1.name as t1name,
t3.id as t3id,
t3.name
FROM t1
JOIN t2
ON t2.fk1 = t1.id
JOIN t3
ON t3.id = t2.fk2,
(SELECT #i := 1, #id := 0) init
ORDER BY t1.id
) tmp
) x
GROUP BY x.id;
Which gives the following result:
t1name | col1id | col1name | col2id | col2name | col3id | col3name
johnny | 1 | MN | 2 | FL | 3 | CA
william | 2 | FL | 3 | CA | NULL | NULL
There is one limitation here however, regarding the group_concat function
The result is truncated to the maximum length that is given by the group_concat_max_len system variable, which has a default value of 1024.
Try the following way using LEFT JOIN and group_concat
select
T1.name,
group_concat(T2.id) as T2Ids,
group_concat(T3.name) as States
from T1 left join T2 ON T1.id = T2.FK1
left join T3 ON T3.id = T2.FK3
group by T1.id
The following are my mysql tables
Table 1:
ID | commonID | Date | text | active
1 11 01.02 abc 1
2 11 02.02 123 1
3 11 03.02 xyz 0
Table 2:
ID | commonID | Date | value | active
1 11 01.02 abc 1
2 11 04.02 123 1
3 11 03.02 xyz 1
The Final result should display this:
| date | text | value
01.02 abc abc
02.02 123 (null)
03.02 (null) xyz
04.02 (null) 123
The Idea here is, to merge the two tables. All entries with a defined commonID like 11 in the example will be selected from both tables. then the tables will be united.
Conditions:
If there are matching dates in TABLE1 and TABLE2 they will be merged
If there is a solo date in TABLE1 or TABLE2, the value/text for the table with no date will become NULL
If there is a record in TABLE1 or TABLE2 that has active = FALSE, it will not be processed.
There can be matching and not matching dates in BOTH tables.
I want to use this for display chronologic events, if there is an event in both tables, there should be only one line for this.
What could be the Solution here?
Try this:
SELECT T1.date,
CASE WHEN T1.active = 1 THEN T1.text END as text,
CASE WHEN T2.active =1 THEN T2.value END as value
FROM Table1 T1 LEFT JOIN
Table2 T2 ON T1.date=T2.date
UNION
SELECT T2.date,
CASE WHEN T1.active = 1 THEN T1.text END as test,
CASE WHEN T2.active = 1 THEN T2.value END as value
FROM Table1 T1 RIGHT JOIN
Table2 T2 ON T1.date=T2.date
Result:
DATE TEXT VALUE
01.02 abc abc
02.02 123 (null)
03.02 (null) xyz
04.02 (null) 123
Sample SQL Fiddle.
Try this:
SELECT t1.date,t1.text,t2.value FROM table1 t1
LEFT JOIN table2 t2 ON t1.commonId = t2.commonId and t1.date = t2.date and t2.active = 1
where t1.active = 1
UNION
SELECT t2.date,t1.text,t2.value FROM table2 t2
LEFT JOIN table1 t1 ON t1.commonId = t2.commonId and t1.date = t2.date and t1.active = 1
where t2.active = 1
Sample http://sqlfiddle.com/#!2/d2c4d/2
Here's one way...
SELECT commonid
, date
, MAX(text) text
, MAX(value) value
FROM
( SELECT id
, commonid
, date
, text
, NULL value
, active
FROM table1
WHERE active <> 0
UNION
SELECT id
, commonid
, date
, NULL
, value
, active
FROM table2
WHERE active <> 0
) x
GROUP
BY commonid,date;
You can move the WHERE active <> 0 bit out of the UNIONs if you like, to just before the GROUP BY.
We have a log table where user processes log entries (success/failure/timeout) each time they run. For e.g.
+----+----------+---------+
| id | username | status |
+----+----------+---------+
| 1 | saqib | success |
| 2 | mary | timeout |
| 3 | scott | timeout |
| 4 | saqib | success |
| 5 | mary | timeout |
| 6 | scott | timeout |
| 7 | saqib | timeout |
| 8 | mary | timeout |
| 9 | scott | timeout |
+----+----------+---------+
We would like to get a usernames which have had a success in the past the but the latest entry for them was a timeout. (saqib in the above example)
Is there single query that can do this? Right now we are doing this using a PHP script, but would like to use mySQL query for this.
Thanks
SQL Fiddle
SELECT DISTINCT m1.username
FROM
(
SELECT s1.username, s1.ids
FROM
(
SELECT username, MAX(id) as ids
FROM MyTable
GROUP BY username
) AS s1
INNER JOIN MyTable AS s2
ON s1.ids = s2.id
WHERE s2.status = 'timeout'
) AS m1
INNER JOIN MyTable m2 ON m1.username = m2.username
AND m2.status = 'success'
You can retrieve the latest id for each username and then JOIN it with the original table checking if there were entries for each user with status success and id less then maximum.
SELECT t.*
FROM ( SELECT username
, MAX(id) as ind
FROM tbl
GROUP BY username
) x JOIN tbl t ON t.username = x.username
AND t.id = x.ind
AND t.status IN ('timeout', 'failure')
AND EXISTS ( SELECT *
FROM tbl
WHERE username = x.username
AND id < x.ind
AND status = 'success'
)
Example
I would use exists for this problem. Exists are nice because they generally are faster than joining to the table. Unrelated, but I would recommend using a time stamp as opposed to relying on the id number.
Select username
From table t1
Where t1.status = 'timeout'
and exists (Select 1
From table t2
Where t1.username = t2.username
and t2.status = 'success'
Limit 1)
and not exists (Select 1
From table t3
Where t3.username = t1.username
and t3.id > t1.id
Limit 1);
SELECT
UserName, max(id)
FROM TABLE
WHERE
UserName IN (SELECT UserName from Table where Status = 'Success')
GROUP BY UserName
Having MAX(id) = (select max(id) from username where status = 'timeout')
You can join the table to itself:
SELECT GROUP_CONCAT(CONCAT_WS(' -> ', a.id, b.id) SEPARATOR ','), a.username
FROM t a
JOIN t b USING (username)
WHERE b.id > a.id
AND (a.status = 'success'
AND b.status = 'timeout')
GROUP BY a.username;
This shows all pairs of previous success to later timeout.
SQLFiddle
You can do so by joining 2 subqueries 1 for the maximum id per user with success status which will satisfy the condition which have had a success in the past and 2 for the max id to get users with latest timeout last comparison part t1.id < t2.id will satisfy the user should have success in past
select * from
(select `username`,max(id) id
from t
where `status` ='success'
group by `username`
) t1
join
(
select `username`,max(id) id
from t
where `status` ='timeout'
group by `username`
) t2
on(t1.username = t2.username)
where t1.id < t2.id
Demo
Another solution this will be much cleaner bu just using one query with max and case
select
username,
max(case when `status` ='success' then id else 0 end) success,
max(case when `status` ='timeout' then id else 0 end) timeout
from t
group by username
having timeout > success
and success > 0
Demo 2
I'm trying to select just records which changed values compared to previous record,
my table is looking like this
Id(int) | value(boolean) |
-------------------------
1 | 0 |
-------------------------
2 | 1 |
-------------------------
3 | 1 |
-------------------------
4 | 1 |
-------------------------
5 | 0 |
-------------------------
6 | 0 |
-------------------------
I must get id:2,5
thanks for your help
I did a self-join, but instead of joining identical ids, I join t1.id = t2.id-1 and then compare the values:
select t2.id
from thetable t1
inner join thetable t2 on t1.id = t2.id-1
where t1.value != t2.value
sqlfiddle: http://sqlfiddle.com/#!2/6d626/4
Edit to add:
Thanks to #Ravinder, I figured out a way to do this even if the ids aren't sequential.
I used this related question.
SET #a :=0;
SET #b :=1;
SELECT t1.id, t1.rownum
FROM
(SELECT if(#a, #a:=#a+1, #a:=1) as rownum, id, value FROM thetable) AS t1
LEFT JOIN
(SELECT if(#b, #b:=#b+1, #b:=1) as rownum, id, value FROM thetable) AS t2
ON t1.rownum = t2.rownum
where t2.value != t1.value
SQLFiddle with non-sequential ids
Basically if you don't have sequential ids you create your own. I called them rownum.
You can use mysql variable in query to check if its new or unchanged
SELECT
`Id`,
`value`,
(CASE when #test=value then 'unchanged'
else 'changed' end) occurance,
#test:=`value` testvar
FROM
Table1
,(SELECT #test:='new') t
HAVING occurance='changed'
ORDER BY `Id`
See fiddle demo
I have a relational DB that I can't think of how to form this query.
Here's the info
Table1
id name
1 Mike
Table2
id table_1_id value setting
1 1 something setting1
2 1 something2 setting2
2 1 something3 setting3
Currently, this is my sql query
SELECT * FROM Table1
JOIN Table2 on Table2.table_1_id = Table1.id
What this outputs is something like this
id name table_1_id value setting
1 Mike 1 something1 setting1
1 Mike 1 something2 setting2
1 Mike 1 something3 setting3
Is it possible to construct this in such a way to return these results so I can export it to a CSV file?
id name table_1_id something1 something2 something3
1 Mike 1 setting1 setting2 setting3
SELECT
Table1.*,
something1Table.setting AS something1,
something2Table.setting AS something2,
something3Table.setting AS something3
FROM Table1
JOIN Table2 AS something1Table ON something1Table.table_1_id = Table1.id AND something1Table.value = 'something'
JOIN Table2 AS something2Table ON something2Table.table_1_id = Table1.id AND something2Table.value = 'something2'
JOIN Table2 AS something3Table ON something3Table.table_1_id = Table1.id AND something3Table.value = 'something3'
You need a conditional aggregation:
select table1.id, table1.name,
max(case when value = 'something1' then setting end) as setting1,
max(case when value = 'something2' then setting end) as setting2,
max(case when value = 'something3' then setting end) as setting3
from table1 join
table2
on table1.id = table2.id
group by table1.id, table1.name
This type of data transformation is known an a pivot but MySQL does not have a pivot function. So you will want to replicate it using an aggregate function with a CASE expression.
If you know the the number of values ahead of time, then you can hard-code your query similar to this:
select t1.id,
t1.name,
max(case when t2.value = 'something' then t2.setting end) as setting1,
max(case when t2.value = 'something2' then t2.setting end) as setting2,
max(case when t2.value = 'something3' then t2.setting end) as setting3
from table1 t1
left join table2 t2
on t1.id = t2.table_1_id
group by t1.id, t1.name;
See SQL Fiddle with Demo
But if you have an unknown number of values that you want to transform into columns, then you can use a prepared statement to generate dynamic sql.
The query would be similar to this:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'max(case when t2.value = ''',
value,
''' then t2.setting end) AS `',
value, '`'
)
) INTO #sql
FROM table2;
SET #sql = CONCAT('SELECT t1.id,
t1.name, ', #sql, '
FROM table1 t1
left join table2 t2
on t1.id = t2.table_1_id
group by t1.id, t1.name');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See SQL Fiddle with Demo
The result of both versions is:
| ID | NAME | SOMETHING | SOMETHING2 | SOMETHING3 |
---------------------------------------------------
| 1 | Mike | setting1 | setting2 | setting3 |
GROUP_CONCAT may be of use. It doesn't give you exactly what you want, because it would put the concatenated values into a single field. But depending on what you're actually trying to accomplish, perhaps you can work around that. The advantage of the GROUP_CONCAT is that it can handle any number of table2 rows per table1 row, whereas the conditional aggregation above hardwires having three entries (which may well be what you want).
SELECT table1.*,
GROUP_CONCAT(value) AS value_group,
GROUP_CONCAT(setting) AS setting_group
FROM table1
INNER JOIN table2
ON table2.table_1_id = table1.id
returns
id,person,value_group,setting_group
1,Mike,"something1,something2,something3","setting1,setting2,setting3"