Optimizing MySQL SELECT query with subqueries - mysql

Hi is it possible to optimize the SELECT query below? The query itself is working but when we are querying large data we are encountering a message in php which is "Maximum execution time of 30 seconds exceeded". I reduced the columns in the query up to Topping3 but I am querying up to Topping15 column.
SELECT
itemID,
itemName,
Topping1,
(SELECT DISTINCT Description FROM items WHERE PLU = a.Topping1 AND ClientID = 1679) AS Top1_desc,
Topping2,
(SELECT DISTINCT Description FROM items WHERE PLU = a.Topping2 AND ClientID = 1679) AS Top2_desc,
Topping3,
(SELECT DISTINCT Description FROM items WHERE PLU = a.Topping2 AND ClientID = 1679) AS Top3_desc,
FROM
items a
WHERE
...
Current data on items table
--------------------------------------------------------------------
| itemID | itemName | Description | Topping1 | Topping2 | Topping3 |
--------------------------------------------------------------------
| 1 | HAM1 | Hamburger | ONI1 | TOMO1 | |
--------------------------------------------------------------------
| 2 | ONI1 | Onion | | | |
--------------------------------------------------------------------
| 3 | TOMO1 | Tomato | | | |
--------------------------------------------------------------------
and this is the expected result
--------------------------------------------------------------------------------------------------------
| itemID | itemName | Description | Topping1 | Top1_desc | Topping2 | Top2_desc | Topping3 | Top3_desc |
--------------------------------------------------------------------------------------------------------
| 1 | HAM1 | Hamburger | ONI1 | Onion | TOMO1 | Tomato | | |
--------------------------------------------------------------------------------------------------------
| 2 | ONI1 | Onion | | | | | | |
--------------------------------------------------------------------------------------------------------
| 3 | TOMO1 | Tomato | | | | | | |
--------------------------------------------------------------------------------------------------------

This should be fast, unless there are a lot of entries with the same client ID. You could add LIMIT 1 after all subqueries, i.e.: (SELECT DISTINCT Description FROM items WHERE PLU = a.Topping1 AND ClientID = 1679 LIMIT 1) etc.
I suspect however that it is an index problem. Are the fields ClientID and PLU indexed?
EDIT: Alternative for your query:
SELECT
itemID,
itemName,
Topping1,
t1.Description AS Top1_desc,
Topping2,
t2.Description AS Top2_desc,
Topping3,
t3.Description AS Top3_desc,
FROM
items a
LEFT JOIN
items t1 ON t1.PLU=a.Topping1 AND t1.ClientID = 1679
LEFT JOIN
items t2 ON t2.PLU=a.Topping2 AND t2.ClientID = 1679
LEFT JOIN
items t3 ON t3.PLU=a.Topping3 AND t3.ClientID = 1679
WHERE
...
GROUP BY
a.itemID
Fields itemID, PLU and ClientID need indexes.

Related

How Affect Group By to Other Second Join Table

I have some table like this
table request_buys
| id | invoice | user_id |
| -- | ----------------- | ------- |
| 3 | 20220405/01104298 | 1 |
table traces
| id | request_buy_id | status_id | created_at |
| -- | -------------- | --------- | ------------------- |
| 37 | 3 | 1 | 2022-03-27 14:12:25 |
| 38 | 3 | 2 | 2022-03-28 14:12:25 |
| 39 | 3 | 3 | 2022-03-29 14:12:25 |
| 40 | 3 | 4 | 2022-03-30 14:12:25 |
| 41 | 3 | 5 | 2022-03-31 14:12:25 |
| 42 | 3 | 6 | 2022-04-01 14:12:25 |
table statuses
| id | nama |
| -- | ----------------- |
| 1 | Order Placed |
| 2 | Order Paid |
| 3 | Accepted |
| 4 | Picked by Courier |
| 5 | In Transit |
| 6 | Delivered |
| 7 | Rated |
| 8 | Rejected |
| 9 | Canceled |
and then i try to design query like below
select
request_buys.invoice,
MAX(traces.id) as traces_id,
MAX(statuses.nama) as statuses_nama
from
`request_buys`
inner join `traces` on `request_buys`.`id` = `traces`.`request_buy_id`
inner join `statuses` on `traces`.`status_id` = `statuses`.`id`
where
`user_id` = 1
group by
request_buys.id
and produces output like the following
output
| invoice | traces_id | statuses_nama |
| ----------------- | --------- | ----------------- |
| 20220405/01104298 | 42 | Picked by Courier |
and the output i expect should be like in the table below
expect
| invoice | traces_id | statuses_nama |
| ----------------- | --------- | ----------------- |
| 20220405/01104298 | 42 | Delivered |
I understand my error is in MAX(statuses.nama) which I should change like removing MAX() in statuses.nama
But i just get error like this "SELECT list is not in GROUP BY clause and contains nonaggregated ... this is incompatible with sql_mode=only_full_group_by"
then I tried some to clear the value "ONLY_FULL_GROUP_BY" with a query like the following
SET sql_mode=(SELECT REPLACE(##sql_mode,'ONLY_FULL_GROUP_BY',''))
and the result is like this
output
| invoice | traces_id | statuses_nama |
| ----------------- | --------- | ----------------- |
| 20220405/01104298 | 42 | Order Placed |
and I'm really stuck at this
and how to make trace_id.status_id from the "GROUP BY" result based on request_buys.id still have a relationship with statuses.id
Your problem lies with your misuse of the MAX(statuses.nama) expression. Based on your expected output,you intend to get the statuses.nama which matches the MAX(traces.id), NOT the MAX(statuses.nama) value which returns the highest value in terms of alphabetic order. In this case, the initial letter 'P' > 'D' . I have tweaked your code a bit and tried it on workbench,supposing there are more than one invoice for a particular user.(e.g insert into request_buys values (4,'20230405/01104298',1); insert into traces values (43,4,7,'2022-04-01 14:12:25');) It works as intended.
select invoice, t.id as traces_id, s.nama as statuses_name from request_buys r
join traces t on r.id=t.request_buy_id
join statuses s on t.status_id=s.id
join
(select traces.request_buy_id, MAX(traces.id) as traces_id
from `request_buys`
inner join `traces` on `request_buys`.`id` = `traces`.`request_buy_id`
where
`user_id` = 1
group by
traces.request_buy_id ) join_t
on t.request_buy_id=join_t.request_buy_id and t.id=join_t.traces_id
;
If I'm understanding correctly, you're trying to retrieve the most recent status for each invoice. Using MAX(nama) won't return that result, because it just picks the maximum status name alphabetically.
Assuming you're using MySQL 8.x, use ROW_NUMBER() to sort and rank the statuses for each invoice, by the most recent date first. Then grab the latest one using where rowNum = 1
WITH cte AS (
SELECT rb.id AS request_buy_id
, rb.invoice
, t.id AS traces_id
, s.nama AS statuses_nama
, ROW_NUMBER() OVER(PARTITION BY rb.id ORDER BY t.created_at DESC) AS RowNum
FROM request_buys rb
INNER JOIN traces t ON rb.id = t.request_buy_id
INNER JOIN statuses s ON t.status_id = s.id
WHERE user_id = 1
)
SELECT *
FROM cte
WHERE RowNum = 1
;
Result:
request_buy_id
invoice
traces_id
statuses_nama
RowNum
3
20220405/01104298
42
Delivered
1
db<>fiddle here

MySQL Group results

the following query:
SELECT COUNT(*) AS count, items.category, customers.sector
FROM customers
LEFT JOIN items ON items.customer_id = customers.id
GROUP BY items.category, customers.sector
ORDER BY customers.sector ASC
Gives me this result:
| count | category | sector |
|-------|-----------------------|--------------|
| 3 | A-Frames & Trolleys | Automotive |
| 4 | Suction Mounts | Automotive |
| 1 | Hand Cups | Automotive |
| 103 | Glazing Tools | Construction |
| 2 | A-Frames & Trolleys | Construction |
| 2 | Suction Mounts | Construction |
|_______|_______________________|______________|
I want the sector column to be unique and to show the category with the biggest count
eg:
| count | category | sector |
|-------|-----------------------|--------------|
| 4 | Suction Mounts | Automotive |
| 103 | Glazing Tools | Construction |
|_______|_______________________|______________|
Thanks
I think the easiest way to do this is the substring_index()/group_concat() trick:
select max(count) as `count`,
substring_index(group_concat(category order by count desc), ',', 1) as category,
sector
from (SELECT COUNT(*) AS count, i.category, c.sector
FROM customers c LEFT JOIN
items i
ON i.customer_id = c.id
GROUP BY i.category, c.sector
) t
group by c.sector;

mysql join 3 tables by id

I have 3 tables to join and need some help to make it work, this is my schema:
donations:
+--------------------+------------+
| uid | amount | date |
+---------+----------+------------+
| 1 | 20 | 2013-10-10 |
| 2 | 5 | 2013-10-03 |
| 2 | 50 | 2013-09-25 |
| 2 | 5 | 2013-10-01 |
+---------+----------+------------+
users:
+----+------------+
| id | username |
+----+------------+
| 1 | rob |
| 2 | mike |
+----+------------+
causes:
+--------------------+------------+
| id | uid | cause | <missing cid (cause id)
+---------+----------+------------+
| 1 | 1 | stop war |
| 2 | 2 | love |
| 3 | 2 | hate |
| 4 | 2 | love |
+---------+----------+------------+
Result I want (data cropped for reading purposes)
+---------+-------------+---------+-------------+
| id | username | amount | cause |
+---------+-------------+---------+-------------+
| 1 | rob | 20 | stop war |
| 2 | mike | 5 | love |
+---------+-------------+-----------------------+
etc...
This is my current query, but returns double data:
SELECT i.*, t.cause as tag_name
FROM users i
INNER JOIN donations tti ON (tti.uid = i.id)
INNER JOIN causes t ON (t.uid = tti.uid)
EDIT: fixed sql schema on fiddle
http://sqlfiddle.com/#!2/0e06c/1 schema and data
How I can do this?
It seems your table's model is not right. There should be a relation between the Causes and Donations.
If not when you do your joins you will get duplicated rows.
For instance. Your model could look like this:
Donations
+--------------------+------------+
| uid | amount | date | causeId
+---------+----------+------------+
| 1 | 20 | 2013-10-10 | 1
| 2 | 5 | 2013-10-03 | 2
| 2 | 50 | 2013-09-25 | 3
| 2 | 5 | 2013-10-01 | 2
+---------+----------+------------+
causes:
+----------------------+
| id | cause |
+---------+------------+
| 1 | stop war |
| 2 | love |
| 3 | hate |
+---------+------------+
And the right query then should be this
SELECT i.*, t.cause as tag_name
FROM users i
INNER JOIN donations tti ON (tti.uid = i.id)
INNER JOIN causes t ON (t.id = tti.causeId)
Try this
SELECT CONCAT(i.username ,' ',i.first_name) `name`,
SUM(tti.amount),
t.cause AS tag_name
FROM users i
LEFT JOIN donations tti ON (tti.uid = i.id)
INNER JOIN causes t ON (t.uid = tti.uid)
GROUP BY i.id
Fiddle
You need to match the id from both the users and causes table at the same time, like so:
SELECT i.*, t.cause as tag_name
FROM users i
INNER JOIN donations tti ON (tti.uid = i.id)
INNER JOIN causes t ON (t.uid = tti.uid and t.id = i.id)
Apologies for formatting, I'm typing this on a phone.

How to get info from three tables with a MIN (MySQL)

I have four tables, like these:
items
| id | name | category |
-------------------------
| 1 | item1 | toy |
| 2 | item2 | toy |
| 3 | item3 | home |
-------------------------
items2
| id | name | category | size |
--------------------------------
| 1 | itemA | toy | s |
| 2 | itemB | home | l |
--------------------------------
prices
| items.id | price |
--------------------
| 1 | 10 |
| 1 | 15 |
| 2 | 20 |
| 3 | 25 |
| 3 | 20 |
--------------------
prices
| items2.id | price |
--------------------
| 1 | 15 |
| 2 | 50 |
| 2 | 40 |
--------------------
I need to get a result which have both, items and items2, with the MIN of each price. In this example the result should be something like this:
| id | name | category | size | minprice |
-------------------------------------------
| 1 | item1 | toy | null | 10 |
| 2 | item2 | toy | null | 20 |
| 3 | item3 | home | null | 20 |
| 1 | itemA | toy | s | 15 |
| 2 | itemB | home | l | 40 |
-------------------------------------------
I also should be able to ORDER BY minprice, but I'm sure when I know how to join them I can do that too.
Thanks in advance!
I would suggest that you merge the tables items and items2. If a field doesn't apply to an item (such as size in this case), that is what null is for.
I haven't tested that this works but you want something like:
SELECT i.id, name, category, null as size, min(prices.price) from items i
JOIN prices on prices.id = i.id
GROUP BY i.id, i.name, i.category, size
UNION ALL
SELECT i2.id, name, category, size, min(prices2.price) from items2 i2
JOIN prices2 on prices2.id = i2.id
GROUP BY i2.id, i2.name, i2.category, i2.size
From what I can tell, you are over complicating things. You only need one table. For tuples in the item relation where there is no size, simply mark it as null.
This is probably an oversimplification. If prices are "sales" or something similar, you could use a left join to build the relation you are looking for
so
SELECT i.id, i.name, i.category, i.size, p.price as min_price FROM items i LEFT JOIN price p where p.price <= 40 ORDER BY min_price

Querying across 6 tables, is there a better way of doing this?

What I did was, I wanted each user to have their own "unique" numbering system. Instead of auto incrementing the item number by 1, I did it so that Bob's first item would start at #1 and Alice's number would also start at #1. The same goes for rooms and categories. I achieved this by creating "mapping" tables for items, rooms and categories.
The query below works, but I know it can definitely be refactored. I have primary keys in each table (on the "ids").
SELECT unique_item_id as item_id, item_name, category_name, item_value, room_name
FROM
users_items, users_map_item, users_room, users_map_room, users_category, users_map_category
WHERE
users_items.id = users_map_item.map_item_id AND
item_location = users_map_room.unique_room_id AND
users_map_room.map_room_id = users_room.room_id AND
users_map_room.map_user_id = 1 AND
item_category = users_map_category.unique_category_id AND
users_map_category.map_category_id = users_category.category_id AND
users_category.user_id = users_map_category.map_user_id AND
users_map_category.map_user_id = 1
ORDER BY item_name
users_items
| id | item_name | item_location |item_category |
--------------------------------------------------------
| 1 | item_a | 1 | 1 |
| 2 | item_b | 2 | 1 |
| 3 | item_c | 1 | 1 |
users_map_item
| map_item_id | map_user_id | unique_item_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
users_rooms
| id | room_name |
----------------------
| 1 | basement |
| 2 | kitchen |
| 3 | attic |
users_map_room
| map_room_id | map_user_id | unique_room_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
users_category
| id | room_name |
----------------------
| 1 | antiques |
| 2 | appliance |
| 3 | sporting goods |
users_map_category
| map_room_id | map_user_id | unique_category_id |
----------------------------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
Rewriting your query with explicit JOIN conditions makes it more readable (while doing the same).
SELECT mi.unique_item_id AS item_id
, i.item_name
, c.category_name
, i.item_value
, r.room_name
FROM users_map_item mi
JOIN users_items i ON i.id = mi.map_item_id
JOIN users_map_room mr ON mr.unique_room_id = i.item_location
JOIN users_room r ON r.room_id = mr.map_room_id
JOIN users_map_category mc ON mc.unique_category_id = i.item_category
JOIN users_category c ON (c.user_id, c.category_id)
= (mc.map_user_id, mc.map_category_id)
WHERE mr.map_user_id = 1
AND mc.map_user_id = 1
ORDER BY i.item_name
The result is unchanged. Query plan should be the same. I see no way to improve the query further.
You should use LEFT [OUTER] JOIN instead of [INNER] JOIN if you want to keep rows in the result where no matching rows are found in the right hand table. You may want to move the additional WHERE clauses to the JOIN condition in this case, as it changes the outcome.