MySql query help : joints with sums and counts - mysql

I have this database structure:
TBL_A | TBL_B | TBL_C | TBL_D | TBL_E
-------+---------+---------+---------+----------
id | id_tbla | id_tbla | id_tbla | id
name | id_user | id_user | id_user | name_tbla
... | is_bool | | weight | id_user
Here is what I'm trying to achieve :
SELECT
a.id,
a.name,
b.is_bool,
count(c.id_user) AS nb_views,
sum(d.weight) AS total_weight,
count(distinct e.id_user) AS distinct_users,
FROM TBL_A AS a
LEFT JOIN (TBL_B AS b) on (b.id_tbla = a.id)
LEFT JOIN (TBL_C AS c) on (c.id_tbla = a.id)
LEFT JOIN (TBL_D AS d) on (d.id_tbla = a.id)
LEFT JOIN (TBL_E AS e) on (e.name_tbla = a.name)
where a.id = 1 and e.id_user = 1
The query is performed but the results (nb_views, total_weight, distinct_users) are wrong. Any idea why?

You're trying to compute too many aggregates in one query.
Enita non sunt multiplicanda praeter necessitatem
(Latin, "entities are not to be multiplied beyond necessity")
Your tables B, C, D, and E are produced Cartesian Products against each other. Suppose the
given row in A matches:
3 rows in B
6 rows in C
4 rows in D
1 row in E
The total number of rows in the result is 3 * 6 * 4 * 1 = 72 rows. So your count(c.id_user) is 12 times what it should be, your sum(d.weight) is 18 times what it should be, etc.
The simplest remedy is to compute each of these aggregates in a separate query:
SELECT a.id, a.name, COALESCE(b.is_bool, FALSE) AS is_bool
FROM TBL_A AS a LEFT JOIN TBL_B AS b ON (b.id_tbla = a.id)
WHERE a.id = 1;
SELECT a.id, COUNT(c.id_user) AS nb_views
FROM TBL_A AS a LEFT JOIN TBL_C AS c ON (c.id_tbla = a.id)
WHERE a.id = 1;
SELECT a.id, SUM(d.weight) AS total_weight,
FROM TBL_A AS a LEFT JOIN TBL_D AS d ON (d.id_tbla = a.id)
WHERE a.id = 1;
SELECT a.id, COUNT(DISTINCT e.id_user) AS distinct_users,
FROM TBL_A AS a LEFT JOIN TBL_E AS e
ON (e.name_tbla = a.name AND e.id_user = 1)
WHERE a.id = 1;

Related

Optimize and reduce size of Union Select Query

My question is about how to optimize and reduce size of a sql query. I want to join more than 20 multiple queries using UNION, it is giving me the correct result as per the below logic, but I am looking for two things here
something more efficient
I already have 20 UNIONS in my query, and every month I have to add 2-4 UNIONS more which make this query very long so is there any way this query can be rephrased with less code
Select
'343' As 'Manual ID',
'24/07/2020' As 'Date',
A.ID,
O.Order_Name,
C.Customer_Name,
Q.Quantity
From Shipper A
Left Join Order O A.ID = O.ID
Left Join Customer C A.ID = C.ID
Left Join Quantity Q Q.ID = C.ID
where A.ID IN (1)
UNION
Select
'323' As 'Manual ID',
'24/08/2020' As 'Date',
A.ID,
O.Order_Name,
C.Customer_Name,
Q.Quantity
From Shipper A
Left Join Order O A.ID = O.ID
Left Join Customer C A.ID = C.ID
Left Join Quantity Q Q.ID = C.ID
where A.ID IN(2,3,4)
and so on ...
Result
Manual ID | Date | Shipper | Order Name | Customer Name | Qty
343 | 24/07/2020 | 1 | order1 | A | 5
323 | 24/08/2020 | 2 | order2 | B | 2
323 | 24/08/2020 | 3 | order3 | C | 1
323 | 24/08/2020 | 4 | order4 | D | 12
You can try this:
Select
CASE
WHEN A.ID IN(1) THEN '343'
WHEN A.ID IN(2,3,4) THEN '323'
END As 'Manual ID',
CASE
WHEN A.ID IN(1) THEN '24/07/2020'
WHEN A.ID IN(2,3,4) THEN '24/08/2020'
END As 'Date',
A.ID,
O.Order_Name,
C.Customer_Name,
Q.Quantity
From Shipper A
Left Join Order O A.ID = O.ID
Left Join Customer C A.ID = C.ID
Left Join Quantity Q Q.ID = C.ID
Where A.ID IN(1,2,3,4)
First suggestion is to move the parameters in to another table, then join on it. You can even make that an inline view if you don't want to use a real table...
Second suggestion is to use UNION ALL to avoid the costs of deduplication incurred by UNION.
SELECT
params.*,
O.Order_Name,
C.Customer_Name,
Q.Quantity
FROM
(
SELECT '343' As 'Manual ID', '24/07/2020' As 'Date', 1 AS A_ID
UNION ALL SELECT '323' As 'Manual ID', '24/08/2020' As 'Date', 2 AS A_ID
UNION ALL SELECT '323' As 'Manual ID', '24/08/2020' As 'Date', 3 AS A_ID
UNION ALL SELECT '323' As 'Manual ID', '24/08/2020' As 'Date', 4 AS A_ID
)
AS params
INNER JOIN Shipper A ON A.ID = params.A_ID
Left Join Order O ON A.ID = O.ID
Left Join Customer C ON C A.ID = C.ID
Left Join Quantity Q ON Q.ID = C.ID
Alternatively, don't recompute this every month. Write a new query each month, and insert the results into another table?
If you just want to go for query the better way would be to use the case when statement but every now and then you need to keep updating the query adding new cases.
Another, optimized solution will be to create a new table to store
Manual ID, Date, (Common) ID present in Shipper (Table). Then create a view to join all above tables with new Table.
New Table
Manual ID | Date | ID |
343 | 24/07/2020 | 1 |
323 | 24/08/2020 | 2 |
323 | 24/08/2020 | 3 |
323 | 24/08/2020 | 4 |
Then Create a View Joining all Tables including new new table with ID.
In this you just need add new value to new table and you will complete result in view it self.
CREATE VIEW MY_VIEW
AS
SELECT * FROM
(
Select
T.[Manual ID],
T.[Date],
A.ID,
O.Order_Name,
C.Customer_Name,
Q.Quantity
From Shipper A
Left Join Order O A.ID = O.ID
Left Join Customer C A.ID = C.ID
Left Join Quantity Q Q.ID = C.ID
Left Join NewTable T T.ID = A.ID
)
Now just insert value in new table and fetch complete data from MY_VIEW. It will give the same result as you are excepting.

Mysql SELECT only unique values in one column when left joined with another table

This is the query:
SELECT a.id, a.userName,if(o.userId=1,'C',if(i.userId=1,'I','N')) AS relation
FROM tbl_users AS a
LEFT JOIN tbl_contacts AS o ON a.id = o.contactId
LEFT JOIN tbl_invites AS i ON a.id = i.invitedId
ORDER BY relation
This returns the output as follows:
+----+--------------+-------------+
| ID | USERNAME | RELATION |
+----+--------------+-------------+
| 1 | ray | C |
+----+--------------+-------------+
| 2 | john | I |
+----+--------------+-------------+
| 1 | ray | N |
+----+--------------+-------------+
I need to remove the third row from the select query by checking if possible that id is duplicate. The priority is as follows:
C -> I -> N. So since there is already a "ray" with a C, I dont want it again with an I or N.
I tried adding distinct(a.id) but it doesn't work. How do I do this?
Why doesn't DISTINCT work for this?
From the specs you gave, all you have to do is group by ID and username, then pick the lowest value of relation you can find (since C < I < N)
SELECT a.id, a.userName, MIN(if(o.userId=1,'C',if(i.userId=1,'I','N'))) AS relation
FROM tbl_users AS a
LEFT JOIN tbl_contacts AS o ON a.id = o.contactId
LEFT JOIN tbl_invites AS i ON a.id = i.invitedId
GROUP BY a.id, a.username
There are multiple ways to get the group-wise maximum/minimum as you can see in this manual page.
The best one suited for you is the first one, if the order of the rows can not be defined by alphabetic order.
In this case, given if the desired order were z-a-m (see Rams' comment) you'd need the FIELD() function.
So your answer is
SELECT
a.id,
a.userName,
if(o.userId=1,'C',if(i.userId=1,'I','N')) AS relation
FROM tbl_users a
LEFT JOIN tbl_contacts AS o ON a.id = o.contactId
LEFT JOIN tbl_invites AS i ON a.id = i.invitedId
WHERE
if(o.userId=1,'C',if(i.userId=1,'I','N')) = (
SELECT
if(o.userId=1,'C',if(i.userId=1,'I','N')) AS relation
FROM tbl_users aa
LEFT JOIN tbl_contacts AS o ON aa.id = o.contactId
LEFT JOIN tbl_invites AS i ON aa.id = i.invitedId
WHERE aa.id = a.id AND aa.userName = a.userName
ORDER BY FIELD(relation, 'N', 'I', 'C') DESC
LIMIT 1
)
Note, you can also do it like ORDER BY FIELD(relation, 'C', 'I', 'N') to have it more readable / intuitive. I turned it the other way round, because if you'd have the possibility of having a 'X' in the relation, the FIELD() function would have returned 0 because X is not specified as a parameter. Therefore it would be sorted before 'C'. By sorting descending and turning the order of the parameters around this can not happen.

Mysql join 4 tables can it be done in one query

My database
category_group(id,name)
category(id,name,cat_group_id)
topic(id,name,cat_id)
comment(id,name,topic_id)
I want to get:
Category Group 1
=====================
Category 1
Count topic | Count comment
-------------------
Category 2
Count topic | Count comment
Category Group 2
=====================
Category 3
Count topic | Count comment
-------------------
Category 4
Count topic | Count comment
I can only do with a lot of different query but I think it is not good practice.
If all the table are strictly related you can use inner join
select a.*, b.*, c.*, d.*
from category as a
inner join category_group as b on a.cat_group_id = b id
inner join topic as c on a.id = c.cat_id
inner join comment as d.topic_id = c.id
else where need use left join
Then for in your case you can do this
select b.name, a.name, count(d.*) as count_commect, count(c.*) as count_topic
from category as a
inner join category_group as b on a.cat_group_id = b id
inner join topic as c on a.id = c.cat_id
inner join comment as d.topic_id = c.id
group by a.name, b.name
order by a.name, b.name

SQL Union query simplification

I don't know if this question is apropriate on this forum.
I have a huge query :
SELECT threshold.id, brand.id, COUNT(brand.id), threshold
FROM current_stock, article, product, brand, delivery, threshold
WHERE current_stock.article_id = article.id
AND article.product_product_code = product.product_code
AND product.brand_id = brand.id
AND article.delivery_id = delivery.id
AND delivery.store_id = 'E260'
AND threshold.brand_id = brand.id
GROUP BY brand.id
HAVING COUNT(brand.id) <= threshold
UNION
SELECT threshold.id, brand.id, 0, threshold
FROM current_stock, article, product, brand, delivery, threshold
WHERE threshold.store_id = 'E260'
AND threshold.brand_id NOT IN (
SELECT brand_id FROM current_stock, article, product, delivery
WHERE current_stock.article_id = article.id
AND article.product_product_code = product.product_code
AND article.delivery_id = delivery.id
AND delivery.store_id = 'E260')
And I think it's possible to do better but after a entire day of try I haven't found a better query giving the same result.
For clarify, I have a stock (with current_stock, article, product and delivery). I also have thresholds. what I want is to check for each thresholds if there is the given minimum amount of stock for the given brand.
My problem is that if there is 0 article of a brand, the first part of the query will not take care about the threshold on this brand. It's why I have added an uggly Union.
Someone have an idea for a better way to do this ?
EDIT
This what I have done after the reading of comments and answers :
SELECT t.id, b.id, t.threshold, count(b.id) stock
FROM threshold t
inner join brand b on b.id = t.brand_id
left join product p on p.brand_id = b.id
inner join article a on a.product_product_code = p.product_code
inner join delivery d on d.id = a.delivery_id
inner join current_stock cs on cs.article_id = a.id
WHERE
t.store_id = 'E260' AND
d.store_id = 'E260'
GROUP BY b.id
HAVING stock <= t.threshold
My problem is that it don't gives all threholds... only ones that have at least one 'current_stock'. I have perhaps don't understand how joins are working.
Here an example of threshold table :
| id | brand_id | threshold |
-----------------------------
| 1 | 86 | 1 |
| 2 | 28 | 1 |
| 3 | 12 | 1 |
What I want as result this :
# with 2 entries in 'current_stock' for the brand id 28, 1 for 12 and 0 for 86
| t.id | b.id | threshold | stock |
-----------------------------------
| 1 | 86 | 1 | 0 |
| 3 | 12 | 1 | 1 |
Guessing a few parts here since you've used implicit joins in your sample. An explicit version would look something like this (provided I guessed correctly for how you are joining the threshold table).
SELECT
t.id,
b.id,
COUNT(b.id),
t.threshold
FROM
current_stock c
inner join article a on a.id = c.article_id
inner join delivery d on d.id = a.delivery_id
inner join product p on p.product_code = a.product_product_code
inner join brand b on b.id = p.brand_id
inner join threshold t on t.brand_id = b.id
WHERE
d.store_id = 'E260'
GROUP BY b.id
HAVING COUNT(b.id) <= t.threshold
Now to get your results to include rows where there aren't any 'articles' you can start switching out the inner joins for left joins. However, you can't simply use left outer join article... in the example above, because the store_id in the WHERE clause will just turn it back into a pseudo inner join.
Instead, is there a different field you can join the delivery table on from current_stock?
EDIT - 07/29/15
I think you're close, you may just have one too many filters and you're counting from 'b' when the wanted outcome suggests you should be counting from 'cs' instead. Try this:
SELECT t.id, b.id, t.threshold, count(cs.id) stock
FROM
threshold t
inner join brand b on b.id = t.brand_id
inner join product p on p.brand_id = b.id
inner join article a on a.product_product_code = p.product_code
left outer join delivery d on d.store_id = t.store_id
left outer join current_stock cs on cs.article_id = a.id
WHERE
t.store_id = 'E260'
GROUP BY b.id
HAVING stock <= t.threshold

How to remove unnecessary join, update Where conditions

I've got three tables in a MySql database that I'm joining in a query to get id/value pairs.
| A | B | C |
| -------- |--------------|---------------|
| id | id | id |
| name | fooId | attributeId |
| desc | value | displayIndex |
| ... | attributeId | ... |
What I have now is:
SELECT C.id, B.value
FROM A, B, C
WHERE A.id = B.attributeId
AND A.id = C.attributeId
AND B.fooId = 25
ORDER BY C.displayIndex
So basically we're joining B and C through A. It used to be that an entry in the C table had to have a corresponding (parent) entry in the A table. However, that will no longer be the case. The C table will still be MOSTLY controlled by the A table, however, there are some instances when we need a stand alone (always on) entry in the C table.
EDIT
I want all the records from B and C that match on attributeId but I also want any record where C.attributeId = -1. Can someone help with what I'd need to do with this query?
Edit #2
Based on feedback and suggestions you guys have made and some googling I now have this:
(SELECT C.id, B.value, C.displayIndex
FROM B, C
WHERE B.attributeId = C.attributeId
AND B.fooId = 25)
UNION
(SELECT C.id, null, C.displayIndex
FROM C
WHERE C.attributeId = -1)
ORDER BY 3
Is there a better what to do this? Are there any problems with UNION?
I've updated my answer to address the edits from the OP.
This will return all records where the attributeId for tables B and C match,
with B.fooId = 25, OR C.attributeId = -1.
When C.attributeId = -1 and there is no match in table B, NULL will be returned in place of B.Value, which appears to be acceptable based on the Edit #2 from the OP
SELECT C.Id, B.Value, C.displayIndex
FROM C
LEFT JOIN B ON C.attributeId = B.attributeId
WHERE B.fooId = 25
OR C.attributeId = -1
ORDER BY C.DisplayIndex ASC
You're probably getting a Cartesian product.
Like Adam Wenger said you'll need to inner join them. Inner joins will return rows when there is at least 1 match in all joined tables.
If you need to ensure that the result is also in Table A, you're going to need to join that one too:
SELECT c.Id, b.Value
FROM B
INNER JOIN A ON B.attributeId = A.ID
INNER JOIN C ON B.attributeId = C.attributeId
WHERE B.fooId = 25
AND C.attributeId = -1
ORDER BY C.DisplayIndex