MySQL joins with one to many table relationships - mysql

I have no idea if this is possible but is there a way in MySQL to produce a single query where the multiple results of a one to many table join can be set as an array on a key of the result for the one item?
I realise that question isn't very clear so I'll explain what I'm after further:
Firstly, I'm currently using implicit joins and would like to learn more on explicit joins (of which I currently know very little), perhaps these could provide the answer I'm looking for?
For example given two tables:
CREATE TABLE `a` (
`id_a` int(11) NOT NULL AUTO_INCREMENT,
`a_column1` varchar(255) NOT NULL,
...
PRIMARY KEY (`id_a`)
)
CREATE TABLE `b` (
`id_b` int(11) NOT NULL AUTO_INCREMENT,
`id_a` int(11) NOT NULL,
`b_column1` varchar(255) NOT NULL,
...
PRIMARY KEY (`id_b`)
)
Where table b has many entries related to a single entry in table a.
If I were to run the following query:
SELECT a.*, b.* FROM a, b WHERE b.id_a = a.id_a AND a.id_a = x;
I would get an array with multiple entries with the data of the single item id x repeated. What I actually want is a single row returned from table a with a key defined as b which contains an array of the multiple matching entries from table b. I suspect that this is not possible with a query alone, but it would be great if it was. Currently I am doing the following in PHP (where $this->_db is a Zend Framework database adapter). This runs a lot of queries!:
$query = "SELECT * FROM a WHERE id_a = ?";
$items = $this->_db->fetchAll($query, $id);
foreach($items as $key => $item) {
$query = "SELECT * FROM b WHERE id_a = ?";
$items[$key]['b'] = $this->_db->fetchAll($query, $item['id']);
}
Alternatively I can use my original join query and post process, which I suspect is more efficient, but means I need to explicitly copy over the columns I need (a pain and far from elegant):
$query = "SELECT * FROM a, b WHERE a.id_a = b.id_a AND a.id_a = ?";
$items = $this->_db->fetchAll($query, $id);
$output = array('a_column1' => $items[0]['a_column1'], etc...);
$output['b'] = array();
foreach($items as $item) {
$b = array('b_column1' => $item['b_column1'], etc...);
$output['b'][] = $b;
}

Your query that uses implicit JOIN:
SELECT a.*
, b.*
FROM a, b
WHERE b.id_a = a.id_a
AND a.id_a = x
With explicit JOIN:
SELECT a.*
, b.*
FROM a
JOIN b
ON b.id_a = a.id_a
WHERE a.id_a = x
One way to have the data in one query is to use the GROUP_CONCAT() function. But it may not be in a format you can use:
SELECT a.*
, GROUP_CONCAT( b.id_b
ORDER BY b.id_b ASC
SEPARATOR ','
) AS b_ids
, GROUP_CONCAT( b.b_column1
ORDER BY b.id_b ASC
SEPARATOR ','
) AS b_column1s
, ... --- etc
FROM a
JOIN b
ON b.id_a = a.id_a
WHERE a.id_a = x
GROUP BY a.id_a

You're probably looking for an ORM (object-relational mapper), which would handle associations between objects and would be able to return one A object containing an array of B objects.
See Good PHP ORM Library?
Using explicit joins, the query would look like this:
SELECT a.*, b.* FROM a inner join b on b.id_a = a.id_a where a.id_a = x;

Related

How to query mysql to select and group by multiple values

I'm trying to select and group by all the contentid values of the table below where the match criteria can be several different values.
the contentid values actually represent cars, so I need to select [and group by] all the contentis where the values are 'GMC' and the values are 'sedan' and the value is 'automatic.
i.e. I'm trying to select all the GMC sedans with an automatic transmission.
a query like this fails [obviously]:
select * from modx_site_tmplvar_contentvalues WHERE
`value` = 'gmc' and
`value` = 'tacoma'
group by contentid
I have no idea how to create a query like that. Any suggestions?
You need to "pivot" these data on "tmplvarid", but unfortunately for you MySQL doesn't have a PIVOT statement like other RDBMS. However, you can pivot it yourself by joining in the table multiple times for each variable you care about:
SELECT
contents.contentid,
transmission.value as transmission,
type.value as type,
make.value as make
FROM
(SELECT DISTINCT contentid FROM modx_site_tmplvar_contentvalues) AS contents
LEFT JOIN
modx_site_tmplvar_contentvalues AS transmission
ON contents.contentid = transmission.contentid
AND transmission.tmplvarid = 33 -- id for transmission
LEFT JOIN
modx_site_tmplvar_contentvalues AS make
ON contents.contentid = make.contentid
AND make.tmplvarid = 13 -- id for make
LEFT JOIN
modx_site_tmplvar_contentvalues AS type
ON contents.contentid = type.contentid
AND type.tmplvarid = 17 -- id for type
WHERE
type.value = 'sedan'
AND make.value = 'GMC'
AND transmission.value = 'automatic'
You can expand this with additional joins for other criteria such as year (id 15) or mileage (id 16).
If you need to use the value only, you could try:
SELECT DISTINCT
contents.contentid,
transmission.value as transmission,
type.value as type,
make.value as make
FROM
(SELECT DISTINCT contentid FROM modx_site_tmplvar_contentvalues) AS contents
INNER JOIN
modx_site_tmplvar_contentvalues AS transmission
ON contents.contentid = transmission.contentid
AND transmission.value = 'automatic'
INNER JOIN
modx_site_tmplvar_contentvalues AS make
ON contents.contentid = make.contentid
AND make.value = 'GMC'
INNER JOIN
modx_site_tmplvar_contentvalues AS type
ON contents.contentid = type.contentid
AND type.value = 'sedan'
In any case, make sure you have an index on the value column; these queries are going to get slow.
please try this:
SELECT *
FROM modx_site_tmplvar_contentvalues t1 INNER JOIN modx_site_tmplvar_contentvalues t2 ON t1.contentid = t2.content_id
WHERE
t1.`value` = 'gmc'
AND t2.`value` = 'tacoma';
You can do this with a group by. This is the most flexible in terms of expressing the conditions. In MySQL, multiple joins will often perform better:
select contentid
from modx_site_tmplvar_contentvalues
group by contentid
having sum(`value` = 'gmc') > 0 and
sum(`value` = 'tacoma') > 0;
This is always false:
`value` = 'gmc' and
`value` = 'tacoma'
Instead, use OR:
`value` = 'gmc' OR
`value` = 'tacoma'
In a condition "and" means "this and this is true at the same time". If you want all foos and all bars, then your condition is "foo OR bar".
EDIT:
To select groups containing your values, you can write subqueries:
SELECT DISTINCT name FROM table WHERE name IN (SELECT name FROM table WHERE value='value1') AND name IN (SELECT name FROM table WHERE value='value2')

combing two SELECT statements into a JOIN

I have two tables: table_1: item_id, name ... table_2: image_id, item_id, url
The following function retrieves the 'urls' of the items with the '$name' that is passed in. I first need to get the 'item_id' from table_1 to use in the query. I only need to return the 'url's from table_2.
It currently works fine, but I feel it could be streamlined to be done in one query with a JOIN rather than two separate queries. I've tried using different 'JOIN's to make it work but I can't seem to get it right.
Here's the function as it stands now with the two queries...
function get_items( $name ) {
global $wpdb;
$sql1 = "SELECT `item_id` FROM `table_1` WHERE `name` = '$name'";
$results1 = $wpdb->get_var( $sql1 );
$sql1 = "SELECT `url` FROM `table_2` WHERE `item_id` = '$results1'";
$results2 = $wpdb->get_results( $sql1, ARRAY_A );
return $results2;
}
and here is the 'JOIN' that I tried implementing, but was unsuccessful (I've also switched it around and did a 'LEFT/RIGHT JOIN' as well resulting in the same result) ...
$sql1 = "SELECT `table_2`.`url`
FROM `table_2`
INNER JOIN `table_1`
ON `table_2`.`item_id` = `table_1`.`item_id`
WHERE `table_1`.`item_id` = '$name'";
Any advice on combining these two queries?
The problem with your query is this WHERE table_1.item_id = '$name', it should be the name not the item_id
SELECT b.url
FROM table1 a
INNER JOIN table2 b
ON a.item_id = b.item_id
WHERE a.name = '$name'
select url from table_2 where item_id IN (select item_id from table_1 where name = $name )

Reuse select result in the same query

I have a query like this:
DELETE FROM rules_table
WHERE
type1 = (
SELECT type_id
FROM types_table
WHERE name = '<some_name>')
OR
type2 = (
SELECT type_id
FROM types_table
WHERE name = '<some_name>')
Please note that <some_name> is the same in both occurrences
I submit a query from php script and I'd prefer it to be a single request rather then selecting the type_is with one request, parsing the result and submitting the delete request.
And also as far as I know, running the same SELECT statement two times is also a bad idea.
try
DELETE FROM rules_table
WHERE (
SELECT type_id
FROM types_table
WHERE name = '<some_name>'
) in (type1, type2)
instead of subquery you can use INNER JOIN:
DELETE a
FROM rules_table a
INNER JOIN types_table b
ON (a.type1 = b.type_id OR a.type2 = b.type_id)
WHERE b.name = '<some_name>';

create dynamically created columns from a table in mysql

i would like to create virtual columns that are dynamic where the values are being generated based on a join table.
i have the following table called types:
id, name
1, TypeA
2, TypeB
and i have a table called category
id, name, type
1, a, 1
2, b, 2
i would like to have a query that returns the following
category name, TypeA, TypeB
a, 1, 0
b, 0, 1
is this possible to do in mysql?
I'd outline several cases here.
First and most straightforward is the following:
SELECT c.name AS "CatName",
IF(typea.id IS NULL, 0, 1) AS "TypeA",
IF(typeb.id IS NULL, 0, 1) AS "TypeB"
FROM category c
LEFT JOIN types typea ON c.type = typea.id AND typea.name = 'TypeA'
LEFT JOIN types typeb ON c.type = typeb.id AND typeb.name = 'TypeB';
But this requires manually mentioning all types in the query, which is apparently not what you're seeking for.
It is possible to build SQL query and use it, this method assumes you're running queries from some script, that can grab output from the first query and use it as a new query.
SELECT concat('SELECT c.name AS "CatName",',
group_concat(concat('IF(',lower(t.name),
'.id IS NULL,0,1) AS "',t.name,'"')),
' FROM category c ',
group_concat(concat('LEFT JOIN types ',
lower(t.name),' ON c.type = ',lower(t.name),'.id AND ',
lower(t.name),'.name = ''',t.name,'''') SEPARATOR ' '),
';')
FROM types t;
Writing a small shell (or other) script should be easy.
In the standard SQL it is not possible to use contents of the tables to create DML statements. Different databases provide different facilities for this, like PIVOT statement, procedural languages, etc. I do not know how to achieve this with MySQL facilities, but the idea is to dynamically build a query outlined in point #2 and execute it.
I've covered first 2 cases on the SQL Fiddle.
There is a feature called PIVOT which does what you want, but unfortunately, it is not available in MySQL.
What you could do however, is concatenate all types into a single string per category:
SELECT
a.name,
GROUP_CONCAT(b.name) AS types
FROM
category a
LEFT JOIN
types b ON a.type = b.id
GROUP BY
a.id
Which would result in something like:
name | types
--------------------------------
a | TypeA
b | TypeB
c | TypeA,TypeB,TypeC,TypeD
Where category c has four different types, but a and b only have one type associated with them.
If you know beforehand what and how many types you're going to check on, and want to display a boolean value if that type exists for the category, you could do this:
SELECT,
a.name,
b.id IS NOT NULL AS TypeA,
c.id IS NOT NULL AS TypeB,
-- etc...
FROM
category a
LEFT JOIN
types b ON a.type = b.id AND b.id = 1
LEFT JOIN
types c ON a.type = c.id AND c.id = 2
-- etc...
Edit: If you don't know the number of columns you're going to create beforehand, but still want boolean values for each type in their own separate columns, another option would be to dynamically build the query string in your application logic. Let's say you were using PHP for example:
$columns = $ljoins = array();
$i = 1;
foreach($pdo->query('SELECT id, name FROM types') as $row)
{
$columns[] = "t$i.id IS NOT NULL AS " . $row['name'];
$ljoins[] = "LEFT JOIN types t$i ON a.type = t$i.id AND t$i.id = " . $row['id'];
$i++;
}
$sql = 'SELECT a.name, ' . implode(', ', $columns) . ' FROM category a ' . implode(' ', $ljoins);
$stmt = $pdo->query($sql);
// Do stuff with result-set

Mysql: Get all results that has all this relations

I have two tables:
objects object_features
------------- -------------------
id id
name object_id
term_id
What I want to achieve is, giving a list of features, get all objects that has all of them.
I'm trying this:
SELECT objects.*
FROM `object_features` LEFT JOIN `objects` ON ( objects.id=object_features.object_id)
WHERE term_id IN ('1','3','4','10')
This is the php code I'm using:
$feature_list = array(1,3,4,10);
$sql = 'SELECT objects.*
FROM `object_features` LEFT JOIN `objects` ON ( objects.id=object_features.object_id)
WHERE term_id IN ('.implode(',', $feature_list).')';
This is near to what I need, but differing that it returns me any object that has any of the features given, instead of ALL the features
one option is to group by the data you want returned from object and add a having clause that counts object.id and tests to see if it is the same as the length of the array.
SELECT objects.id, objects.name
FROM `object_features` LEFT JOIN `objects` ON ( objects.id=object_features.object_id)
WHERE term_id IN ('1','3','4','10')
group by objects.id,objects.name
having count(objects.id) = 4
Cant swear to the syntax on that as I've been writing tsql recently and don't have an instance of mysql to test on.
try
'WHERE term_id = '.impode(' AND termid = ', $features_ids).')'
This will result in:
WHERE termid = 1 AND termid = 3 AND termid = 5
Actually you need a GROUP BY to group by each object and using a HAVING clause to allow only rows that have all the termids
SELECT objects.*
FROM `object_features` LEFT JOIN `objects` ON ( objects.id=object_features.object_id)
WHERE term_id IN ('1','3','4','10')
GROUP BY objects.id, objects.name
HAVING count(term_id) = 4
The SQL way of doing it would be:
SELECT objects.*
FROM objects
WHERE null not in
(
select of.object_id
from features f
left join object_features of on (f.id = of.id)
)
Assuming you have a features table with all the features.
If you need to list only certain features, you can do (check out the where condition on the subquery):
SELECT objects.*
FROM objects
WHERE null not in
(
select of.object_id
from features f
left join object_features of on (f.id = of.id)
where f.id in (1,2,3,4,5)
)