my mySQL query takes too much time (about 12 seconds) - any suggestions - mysql

I have a problem with a mySQL query, as it takes too much times (about 12 seconds) the table have about 70,000 records
here is my query:
SELECT DATE(`orders`.`date`) AS `dater`,
COUNT(*) AS `amount`,
( SELECT SUM(`amount`) FROM `payment`
WHERE DATE(`dater`)=DATE(`payment`.`posted_date`)
) AS `charge`
FROM `orders`
GROUP BY `dater`
ORDER BY `dater` DESC
LIMIT 0,10
As you can see there is 2 tables
1. table: orders
it is the orders tables here we have 1 main column: date (we need here to count the orders per day)
sample:
--------------------
date | id
--------------------
01-01-2017 | 1
--------------------
01-01-2017 | 2
--------------------
01-02-2017 | 3
--------------------
table: payment:
it is the payments table here we have 2 main columns: posted_date, amount (we need here to get the sum of amount field for the every day)
sample:
--------------------
posted_date | amount
--------------------
01-01-2017 | 100
--------------------
01-01-2017 | 50
--------------------
01-02-2017 | 200
--------------------
so at end the results should be [date, amount, charge]
sure in less time, as it is impossible to go with this 12 seconds :D
I see that the problem is on this internal SELECT:
(SELECT SUM(`amount`) FROM `payment`
WHERE DATE(`dater`)=DATE(`payment`.`posted_date`)
) AS `charge`
any suggestion of how can I avoid making SELECT inside the SELECT query?

You have a correlated subquery. MySQL's query planner handles those in a naive way, to put it politely.
Refactor your query to use a join instead. Your correlated subquery would look like this as a joinable query.
SELECT SUM(amount) charge,
DATE(posted_date) posted_date
FROM payment
GROUP BY DATE(posted_date)
This gets one row per day from the payment table.
Next, you need to get a similar sort of result from your orders table.
SELECT DATE(date) AS dater,
COUNT(*) AS amount
FROM orders
GROUP BY DATE(date)
Then, join those two together
SELECT a.dater, a.amount, b.charge
FROM (
SELECT DATE(date) AS dater,
COUNT(*) AS amount
FROM orders
GROUP BY DATE(date)
) a
LEFT JOIN (
SELECT SUM(amount) charge,
DATE(posted_date) posted_date
FROM payment
GROUP BY DATE(posted_date)
) b ON a.dater = b.posted_date
ORDER BY a.dater DESC
LIMIT 0,10
It's necessary to join two subqueries here, because you need two aggregates by date for the join to work.

Related

MySQL most price change over time

price | date | product_id
100 | 2020-09-21 | 1
400 | 2020-09-20 | 2
300 | 2020-09-20 | 3
200 | 2020-09-19 | 1
400 | 2020-09-18 | 2
I add an entry into this table every day with a product's price that day.
Now I want to get most price drops for the last week (all dates up to 2020-09-14), in this example it would only return the product_id = 1, because that's the only thing that changed.
I think I have to join the table to itself, but I'm not getting it to work.
Here's something that I wanted to return the most price changes over the last day, however it's not working.
select pt.price, pt.date, pt.product_id, (pt.price - py.price) as change
from prices as pt
inner join (
select *
from prices
where date > '2020-09-20 19:33:43'
) as py
on pt.product_id = py.product_id
where pt.price - py.price > 0
order by change
I understand that you want to count how many times the price of each product changed over the last 7 days.
A naive approach would use aggregation and count(distinct price) - but it fails when a product's price changes back and forth.
A safer approach is window functions: you can use lag() to retrieve the previous price, and compare it against the current price; it is then easy to aggregate and count the price changes:
select product_id, sum(price <> lag_price) cnt_price_changes
from (
select t.*, lag(price) over(partition by product_id order by date) lag_price
from mytable t
where date >= current_date - interval 7 day
) t
group by product_id
order by price_changes desc
Try using MAX() and MIN() instead....
select MAX(pt.price), MIN(pt.price), MAX(pt.price) - MIN(pt.price) as change
from prices as pt
inner join (
select *
from prices
where date > '2020-09-20 19:33:43'
) as py
on pt.product_id = py.product_id
order by change
Instead of subtracting every row by every other row to get the result, you can find the max and min's easily by means of MAX() and MIN(), and, ultimately, **MAX() - MIN()**. Relevant lines from the linked MySQL documentation...
MAX(): Returns the maximum value of expr.
MIN(): Returns the minimum value of expr.
You won't be able to pull the other fields (id's, dates) since this is a GROUP BY() implied by the MAX() and MIN(), but you should then be able to get that info by query SELECT * FROM ... WHERE price = MAX_VALUE_JUST_ACQUIRED.
This examples will get you results per WeekOfYear and WeekOfMonth regarding the lowering of the price per product.
SELECT
COUNT(m1.product_id) as total,
m1.product_id,
WEEK(m1.ddate) AS weekofyear
FROM mytest m1
WHERE m1.price < (SELECT m2.price FROM mytest m2 WHERE m2.ddate<m1.ddate AND m1.product_id=m2.product_id LIMIT 0,1)
GROUP BY weekofyear,m1.product_id
ORDER BY weekofyear;
SELECT
COUNT(m1.product_id) as total,
m1.product_id,
FLOOR((DAYOFMONTH(ddate) - 1) / 7) + 1 AS weekofmonth
FROM mytest m1
WHERE m1.price < (SELECT m2.price FROM mytest m2 WHERE m2.ddate<m1.ddate AND m1.product_id=m2.product_id LIMIT 0,1)
GROUP BY weekofmonth,m1.product_id
ORDER BY weekofmonth;
Try this out in SQLFiddle.

Select one line, one line above and one line below [duplicate]

This question already has answers here:
MySQL select before after row
(4 answers)
Closed 3 years ago.
I have a table with a value column. I need to select the line chosen by the user (on the front), one line above and one line below, so:
My 'orders' table rows
----------------------------------------------
id | price |
----------------------------------------------
1 | 1500.00 |
2 | 1380.00 |
3 | 1880.00 |
4 | 1900.00 |
5 | 1450.00 |
6 | 1700.00 |
If the person chose: 1450.00, I want to bring 1450.00, 1380.00 and 1500.00. Is it possible to make this MySql select? or will i have to do this in php?
I only have the initial query for now:
SELECT * FROM `orders`
ORDER BY price;
You could select max and min price to get row. Let try this code:
SELECT *
FROM orders A
WHERE A.price = 1450.00
OR A.price = (SELECT MAX(B.price) FROM orders B WHERE B.price < 1450.00)
OR A.price = (SELECT MIN(B.price) FROM orders B WHERE B.price > 1450.00)
One way you could do it is using 2 MySQL statements. Something like:
SELECT * FROM `orders` WHERE `price`>=$user_price ORDER BY `price` ASC LIMIT 2
Which should give you the user's selected price and the row above. Then you can execute
SELECT * FROM `orders` WHERE `price`<$user_price ORDER BY `price` DESC LIMIT 1
Which should give you the row below.
I'm not sure if there's a way to combine the two queries, but if I do find anything I'll update the answer :)
Here's how to get this, use max() on < 1450 and and min() on > 1450
select * from test
where price in
(select price from test where price = 1450)
or price in
(select max(price) from test where price < 1450)
or price in
(select min(price) from test where price > 1450)
instead of using or, use in instead to more cleaner.
select * from test
where price in (1450,
(select max(price) from test where price < 1450),
(select min(price) from test where price > 1450))
see dbfiddle
If the prices are unique and you want exactly three rows:
(select o.*
from orders o
where o.price <= :price
order by o.price
limit 2
) union all
(select o.*
from orders o
where o.price > :price
order by o.price desc
limit 1
);
Your question does not clarify what to do in these cases:
When the price is less than or equal to the minimum price.
When the price is greater than or equal to the maximum price.
When there are duplicate prices.
I think this query should work
select price
from orders
where price <= (select min(price) from orders where price > 1450.00)
order by price desc
limit 3

Need help on MySQL query, i need to get the starting balance and the end balance by date group by stock_id

I need to get the starting balance from the earliest date and the ending balance from month end and group by stock_id.
My table:
id stock_id balance transact_at
1 1 100 2018-06-15
2 1 70 2018-06-16
3 1 30 2018-06-31
4 2 50 2018-06-01
5 2 10 2018-03-31
I want output:
stock_id start_balance ending_balance
1 100 30
2 50 10
Try this one. In this one two inner queries are fetching starting balance and closing balance by getting minimum and maximum transact_at corresponding to a stock_id and then the parent query is combing the two queries to get starting and closing balance in an single row. I have also shared fiddle link below to try.
select
tabledata1.stock_id,
startBalance,
closingBalance
from (
select
table1.stock_id,
balance as startBalance
from table1 join
(
select stock_id,
min(transact_at) as transact_at
from Table1 group by stock_id
) startTransaction
on Table1.stock_id = startTransaction.stock_id and
Table1.transact_at = startTransaction.transact_at
) tabledata1
join (
select
table1.stock_id,
balance as closingBalance
from table1 join
(
select stock_id,
max(transact_at) as transact_at
from Table1 group by stock_id
) endTransaction
on Table1.stock_id = endTransaction.stock_id
and Table1.transact_at = endTransaction.transact_at
) tabledata2
on tabledata1.stock_id = tabledata2.stock_id;
Demo
One approach in MySQL would be to aggregate by stock_id once and find the opening and closing dates. Then, self-join twice to pull in the actual balances which occurred on those opening and closing dates.
SELECT
t1.stock_id,
t2.balance AS start_balance,
t3.balance AS ending_balance
FROM
(
SELECT
stock_id,
MIN(transact_at) AS min_transact_at,
MAX(transact_at) AS max_transact_at
FROM my_table
GROUP BY stock_id
) t1
INNER JOIN my_table t2
ON t1.stock_id = t2.stock_id AND t2.transact_at = t1.min_transact_at
INNER JOIN my_table t3
ON t1.stock_id = t3.stock_id AND t3.transact_at = t1.max_transact_at;
Demo
Note: For posterity's sake, when MySQL 8+ becomes the norm, we could make use of things like ROW_NUMBER here, which might make it easier to get the result we want.
Try This One.
SELECT stock_id,MAX(balance) as start_balance, MIN(balance) as ending_balance FROM tbl_balance GROUP BY stock_id

How to get rows with max date when grouping in MySQL?

I have a table with prices and dates on product:
id
product
price
date
I create a new record when price change. And I have a table like this:
id product price date
1 1 10 2014-01-01
2 1 20 2014-02-17
3 1 5 2014-03-28
4 2 25 2014-01-05
5 2 12 2014-02-08
6 2 30 2014-03-12
I want to get last price for all products. But when I group with "product", I can't get a price from a row with maximum date.
I can use MAX(), MIN() or COUNT() function in request, but I need a result based on other value.
I want something like this in final:
product price date
1 5 2014-03-28
2 30 2014-03-12
But I don't know how. May be like this:
SELECT product, {price with max date}, {max date}
FROM table
GROUP BY product
Alternatively, you can have subquery to get the latest get for every product and join the result on the table itself to get the other columns.
SELECT a.*
FROM tableName a
INNER JOIN
(
SELECT product, MAX(date) mxdate
FROM tableName
GROUP BY product
) b ON a.product = b.product
AND a.date = b.mxdate
I think the easiest way is a substring_index()/group_concat() trick:
SELECT product,
substring_index(group_concat(price order by date desc), ',', 1) as PriceOnMaxDate
max(date)
FROM table
GROUP BY product;
Another way, that might be more efficient than a group by is:
select p.*
from table t
where not exists (select 1
from table t2
where t2.product = t.product and
t2.date > t.date
);
This says: "Get me all rows from the table where the same product does not have a larger date." That is a fancy way of saying "get me the row with the maximum date for each product."
Note that there is a subtle difference: the second form will return all rows that on the maximum date, if there are duplicates.
Also, for performance an index on table(product, date) is recommended.
You can use a subquery that groups by product and return the maximum date for every product, and join this subquery back to the products table:
SELECT
p.product,
p.price,
p.date
FROM
products p INNER JOIN (
SELECT
product,
MAX(date) AS max_date
FROM
products
GROUP BY
product) m
ON p.product = m.product AND p.date = m.max_date
SELECT
product,
price,
date
FROM
(SELECT
product,
price,
date
FROM table_name ORDER BY date DESC) AS t1
GROUP BY product;

(My)SQL: group rows by a given field and force the newest data to be used in the grouped row

I have an invoices table which stores a history of invoices for every order, so one order can have multiple invoices.
However in everyday use I only want to select the newest invoice of every order.
An example of two invoices assigned to the same order:
invoice_id | order_id | invoice_number | created_at
=====================================================
1 | 42 | 10621 | 2014-05-28
2 | 42 | 10621 | 2014-05-31
I tryed the following
SELECT * FROM invoices GROUP BY order_id;
which groups the rows by the first row it finds, which is the oldest invoice. Adding an 'ORDER BY created_at DESC' clause doesn't change that.
Is there a way to get only the newest row for each order_id?
Use a self join on the maximum date of invoice,group by results are indeterminate they can't guarantee the order of results to be grouped below query should do the trick
SELECT
i.*
FROM
invoices i
JOIN
(SELECT
order_id,
MAX(created_at) created_at
FROM
invoices
GROUP BY order_id) ii
ON (
i.order_id = ii.order_id
AND i.created_at = ii.created_at
)
Here is a reasonable way to get the most recent row with your data. Note that it does not use group by:
select i.*
from invoices
where not exists (select 1
from invoices i2
where i2.order_id = i.order_id and i2.created_at > i.created_at
);
If performance is a concern, you will want an index on invoices(order_id, created_at).
This version changes the question from "Get me the invoice with the biggest date for each order" to "Get me the invoice for each order such that no other invoice for that order has a larger date".