MySQL get the sum of all rows without retrieving all of them - php

This may be a little confusing but please bear with me. Here's the thing:
I have a database that contains ~1000 records, as the following table illustrates:
+------+----------+----------+
| id | date | amount |
+------+----------+----------+
| 0001 | 14/01/15 | 100 |
+------+----------+----------+
| 0002 | 14/02/04 | 358 |
+------+----------+----------+
| 0003 | 14/05/08 | 1125 |
+------+----------+----------+
What I want to do is this:
Retrieve all the records beginning at 2014 and until yesterday:
WHERE `date` > '14-01-01' AND `date` < CURDATE()
But also get the sum of amount up to the current date, this is:
WHERE `date` < CURDATE()
I've already got this working by just selecting all the records based on the second condition, getting the sum, and then excluding those which don't match the first condition. Something like this:
SELECT `id`, `date`, `amount` FROM `table`
WHERE `date` < CURDATE()
And then:
$rows = fetchAll($PDOStatement);
foreach($rows as $row) {
$sum += $row->amount;
if (
strtotime($row->date) > strtotime('14-01-01') &&
strtotime($row->date) < strtotime(date('Y-m-d'))
) {
$valid_rows[] = $row;
}
}
unset $rows;
Is there a way to achieve this in a single query, efficiently? Would a transaction be more efficient than sorting out the records in PHP? This has to be SQL-standard compliant (I'll be doing this on MySQL and SQLite).
Update:
It doesn't matter if the result ends up being something like this:
+------+----------+----------+-----+
| id | date | amount | sum |
+------+----------+----------+-----+
| 0001 | 14/01/15 | 100 | 458 |
+------+----------+----------+-----+
| 0002 | 14/02/04 | 358 | 458 |
+------+----------+----------+-----+
| 0003 | 14/05/08 | 1125 | 458 |
+------+----------+----------+-----+
The worst case would be when the resulting set ends up being the same as the set that gives the sum (in this case appending the sum would be irrelevant and would cause an overhead), but for any other regular cases the bandwith save would be huge.

You can create a special record with your sum and add it at the end of your first query
SELECT * FROM `table` WHERE `date` > '14-01-01' AND `date` < CURDATE()
UNION
SELECT 9999, CURDATE(), SUM(`amount`) FROM `table` WHERE `date` < CURDATE()
Then you will have all your desired record and the record with id 9999 or whatever is your sum

This could be achieved by correlated subquery, something like below:
SELECT *, (SELECT SUM(amount) FROM t WHERE t.date < t1.date) AS PrevAmount
FROM t AS t1
WHERE `date` > '14-01-01' AND `date` < CURDATE()
However it is very unefficient if the number of records is large.

It's hackish, but:
> select * from foo;
+------+------+
| id | val |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 5 | 5 |
+------+------+
5 rows in set (0.02 sec)
> select * from foo
left join (
select sum(val)
from foo
where id < 3
) AS bar ON 1=1
where id < 4;
+------+------+----------+
| id | val | sum(val) |
+------+------+----------+
| 1 | 1 | 3 |
| 2 | 2 | 3 |
| 3 | 3 | 3 |
+------+------+----------+
Basically, do your summing in a joined subquery. That'll attach the sum result to every row in the outer table's results. You'll waste a bit of bandwidth sending that duplicated value out with every row, but it does get you the results in a "single" query.

EDIT:
You can get the SUM using a LEFT OUTER JOIN.
SELECT t1.`id`, t1.`date`, t2.sum_amount
FROM
`table` t1
LEFT OUTER JOIN
(
SELECT SUM(`amount`) sum_amount
FROM `table`
WHERE `date` < CURDATE()
) t2
ON 1 = 1
WHERE t1.`date` > STR_TO_DATE('01,1,2014','%d,%m,%Y') AND t1.`date` < CURDATE();

This will do what you want it to do...optimizing the subquery is the real challenge:
SELECT id,date,amount,(SELECT SUM(amount) FROM table) AS total_amount
FROM table
WHERE date BETWEEN '14-01-01' AND DATE_ADD(CURDATE(), INTERVAL -1 DAY)

Related

SQL - Get multiple values when limit 1

If I have a table like this:
ID | ident | product
1 | cucu1 | 99867 |
2 | kkju7 | 88987 |
3 | sjdu4 | 66754 |
4 | kjhu6 | 76654 |
5 | cucu1 | 98876 |
And use this query: SELECT ident,COUNT(*) FROM sales WHERE status=? AND team=? AND DATE(date) = DATE(NOW() - INTERVAL 1 DAY) GROUP BY ident order by COUNT(*) DESC LIMIT 1
I get the value: cucu1, since that has the most rows.
But if my table is like this:
ID | ident | product
1 | cucu1 | 99867 |
2 | kkju7 | 88987 |
3 | sjdu4 | 66754 |
4 | kkju7 | 76654 |
5 | cucu1 | 98876 |
It should return both cucu1 and kkju7, since they are the highest with same count, but still it gives me only cucu1. What am I doing wrong?
You can use rank():
SELECT ident, cnt
FROM (SELECT ident, COUNT(*) as cnt,
RANK() OVER (ORDER BY COUNT(*) DESC) as seqnum
FROM sales
WHERE status = ? AND team = ? AND
DATE(date) = DATE(NOW() - INTERVAL 1 DAY)
GROUP BY ident
) i
WHERE seqnum = 1;
LIMIT keyword simply limits the results to 1 row, no matter if there are equal values before or after the returned row.
A better solution would be to select the rows which have a count that's equal to max count in the query, which can be achieved by the following query:
SELECT ident,COUNT(*)
FROM sales
WHERE status=?
AND team=?
AND DATE(date) = DATE(NOW() - INTERVAL 1 DAY)
GROUP BY ident
HAVING COUNT(*) = MAX(COUNT(*))

Get rows above and below (neighbouring rows) a certain row, based on two criteria SQL

Say I have a table like so:
+---+-------+------+---------------------+
|id | level |score | timestamp |
+---+-------+------+---------------------+
| 4 | 1 | 70 | 2021-01-14 21:50:38 |
| 3 | 1 | 90 | 2021-01-12 15:38:0 |
| 1 | 1 | 20 | 2021-01-14 13:10:12 |
| 5 | 1 | 50 | 2021-01-13 12:32:11 |
| 7 | 1 | 50 | 2021-01-14 17:15:20 |
| 8 | 1 | 55 | 2021-01-14 09:20:00 |
| 10| 2 | 99 | 2021-01-15 10:50:38 |
| 2 | 1 | 45 | 2021-01-15 10:50:38 |
+---+-------+------+---------------------+
What I want to do is show 5 of these rows in a table (in html), with a certain row (e.g. where id=5) in the middle and have the two rows above and below it (in the correct order). Also where level=1. This will be like a score board but only showing the user's score with the two above and two below.
So because scores can be the same, the timestamp column will also need to be used - so if two scores are equal, then the first person to get the score is shown above the other person.
E.g. say the user is id=5, I want to show
+---+-------+------+---------------------+
|id | level |score | timestamp |
+---+-------+------+---------------------+
| 4 | 1 | 70 | 2021-01-14 21:50:38 |
| 8 | 1 | 55 | 2021-01-14 09:20:00 |
| 5 | 1 | 50 | 2021-01-13 12:32:11 |
| 7 | 1 | 50 | 2021-01-14 17:15:20 |
| 2 | 1 | 45 | 2021-01-15 10:50:38 |
| 1 | 1 | 20 | 2021-01-14 13:10:12 |
+---+-------+------+---------------------+
Note that id=7 is below id=5
I am wondering does anyone know a way of doing this?
I have tried this below but it is not outputting what I need (it is outputting where level_id=2 and id=5, and the other rows are not in order)
((SELECT b.* FROM table a JOIN table b ON b.score > a.score OR (b.score = a.score AND b.timestamp < a.timestamp)
WHERE a.level_id = 1 AND a.id = 5 ORDER BY score ASC, timestamp DESC LIMIT 3)
UNION ALL
(SELECT b.* FROM table a JOIN table b ON b.score < a.score OR (b.score = a.score AND b.timestamp > a.timestamp)
WHERE a.level_id = 1 AND a.id = 5 ORDER BY score DESC, timestamp ASC LIMIT 2))
order by score
If it is easier to output all rows in the table, say where level = 1, so it is a full score board.. and then do the getting a certain row and two above and below it using PHP I'd also like to know please :) ! (possibly thinking this may keep the SQL simpler)?
You can use cte and inner join as follows:
With cte as
(select t.*,
dense_rank() over (order by score) as dr
from your_table t)
Select c.*
From cte c join cte cu on c.dr between cu.dr - 2 and cu.dr + 2
Where cu.id = 5
Ordwr by c.dr, c.timestamp
I would suggest window functions:
select t.*
from (select t.*,
max(case when id = 7 then score_rank end) over () as id_rank
from (select t.*,
dense_rank() over (order by score) as score_rank
from t
where level = 1
) t
) t
where score_rank between id_rank - 2 and id_rank + 2;
Note: This returns 5 distinct score values, which may result in more rows depending on duplicates.
Here is a db<>fiddle.
EDIT:
If you want exactly 5 rows using the timestamp, then:
select t.*
from (select t.*,
max(case when id = 7 then score_rank end) over () as id_rank
from (select t.*,
dense_rank() over (order by score, timestamp) as score_rank
from t
where level = 1
) t
) t
where score_rank between id_rank - 2 and id_rank + 2
order by score;
Note: This still treats equivalent timestamps as the same, but they seem to be unique in your data.

Delete all rows, except last 10 for each client that has related row(s) in the table in one query?

So my situation is this:
Clients table - has client data etc, not too exciting
Recently Viewed table - table that has recently viewed things for the client(s), And has structure like this:
( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY
, client_id INT NOT NULL
, cookie_user_id INT NOT NULL
, hotel_id INT NOT NULL
, added DATETIME NOT NULL
, comment TEXT
,status TINYINT NOT NULL DEFAULE 1
);
I currently have a partially working SQL to delete rows in the recently viewed table that right now globally limits number of latest remaining undeleted records in it. This is how it looks like now
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN (
SELECT id
FROM (
SELECT `id`
FROM `recently_viewed`
WHERE `client_id` IN (SELECT `id` FROM `klijenti`)
ORDER BY `id` DESC
LIMIT 5
) x
)
AND `client_id` <> 0
"LIMIT 5" part should limit to the N records to remain in recently viewed table on a "per client" basis. Right now it limits records in recently viewed table to 5 no matter how many clients actually have records there. So if I have 10 clients, each of them has 8 records in that table, I would like this query to delete as many oldest records as needed to leave only 5 newest recently viewed items for EACH client and not just leave 5 overall in the table, ignoring the "per each client" logic. Hope that makes sense to you :)
Currently, this query would be ok if I would first fetch all clients in the app and then do a foreach loop to make another query for each client and leave 5 of his latest recently viewed items, but would like to do this in one SQL query instead.
How could this be done ? Thank you
You can do it like this:
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN (
SELECT id
FROM (
SELECT t.`id`,count(*) as rnk
FROM `recently_viewed` t
INNER JOIN `recently_viewed` s
ON(t.`client_id` = s.`client_id` and t.added <= s.added)
WHERE t.`client_id` IN (SELECT `id` FROM `klijenti`)
GROUP BY t.`ID`
) x
WHERE rnk <= 5
)
AND `client_id` <> 0
You can use vartiables to in order to count the 5 more recent records per client_id:
DELETE FROM `recently_viewed`
WHERE `recently_viewed`.`id` NOT IN
(
SELECT id
FROM (
SELECT `id`,
#rn := IF(#cid = `client_id`, #rn + 1,
IF(#cid := `client_id`, 1, 1)) AS rn
FROM `recently_viewed`
CROSS JOIN (SELECT #rn := 0, #cid := 0) AS vars
WHERE `client_id` IN (SELECT `id` FROM `klijenti`)
ORDER BY `client_id`, `id` DESC) x
WHERE x.rn <= 5
)
Giorgos's answer is faster, but here's another method...
Consider the following...
SELECT * FROM my_table ORDER BY x,i;
+---+------+
| i | x |
+---+------+
| 2 | A |
| 3 | A |
| 6 | A |
| 8 | A |
| 1 | B |
| 5 | B |
| 4 | C |
| 7 | C |
| 9 | C |
+---+------+
Let's say we want to select the two latest i for each x. Here's one way to do that...
SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) <= 2;
+---+------+
| i | x |
+---+------+
| 1 | B |
| 5 | B |
| 6 | A |
| 7 | C |
| 8 | A |
| 9 | C |
+---+------+
The inverse of this set can be found as follows....
SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) > 2;
+---+------+
| i | x |
+---+------+
| 2 | A |
| 3 | A |
| 4 | C |
+---+------+
...which in turn can be incorporated in a DELETE. Here's a crude method for doing that...
DELETE a FROM my_table a
JOIN
( SELECT m.* FROM my_table m JOIN my_table n ON n.x = m.x AND n.i >= m.i GROUP BY m.i HAVING COUNT(*) > 2 ) b
ON b.i = a.i;
Query OK, 3 rows affected (0.03 sec)
SELECT * FROM my_table ORDER BY x,i;
+---+------+
| i | x |
+---+------+
| 6 | A |
| 8 | A |
| 1 | B |
| 5 | B |
| 7 | C |
| 9 | C |
+---+------+
As I say, if performance is critical, then look at a solution along the lines that Giorgos has provided.

How to get the latest data of a specific day? [MYSQL]

How can I get the latest data of a specific day in MySQL?
Let's assume that I have a column of dates recorded on my database
dates | time | value
---------------------------------------------
2015-08-05 | 11:03:02 | 200
2015-08-05 | 23:04:22 | 2400
2015-08-07 | 8:00:22 | 500
2015-08-08 | 13:00:11 | 400
2015-08-08 | 13:23:11 | 200
2015-08-09 | 17:00:23 | 2200
2015-08-09 | 17:06:00 | 1290
2015-08-09 | 19:22:00 | 900
2015-08-13 | 01:01:22 | 1010
I want to get the latest data or transaction of a specific date, my desired result would be like this
dates | time | value
---------------------------------------------
2015-08-05 | 23:04:22 | 2400
2015-08-07 | 8:00:22 | 500
2015-08-08 | 13:23:11 | 200
2015-08-09 | 19:22:00 | 900
2015-08-13 | 01:01:22 | 1010
Only the latest data of a spefic or distinct date is chosen, what is the possible query with this?
You need to do this way to get that for each dates
select t1.dates,t1.time,t1.value from table as t1 inner join
(
select dates,max(time) as time from table group by dates
) as t2 on t1.dates=t2.dates and t1.time=t2.time
Use
SELECT * FROM TABLE_NAME GROUP BY dates ORDER BY time desc LIMIT 1;
Or
SELECT * FROM TABLE_NAME GROUP BY dates HAVING time <= '23:59:59' LIMIT 1;
Try this:
SELECT `dates`, MAX(`time`) AS `time`, MAX(`value`) AS `value`
FROM `tbl_name`
GROUP BY `dates`
ORDER BY `dates` ASC
Try this query:
SELECT * FROM `table` GROUP BY dates ORDER BY time DESC;

Mysql Query SUM adding each to eachother result?

Sorry if my question makes no sense. Not sure if we can do this with mysql only. Lets say I have this query:
SELECT SUM(win) * 100 as win_profit, date, uid FROM `tips` WHERE uid = 60 AND placed = 1 GROUP by date
This would obviously get the sum of the win column each day that is in the database.
Lets say the database had:
|___win___|____date____|
| 10 | 2014-04-16 |
| 10 | 2014-04-16 |
| 10 | 2014-04-17 |
| 10 | 2014-04-18 |
| 10 | 2014-04-18 |
| 10 | 2014-04-18 |
| 10 | 2014-04-19 |
| 10 | 2014-04-19 |
| 10 | 2014-04-19 |
This would result:
20
10
30
30
How can I get it to result so each adds up, mysql query only. So the result would be:
20
30
60
90
You could get all distinct dates, and LEFT JOIN to find the sum of all values up to that date; I kept the 100 multiplier from your sample query, but you need to remove it to get a result matching your desired result.
SELECT 100 * SUM(b.win), a.date
FROM (SELECT DISTINCT date FROM `tips`) a
LEFT JOIN tips b ON a.date >= b.date
GROUP BY a.date
ORDER BY a.date
An SQLfiddle to test with.
This could be another way to do it...
SET #full_sum=0;
SELECT #full_sum+SUM(win) as win_profit, date as this_date, uid,
#full_sum:=(SELECT #full_sum+SUM(win)
FROM `testing` WHERE uid = 60
GROUP by date HAVING date=this_date)
FROM `testing` WHERE uid = 60 GROUP by date;

Categories