Selecting other side of many to many relationship - mysql

Background
I have three tables: (SQL fiddle: http://sqlfiddle.com/#!2/f7b33/11)
products
+----+-----------+
| id | product |
+----+-----------+
| 1 | product_1 |
| 2 | product_2 |
| 3 | product_3 |
+----+-----------+
products_features
+------------+------------+
| product_id | feature_id |
+------------+------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 4 |
| 2 | 1 |
| 2 | 3 |
| 3 | 4 |
+------------+------------+
features
+----+-----------+
| id | feature |
+----+-----------+
| 1 | feature_1 |
| 2 | feature_2 |
| 3 | feature_3 |
| 4 | feature_4 |
+----+-----------+
I'm then selecting like this:
SELECT products.product,
GROUP_CONCAT(features.feature) AS features
FROM products
LEFT JOIN products_features
ON product_id = products.id
LEFT JOIN features
ON products_features.feature_id = features.id
GROUP BY products.id
to get something like:
+-----------+-----------------------------------------+
| product | features |
+-----------+-----------------------------------------+
| product_1 | feature_1,feature_2,feature_3,feature_4 |
| product_2 | feature_1,feature_3 |
| product_3 | feature_4 |
+-----------+-----------------------------------------+
Question
So, everything is great, However, what I'd like to do is to only have things that have feature_1 and feature_3, whilst still getting the other features.
In other words, I'd like to write a query that would get me:
+-----------+-----------------------------------------+
| product | features |
+-----------+-----------------------------------------+
| product_1 | feature_1,feature_2,feature_3,feature_4 |
| product_2 | feature_1,feature_3 |
+-----------+-----------------------------------------+
I've tried:
SELECT products.product,
GROUP_CONCAT(features.feature) AS features
FROM products
LEFT JOIN products_features
ON product_id = products.id
RIGHT JOIN features
ON products_features.feature_id = features.id AND features.feature in ('feature_1','feature_3')
GROUP BY products.id
but of course I get:
+-----------+---------------------+
| product | features |
+-----------+---------------------+
| (null) | feature_4,feature_2 |
| product_1 | feature_1,feature_3 |
| product_2 | feature_3,feature_1 |
+-----------+---------------------+
So though I now know product_1 and product_2 are the ones with those features, I can't see the rest of the features they have.
What query will give me allow me to specify feature_1 and feature_3 and get the following response?
+-----------+-----------------------------------------+
| product | features |
+-----------+-----------------------------------------+
| product_1 | feature_1,feature_2,feature_3,feature_4 |
| product_2 | feature_1,feature_3 |
+-----------+-----------------------------------------+

I think the most generalizable way to approach this is with a having clause:
SELECT products.product,
GROUP_CONCAT(features.feature) AS features
FROM products
LEFT JOIN products_features
ON product_id = products.id
LEFT JOIN features
ON products_features.feature_id = features.id
GROUP BY products.id
HAVING sum(features.feature = 'feature_1') > 0 and
sum(features.feature = 'feature_3') > 0;
Each clause in the having statement is counting the number of times that a given feature appears. The and is requiring that both features be in the final result set.
EDIT:
Given the structure of your statement, you could also do:
HAVING find_in_set('feature_1', features) > 0 and
find_in_set('feature_3', features) > 0;
This works because you are producing a column with the list of features and you are using a comma as a separator for that list.

Returning data from a group_concat and then splitting the results back in your front-end is a no go. Not only will it result in inefficient use of resources but also might result in errors in the splitting (eg: imagine what would happen when a feature appears and it happens to contain a ,).
The third and main reason not to use a group_concat is that it has a limit on the length. I'm not re-explaining the wheel but check this question for more information.
The best approach will be to return all the matching features for a given product and then just process them in a loop. It should be pretty simple to do (actually, most UI components, web or not, would expect to receive a collection to display them and this is what you're sending to them).
Additionally, it is most likely that you already have the ids of the features that you want to check so it would be more efficient to check for them instead of strings.
My last comment is that you don't actually need left joins in there. A left join will return all elements from the left regardless of whether they have a match in the right. However, you need the right side to have 2 elements (the 2 features) which makes the query contradictory. You just need an inner join in there.
This is the query I would use:
SELECT p.product, f.feature FROM products p
JOIN (
SELECT product_id FROM products_features
WHERE feature_id IN (1, 3)
GROUP BY product_id
HAVING count(*) = 2
) pf ON p.id = pf.product_id
JOIN products_features pf2 ON p.id = pf2.product_id
JOIN features f ON pf2.feature_id = f.id
This is the fiddle for that query.

Related

Grouping by a field, then counting occurrences based on the results from a separate table

This is tough to explain so I'll add tables in to hopefully catch the things I don't type out well.
I have a table of products each with a country id. I want to get the count of unique products for each country id. However there are a couple tables I want to join by to determine if the sku should be counted.
I have prods table that looks like this
| key_id | c_id | sku |
|-----------|--------|-------|
| 1 | 1 | ABC |
| 2 | 2 | ABC |
| 3 | 3 | ABC |
| 4 | 1 | DEF |
| 5 | 2 | DEF |
A filter table (in my code it's a few inner joins of a few tables, but the goal is to make something that works like this)
| sku | want_sku |
|-------|---------------|
| ABC | 0 |
| DEF | 1 |
and this is the desired end result of my query
| c_id | # of unique_skus |
|--------|----------------------|
| 1 | 1 |
| 2 | 1 |
| 3 | 0 |
This is what i've pieced together so far, but it's getting me the total # of skus so something is off.
SELECT
prods.c_id,
COUNT(DISTINCT prods.SKU)
FROM
prods
INNER JOIN
filter
ON filter.sku = prods.sku
WHERE
filter.want_sku = 1
GROUP BY
prods.c_id
This just gets me the max # of distinct skus and assigns that to each of the different c_id. Not sure quite exactly how to fix it. Any help/advice would be greatly appreciated, thanks!
I think you want a left join:
SELECT p.c_id,
COUNT(DISTINCT f.SKU)
FROM prods p LEFT JOIN
filter f
ON f.sku = p.sku AND
f.want_sku = 1
GROUP BY p.c_id;
Note: I don't think you need COUNT(DISTINCT), but that is how you phrased it.

How to get prestashop product attributes through SQL

I need to build a report for a prestashop site showing product id, name, and stock. So far I've done just that through the following query:
SELECT product.id_product, product_lang.name, stock_available.quantity
FROM ps_product product, ps_product_lang product_lang, ps_stock_available stock_available
WHERE stock_available.id_product = product.id_product
AND stock_available.id_product = product.id_product
AND product_lang.id_lang =1
AND product.reference = ''
AND product_lang.id_product = product.id_product
which outputs something like:
|===============================|
| id | name | stock |
| 1 | earring | 45 |
|===============================|
The problem is that some products have the same name and same ID but different attributes, so i'm getting stuff like this:
|===============================|
| id | name | stock |
| 1 | earring | 45 |
| 1 | earring | 76 |
| 1 | earring | 9 |
|===============================|
What i need is to add a new field showing the attributes that differentiate them, so the final output would be something like this:
|================================================|
| id | name | attributes | stock |
| 1 | earring | yellow, short | 45 |
| 1 | earring | red, short | 76 |
| 1 | earring | red, long | 9 |
|================================================|
But I can't figure out how the attributes tables (because there are several of them) relate to the products table in order to add the proper statements to the query and get the desired behaviour.
If there are any prestashop database experts around, or anyone that could help, I would really appreciate it.
Try this:
SELECT DISTINCT
p.id_product, pl.name, psa.quantity
FROM ps_product p
LEFT JOIN ps_product_lang pl ON (pl.id_product = p.id_product)
LEFT JOIN ps_stock_available psa ON (p.id_product = psa.id_product)
GROUP by p.id_product

SQL Join vs Sub-query

I'm running MySQL 5.1.71. In my database there are three tables - load, brass and mfg with load being my "main" table. My goal is to query load and have mfg.name included in the results. I've tried various iterations of JOIN clauses vs sub-queries both with and without WHERE clauses. It seems this should be pretty trivial so I'm not sure how I can't arrive at the solution.
load
-------------------------
| id | desc | brass_id |
-------------------------
| 1 | One | 2 |
| 2 | Two | 1 |
-------------------------
brass
---------------
| id | mfg_id |
---------------
| 1 | 6 |
| 2 | 8 |
---------------
brass_mfg
------------------------
| id | name |
------------------------
| 6 | This Company |
| 8 | That Company |
------------------------
My desired results would be...
results
---------------------------
| load | mfg |
---------------------------
| One | That Company |
| Two | This Company |
---------------------------
A load ID will always have only a single brass ID
A brass ID will always have only a single mfg ID
EDIT
The previously provided sample data (above) has been updated. Also, below are the query I'm running and the results I'm getting. The company is wrong in each record that is returned. I've included in the query and the results the IDs across the tables. The company names that appear are not the names in for the IDs in the mfg table.
SELECT
load.id AS "load.id",
load.brass_id AS "load.brass_id",
brass.id AS "brass.id",
brass.mfg_id AS "brass.mfg_id",
brass_mfg.id AS "brass_mfg.id",
brass_mfg.name AS "brass_mfg.name"
FROM `load`
LEFT JOIN brass ON load.brass_id = brass.id
LEFT JOIN brass_mfg ON brass.id = brass_mfg.id
-----------------------------------------------------------------------------------------
| load.id | load.brass_id | brass.id | brass.mfg_id | brass_mfg.id | brass_mfg.name |
-----------------------------------------------------------------------------------------
| 1 | 2 | 2 | 6 | 2 | Wrong Company |
| 2 | 1 | 1 | 8 | 1 | Incorrect Company |
-----------------------------------------------------------------------------------------
Look at your tables and see what data relates to one another then build up joins table by table to get your desired output.
SELECT p.desc AS Product, m.name AS mfg
FROM product p
INNER JOIN lot l ON p.lot_id = l.id
INNER JOIN mfg m ON l.mfg_id = m.id
If this is single - single relationship, why having middle table?
In your case the best scenario is simple join.
SELECT pt.desc as Product, mfg.name as Mfs
FROM Product pt
Join Lot lt on lt.id = pt.lot_id
Join Mfg mf on mf.id = lt.mfg_id
You have an error in your join query.
Try this one:
Select
l.id AS "load.id",
l.brass_id AS "load.brass_id",
b.id AS "brass.id",
b.mfg_id AS "brass.mfg_id",
m.id AS "brass_mfg.id",
m.`name` AS "brass_mfg.name"
FROM `load` as l
LEFT JOIN brass as b ON l.brass_id = b.id
LEFT JOIN brass_mfg as m ON b.mfg_id = m.id
You need LEFT JOIN only

Joining a variable number of tables based on column value

I have a few Models in my code which is modeled in my MySQL database in this structure:
Properties
+----+------+---------+
| id | name | address |
|----+------+---------|
| 1 | p1 | 123 st |
| 2 | p2 | 123 st |
| 2 | p3 | 123 st |
+----+------+---------+
Tenants (belongs to property)
+----+-------------+-------+
| id | property_id | suite |
|----+-------------+-------|
| 1 | 1 | s1 |
| 2 | 1 | s2 |
| 3 | 2 | s3 |
+----+-------------+-------+
Costs (can belong to property or tenants)
+----+--------------+-----------+--------------+
| id | parent_model | parent_id | name |
|----+--------------+-----------+--------------+
| 1 | property | 1 | gardening |
| 2 | property | 2 | construction |
| 3 | tenant | 1 | renovation |
+----+--------------+-----------+--------------+
Files (can belong to any model)
+----+--------------+-----------+--------------+
| id | parent_model | parent_id | name |
|----+--------------+-----------+--------------+
| 1 | property | 1 | file1.jpg |
| 2 | tenant | 2 | file2.pdf |
| 3 | costs | 3 | file3.doc |
+----+--------------+-----------+--------------+
As you can see from the table structure all models can be linked back to a property record (either directly or via one or more intermediary tables).
In my code I want to write one query that will get the property.id of a file
After looking over this question: Joining different tables based on column value I realized finding a "link" from a file to a property can be done via a few joins.
The number of joins needed is different based on whatever the parent_model is. For file.id = 1 its a matter of joining in the properties table. For file.id = 3 we must join in costs, tenants, and properties
How should a query be written that can get a property.id for all of my files records?
Edit:
This would be a sample output:
+---------+-------------+
| file_id | property_id |
|---------+-------------|
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
+---------+-------------+
In this case all the files worked out to be associated with property_id 1 but this may not always be the case.
I don't think there's a shortcut. You need to traverse all the paths something along these lines
select -- property files
FileID, parent_id
from Files
where parent_model='property'
union
select -- property costs
FileID, c.parent_id
from
Files inner join costs C on Files.parent_id=c.id and c.parent_model='property'
where Files.parent_model='costs'
union
select -- tenant costs
FileID, t.parent_id
Files inner join costs C on Files.parent_id=c.id and c.parent_model='tenant'
inner join tenant t on t.id=c.parent_id
where Files.parent_model='costs'
.... etc.
i.e. just string together all the variations then UNION
You should be able to do this like so:
SELECT
P.id,
P.name,
P.address,
F.name,
...
FROM
Files F
LEFT OUTER JOIN Costs C ON
F.parent_model = 'Cost' AND C.id = F.parent_id
LEFT OUTER JOIN Tenants T ON
(F.parent_model = 'Tenant' AND T.id = F.parent_id) OR
(C.parent_model = 'Tenant' AND T.id = C.parent_id)
LEFT OUTER JOIN Properties P ON
(F.parent_model = 'Property' AND P.id = F.parent_id) OR
(C.parent_model = 'Property' AND P.id = C.parent_id) OR
(P.id = T.property_id)
Depending on your data, this might break down. I don't think that I like the table design in this case.

How to join tables with SQL query and take number of tied columns?

I'm having BookTable in database (with foregin hey LibID):
| BookID | BookName | BookPrice | LibID |
-------------------------------------------
| 1 | Book_1 | 200 | 1 |
| 2 | Book_2 | 100 | 1 |
| 3 | Book_3 | 300 | 2 |
| 4 | Book_4 | 150 | 4 |
and also LibraryTable:
| LibID | LibName | LibLocation |
-----------------------------------
| 1 | Lib_1 | Loc_1 |
| 2 | Lib_2 | Loc_2 |
| 3 | Lib_3 | Loc_3 |
| 4 | Lib_4 | Loc_4 |
I need to write SQL query that will return be the info about the library and number of books for that library:
| LibID | LibName | NumberOfBooks|
------------------------------------
| 1 | Lib_1 | 2 |
| 2 | Lib_2 | 1 |
| 3 | Lib_3 | 0 |
| 4 | Lib_4 | 1 |
It should be one SQL query, probably with nested queries or joins.. Not sure how the query should look like:
SELECT L.LibID AS LibID, L.LibName AS LibName, COUNT(B) AS NumberOfBooks
FROM LibraryTable L, BookTable B
WHERE L.LibID = B.LibID
Will that work?
No, this query will not work. COUNT aggregates data, so you must explicitely tell the DBMS for which group of data you want the count. In your case this is the library (you want one result record per library).
COUNT's parameter is a column, not a table, so change this to * (i.e. count records) or a certain column (e.g. LibID).
The join syntax you are using is valid, but deprecated. Use explicit joins instead. In your case an outer join would even show libraries that have no books at all, if such is possible.
select l.libid, l.libname, count(b.libid) as numberofbooks
from librarytable l
left outer join booktable b on b.libid = l.libid
group by l.libid;
You could also do all this without a join at all and get the book count in a subquery instead. Then you wouldn't have to aggregate. That's way simpler and more readable in my opinion.
select
l.libid,
l.libname,
(select count(*) booktable b where b.libid = l.libid) as numberofbooks
from librarytable l;
SELECT lt.LibID AS LibID, lt.LibName AS LibName, count(*) AS NumberOfBooks
FROM BookTable AS bt
LEFT JOIN LibraryTable AS lt ON bt.LibID = lt.LibID
GROUP BY bt.LibID