Mysql JOIN multiple tables and select mulitple values - mysql

First of all I want to apologize for my bad English. I have a problem with multiple tables.
I'm not exactly a newby in the mySQL world but I cannot figure out a solution for this problem.
For this problem I use 4 tables.
Categories
Products
Specifications
Specificationvalues
Every category has specifications and products and every specificationvalues has products and specification id's.
Now the user can make a selection with different values.
This is where my problem is.
When the user selects value "green" and legs "4" I want all green products with 4 legs.
So I used a JOIN (all kinds i think) to select the right product (example below)
SELECT DISTINCT products.id
FROM products
LEFT JOIN specificationvalues ON specificationvalues.products_id = products.id
LEFT JOIN specifications ON specificationvalues.specifications_id = specifications.id
WHERE specifications.name='materiaal'
AND specifications.name='kleur'
AND specificationvalues.id='77'
AND specificationvalues.id='78'
The problem is that all the values are in separate rows. That's why the WHERE doesn't work. I do not get MySQL error's. Only that it returns 0 rows.
I hope someone can help me! I got a lot of good things from this forum so I hope it will help me again!
I don't know why my changes yesterday where not saved. But here is my data again:
SPECIFICATIONS Table
ID CATEGORIES_ID NAME
38 297 Material
39 297 Measures
40 297 Color
SPECIFICATIONVALUES Table
ID SPECIFICATIONS_ID PRODUCTS_ID VALUE
1 38 988979 Masive wood
2 39 988979 24x57x98
3 40 988979 Yellow
4 40 988980 Black
5 39 388980 24x57x98
PRODUCTS Table
ID NAME
988979 Table
988980 Chair
So now I want all black prducts with measure 24x57x98. I hope you can help me!

I've seen many instances of tables that have rows to simulate representing "columns" where the name is a descriptor and the "id" or "value" is the associated want value.
What you need to consider is what does ONE ROW look like. Your join from specifications name to the specifications values. Does the '77' value only correspond to the 'material' spec, 'kleur', or both... likewise with 78. You could have a variety of combinations, such as
where ( specifications.name = 'material' and specificationValues.id = '77' )
OR ( specifications.name = 'kleur' and specificationValues.id = '78' )
Or, if the specification values ID is regardless of the spec name, you could use
where specifications.name in ('material', 'kleur' )
AND speciificationValues.ID in ( '77', '78' )
PER YOUR REVISED SAMPLE DATA...
In this type of criteria you want, I would do by applying a double-join to represent each criteria you want, such as:
select p.*,
sv1.Value as NameOfColor,
sv2.Value as ProductMeasurement
from
Products p
JOIN SpecificationValues sv1
on p.ID = sv1.products_id
AND sv1.Value = 'Black'
JOIN Specifications s1
on sv1.Specifications_ID = s1.ID
AND s1.Name = 'Color'
JOIN SpecificationValues sv2
on p.ID = sv2.products_id
AND sv2.Value = '24x57x98'
JOIN Specifications s2
on sv2.Specifications_ID = s2.ID
AND s2.Name = 'Measures
Now, may look complex, but look at the simplicity (by explicit spacing between join segments). However, if you ever wanted to add more "crieria" requirements, just duplicate by creating a similar sv3, sv4, sv5 set... Now, that said, if you are building this out dynamically where a user can pick more things, and you are providing some sort of "selection" of the Readable description (Color, Measure, Material), then just preserve the IDs so you don't need the extra join, just know the actual ID, and it would be simplified to...
select p.*,
sv1.Value as NameOfColor,
sv2.Value as ProductMeasurement
from
Products p
JOIN SpecificationValues sv1
on p.ID = sv1.products_id
AND sv1.Specifications_ID = 40
AND sv1.Value = 'Black'
JOIN SpecificationValues sv2
on p.ID = sv2.products_id
AND sv2.SpecificationsID = 39
AND sv2.Value = '24x57x98'
Now, back to original answer, you could get the same results (provided you never have a color of "24x57x98", or a measurement of "Black" per your example data. You can apply the IN (list of codes) AND IN (list of values) and using a HAVING clause to ensure proper count of matched elements IS found. My FINAL query would not use "Products" as the first table, but the second because you could have 10,000 products, but only 142 that are of a given size... start by the table/criteria of a smaller set and join to the products to get the name.
select
p.id,
p.name
from
specificationValues sv
join products p
on sv.products_id = p.id
where
sv.specifications_ID IN ( 39, 40 )
AND sv.value in ( 'Black', '24x57x98' )
group by
p.id
having
count(*) = 2
Ensure your specificationValues table has an index on (specifications_ID, value). This way, the index can match HOW you are looking for data. Some may even suggest having all 3 parts in the index for (specifications_ID, value, products_id)

That's not because all the values are in separate rows, that's because your WHERE clause is always false :
WHERE specifications.name='materiaal' AND specifications.name='kleur'
AND specificationvalues.id='77' AND specificationvalues.id='78'
You can't have and id equals to 77 AND 78.
You must use this kind of code :
WHERE (specifications.name='materiaal' OR specifications.name='kleur')
AND (specificationvalues.id='77' OR specificationvalues.id='78')
EDIT : my last AND will probably also be an OR, depending of your tables.

Related

SQL Left Join on more then 5 tables

The code is retreating what I want it to retrieve, though it does 4 times more that I need.
I have a table called property with a PK property_ID. A property has a title, short description, long description and it belongs to a point of interest, which in turn is connected to a city. Title, short&long description have all independent link tables, in which a link is created between a property and the corresponding translation in the translation table.
I am using left joins to collect all the desired information, below in the image attachment you can see the results.
The desired result would be that it would return only 2 rows, with the corresponding translation of the title, short and long description. At the moment is returning 8 rows.
The issue that I noticed is it orders the columns in a weird way.
If you look closer, you can notice that title and titleLangCode are ordered correctly after titleLangCode, while long and short descriptions are ordered by their own langCode. Grouping them wont work since they are ordered differently, so ordering them has no effect, union didnĀ“t work either because of the column numbers, even tried distinct but with no avail.
The code:
select
property.*,
title_translation.title,
title_translation.langCode as titleLangCode,
short_desc_translation.shortDescription,
long_desc_translation.longDescription,
short_desc_translation.langCode as shortLangCode,
long_desc_translation.langCode as longLangCode,
property_city_poi.city_poi_link_ID
from
property
left join
title_link
on
property.property_ID = title_link.property_ID
left join
title_translation
on
title_link.title_link_ID = title_translation.title_link_ID
left join
short_desc_link
on
property.property_ID = short_desc_link.property_ID
left join
short_desc_translation
on
short_desc_link.short_desc_link_ID = short_desc_translation.short_desc_link_ID
left join
long_desc_link
on
property.property_ID = long_desc_link.property_ID
left join
long_desc_translation
on
long_desc_link.long_desc_link_ID = long_desc_translation.long_desc_link_ID
left join
property_city_poi
on
property.property_ID = property_city_poi.property_ID
where
property.property_ID = 10
Is there a possibility of somehow combing limit with group ? I tried but have not succeed.
title_link
title_link_ID
property_ID
dateCreated
title_translation
title_translation_ID
title_link_ID
langCode
title
short_desc_link
short_desc_link_ID
property_ID
dateCreated
short_desc_translation
short_desc_translation_ID
short_desc_link_ID
langCode
shortDescription
long_desc_link
long_desc_link_ID
property_ID
dateCreated
long_desc_translation
long_desc_translation_ID
long_desc_link_ID
langCode
shortDescription
If I understand this correctly, the issue is that you have multiple translation tables which have a 1 to many relationship to the property. So after the first join you have 2 rows then 4 and then 8 with all combinations of languages.
You can limit this by joining on 2 conditions.
... property p
INNER JOIN titel_link tl on p.id = tl.propertyid
INNER JOIN short_desk_link sdl on p.id = tl.propertyid AND tl.langCode = sdl.langCode
I've simplified this a little and used aliases for table names to shorten the join conditions.
Edit: I'd say this is also a sign of bad database design. you should probably introduce a table 'language' and then a mapping table property-translation mapping. Not sure if this is under your control but a setup with tables like this would be better.
Property: all the details for the property.
Language: A listing of all the languages with Id.
FieldId: A list of named columns that you have in your database.
Translation: A combination of Property, Language and Field on which you can then have a single translation for that field.

SQL Join with multiple conditions for one column

In my situation, I've two tables, a table with all institutions profile and a table which represents material studied by each of these institutions.
In a search, I want to look for institution's name which studied specifics type of material. So it's possible to look for institution which studied "Wood" and "Metal", just "Metal" etc...
I've tried the following query :
SELECT p.name
FROM q1_institution_profiles p
INNER JOIN q9_materials_studied_by_institution pf
ON pf.id_institution = p.id_institution
WHERE pf.id_material_studied = 10 AND pf.id_material_studied = 8
However result is empty because there is a problem when I have multiple conditions (here id_material_studied must be equal 8 and 10).
Is there a way to achieve this properly or do I have to do on join for each criteria ?
Thank you in advance for answer
I think this is what you want:
SELECT p.name
FROM q1_institution_profiles p INNER JOIN
q9_materials_studied_by_institution pf
ON pf.id_institution = p.id_institution
WHERE pf.id_material_studied IN (8, 10)
GROUP BY p.name
HAVING COUNT(DISTINCT pf.id_material_studied) = 2;
That is, get names where there are rows with each of the two values.
The WHERE clause selects only materials 8 and 10. The GROUP BY then groups these by p.name. The question is: Does a single name have both these materials? Well, that is what count(distinct) does.

SELECT, JOIN, and a WHERE != statement returning too many rows

Trying to get a tricky mysql select statement working - and I need some help to cool off my burning noggin...and I have a feeling one of you MYSQL heroes out there will look at this and reel it off.
Goal: List a given user's songs NOT in a given category.
Three tables: :
table 1: song, many fields with assigned UserID, and unique SongID
table 2: category, 3+ fields, with assigned UserID and unique CatID
table 3: linker, one-to-many for listing songs in one or more categories. 3 fields, unique id (index), SongID, CatID
The following gets me close - but does not list a user's songs that aren't assigned to any other category at all OR are a already assigned to a another category (I think thanks to !=).
SELECT DISTINCT song.SongName, song.UserID, song.SongID FROM song
JOIN linker ON song.SongID = linker.SongID
WHERE linker.CatID != 155 AND song.UserID =2
Then, I tried this,
SELECT DISTINCT song.SongName, song.UserID, song.SongID FROM song
LEFT JOIN linker ON song.SongID = linker.SongID
WHERE (linker.SongID IS NULL OR linker.CatID != 155) AND song.UserID =2
Closer but not working (still thanks to != including songs already assigned).
I was thinking I can get away without invoking table 2, since it merely adds and defines categories for a given user. Alternatively, I'm thinking of getting all the user's songs, and then unsetting array values with a given CatID - but this doesn't seem like it should be necessary? I feel like I'm missing something simple? Table structure is not sacred at this point if it absolutely needs to change. Thanks to any who share their thoughts.
Try this (I am used to MSSQL so if my syntax is off, appologies in advance):
SELECT s.SongName, s.UserID, s.SongID
FROM song s
LEFT JOIN linker l on s.SongID = l.SongID AND l.CatID = 155
WHERE s.UserID = 2
AND l.ID is null
SELECT DISTINCT song.SongName, song.UserID, song.SongID
FROM song
LEFT JOIN linker
ON song.SongID = linker.SongID
and linker.CatID != 155
AND song.UserID = 2

How make this eav query to make horizontal result

The case:
tables:
product:
product_id|name |
------------------------
1 |iphone 4 |
2 |gallaxy 2 |
3 |blackbery 6 |
product_attribute:
id|product_id|attribute_id
--------------------------------------------------
1 |1 |2
2 |1 |6
. . .
attribute:
------------------------------
attribute_id|name |value|
1 |width |300
2 |width |320
3 |width |310
4 |height|390
5 |height|370
6 |height|380
should get result:
product_id|height|width
1 |380 |320
......................
Edit:
height and width attributes its only part of product attributes - product need to have dynamic ability to be added by the user in backend as it done in magento, because that i choose eav db design.
Please write queries if it possible in case we dont know which names product have.
Thanks
There are several ways to implement this. Something like this should work joining back on the table multiple times for each attribute value:
SELECT p.product_id,
a.value height,
a2.value width
FROM Product p
JOIN Product_Attribute pa ON p.product_id = pa.product_id
JOIN Attribute a ON pa.attribute_id = a.attribute_id AND a.name = 'height'
JOIN Product_Attribute pa2 ON p.product_id = pa2.product_id
JOIN Attribute a2 ON pa2.attribute_id = a2.attribute_id AND a2.name = 'width'
And here is the Fiddle.
Here is an alternative approach using MAX and GROUP BY that I personally prefer:
SELECT p.product_id,
MAX(Case WHEN a.name = 'height' THEN a.value END) height,
MAX(Case WHEN a.name = 'width' THEN a.value END) width
FROM Product p
JOIN Product_Attribute pa ON p.product_id = pa.product_id
JOIN Attribute a ON pa.attribute_id = a.attribute_id
GROUP BY p.product_id
Good luck.
One approach is to use correlated subqueries in the SELECT list, although this can be less than optimum for performance on large sets. For retrieving just a few rows rows from the product table, it won't be bad. (You'll definitely want appropriate indexes.)
SELECT p.product_id
, ( SELECT a1.value
FROM attribute a1
JOIN product_attribute q1
ON q1.attribute_id = a1.attribute_id
WHERE q1.product_id = p.product_id
AND a1.attribute_name = 'height'
ORDER BY a1.id
LIMIT 0,1
) AS height_1
, ( SELECT a2.value
FROM attribute a2
JOIN product_attribute q2
ON q2.attribute_id = a2.attribute_id
WHERE q2.product_id = p.product_id
AND a2.attribute_name = 'width'
ORDER BY a2.id
LIMIT 0,1
) AS width_1
FROM product p
WHERE p.product_id = 1
This query will return the row from product, along with the values of the attributes, if they exist. If the attribute values are not found, the query will return a NULL in place of the attribute value. (This differs from the behavior of a query that uses INNER JOIN in place of correlated subquery... where a "missing" row from the attribute or product_attribute table would filter out the row from product being returned.)
The purpose of the LIMIT clauses is to guarantee that the subqueries will return not return more than one row. (If a subquery in the SELECT list were return more than one row, MySQL would return an error.) The purpose of the ORDER BY is to make the query deterministic, again, in the event there is more than one row that satisfies the subquery. (Absent the ORDER BY clause, when there is more than one row, MySQL is free to arbitrarily return whichever row it chooses to.)
For "multi-valued" attributes, the same approach works. We just add more subqueries, but specify LIMIT 1,1 to return the second attribute value, LIMIT 2,1 to return the third value, etc.
(Oh, the joy of an EAV model implemented in a relational database.)
Followup:
Q: "... more general case as it happen in eav db that we dont know before which attributes names we have."
A: The relational model is based on the principle that a tuple contains a specified number of columns, of a specified type.
What you are (apparently) trying to do is return a variable number of columns when you run a query. A SELECT statement includes a specific list of expressions to be returned; this cannot vary and the datatypes of the values returned by each expression does not vary from row to row.
The query above returns one instance of a "height" attribute value, and one instance of a "width" attribute value, for each product.
For a more "more general case", we would really expect that each attribute value would be returned on its own separate row.
The more general query, if you don't know "ahead of time" what attributes are associated with a product would be:
SELECT p.product_id
, a.attribute_id
, a.name AS attribute_name
, a.value AS attribute_value
FROM product p
LEFT
JOIN product_attribute q
ON q.product_id = p.product_id
LEFT
JOIN attribute a
ON a.attribute_id = q.attribute_id
WHERE p.product_id = 1
ORDER
BY p.product_id
, a.name
, a.attribute_id
That will return a resultset which can easily be processed:
product_id attribute_id attribute_name attribute_value
---------- ------------ -------------- ---------------
1 6 height 380
1 2 width 320
Q: "looks like should be done in 2 stages: 1. get all attributes names for product 2.then your code with server side code of attributes names in for loop"
A: No, it looks like a single query will return all the attribute name and value pairs for a product. Each attribute name/value will be on a separate row.
There's no need to use a "for loop" to generate additional queries against the database. Yes, it's possible to do that, but totally unnecessary.
If you have some bizarre requirement to compose another query to run against the database to return the resultset in the format you specified, whatever processing you would do to process the resultset from the "more general case" statement, it would probably be more efficient to just process the resultset, without running any more queries against the database.
If you need to return a resultset that looks like this:
product_id height width
---------- ------ -----
1 380 320
(as bizarre a requirement as that is, to compose another query) it's entirely possible to use that resultset from the "more general query" to generate a query that looks like this:
SELECT 1 AS product_id, '380' AS height, '320' AS width
Though such an exercise is rather pointless, given that you aren't returning any new information you didn't return previously, and now you have another resultset that you need to process, which just seems to me to be a boatload of unnecessary overhead.
Let me first say this is a really poor design. Under your current approach, you will need to run multiple subqueries or joins with table aliases to achieve the result you want.
SELECT
product_id,
(
SELECT product_attribute.value
FROM product_attribute, attribute
WHERE product_attribute.product_id=product.product_id
AND product_attribute.attribute_id=attribute.attribute_id
AND product_attribute.name = 'width'
) AS 'width',
(
SELECT product_attribute.value
FROM product_attribute, attribute
WHERE product_attribute.product_id=product.product_id
AND product_attribute.attribute_id=attribute.attribute_id
AND product_attribute.name = 'height'
) AS 'height'
FROM
product
ORDER BY
...
Let me suggest:
attribute
attribute_sid (eg, string id)
product
product_id
name
...
product_attribute
product_id (foreign key to product table)
attribute_sid (foreign key to attribute table)
value
This way, you have a definitive list of attributes, and a single attribute value per product.
SELECT attribute_sid, value FROM product_attribute WHERE product_id = 1
... will retrieve all the attributes and values, which can conveniently be placed in a dict, array, or map.

SELECT where ID matches one of a list

I'm just learning the ins and outs of MYSQL queries but I've run into a roadblock with a project and I'd appreciate some help reaching a solution.
I have two tables, the first contains a reference to the entry (entry_id) and the modification associated (mod_id) as well as the category of modification it refers to (mod_name)
Table 1: exp_store_product_options
mod_id entry_id mod_name
3 2919 COLOR
4 2311 SIZE
5 2311 COLOR
6 3301 COLOR
the second table contains two relevant fields, mod_id and opt_name
Table 2: exp_store_product_modifiers
mod_id opt_name
3 BLACK
4 EU 44
5 BROWN
6 BROWN
What I am trying to achieve, is a listing of all the DISTINCT opt_name fields that (through a join on the mod_id) correspond to entry_ids that I would like to pass in as a lump.
here is the code I've come up with so far, I believe it'll do what I need aside from requiring me to loop through the query for each entry id, and failing on the DISTINCT requirement since for each iteration, everything is distinct. (the {sale_products} tags are from ExpressionEngine, and will loop during the parse to provide me with a list of the entry_id results that are relevant to this page
SELECT DISTINCT opt_name
FROM exp_store_product_options
INNER JOIN exp_store_product_modifiers
ON exp_store_product_options.product_mod_id=exp_store_product_modifiers.product_mod_id
{sale_products parse='inward'}entry_id = '{entry_id}' OR{/sale_products}
AND mod_name = 'SIZE'
====================================================
POSTMORTEM
Just in case anyone else is trying to work within expresso's Store module for ExpressionEngine and needs to build some rudimentary filtering into your templates, here's the code I ultimately got to work. Very similar to Ben's code, but utilizing embeds instead of directly inserting the entry_ids because of issues with parse order:
the template: embeds/product_filter
{exp:query sql="SELECT DISTINCT opt_name
FROM exp_store_product_modifiers
LEFT JOIN exp_store_product_options
ON exp_store_product_options.product_mod_id = exp_store_product_modifiers.product_mod_id
WHERE exp_store_product_modifiers.entry_id IN ({embed:entry_ids})
AND exp_store_product_modifiers.mod_name = '{embed:filter_on}'"
}
<li>{opt_name}</li>
{/exp:query}
with an {embed} that looks like
{embed="embeds/product_filter" entry_ids="{sale_products backspace='1'}{entry_id},{/sale_products}" filter_on="SIZE"}
If you have a list of entries, you can use IN. Also, I'd use a LEFT JOIN here instead of an INNER JOIN.
SELECT DISTINCT opt_name
FROM exp_store_product_options
LEFT JOIN exp_store_product_modifiers
ON exp_store_product_options.product_mod_id = exp_store_product_modifiers.product_mod_id
WHERE exp_store_product_options.entry_id IN (1,2,3)
AND mod_name = 'SIZE'