MySQL: fill in the gaps with last known value - mysql

I have a table containing global sales data at different reporting dates like this:
event_id | date | sales_on_date
-----------+---------------+----------
1 | 2015-01-01 | 1000
1 | 2015-02-01 | 1500
1 | 2015-03-01 | 1600
2 | 2015-01-01 | 200
2 | 2015-02-01 | 500
3 | 2015-01-01 | 100
And I need to write a SELECT statement to output the sales (or the last known sales) for every date, like this:
event_id | date | sales_on_date
-----------+---------------+----------
1 | 2015-01-01 | 1000
1 | 2015-02-01 | 1500
1 | 2015-03-01 | 1600
2 | 2015-01-01 | 200
2 | 2015-02-01 | 500
2 | 2015-03-01 | 500
3 | 2015-01-01 | 100
3 | 2015-02-01 | 100
3 | 2015-03-01 | 100
Currently, I have to make a SQL request to fetch all data from my table, and then use a PHP routine to fill in the gaps, but it seems a 'pure' SQL solution would be much more convenient and elegant.
Any idea how to do this in SQL?

This is a bit complicated, because the query has two parts. The first is generating all the rows. The second is filling them in. The first is a CROSS JOIN. The second a correlated subquery:
select e.event_id, d.date,
(select t.sales_on_date
from t
where t.event_id = e.event_id and t.date <= d.date
order by t.date desc
limit 1
) as sales_on_date
from (select distinct event_id from t) e cross join
(select distinct date from t) d
order by e.event_id, d.date;

Related

SQL query to join same table with SUM up until each returned row

I came across a task where I have to return the total COUNT and SUM of issued policies for each day of the month and compare it to the previous year.
Table PolicyOrder has fields:
PolicyOrderId - primary key
CreatedAt (DATETIME)
CalculatedPremium - cost of policy or "premium"
PolicyOrderStatusId - irrelevant to the question but still - status of the policy.
To solve this I came up with a query that inner joins self table and sums/counts by grouping according to DAY of the creation date.
SELECT
DATE(po1.CreatedAt) AS dayDate_2017,
SUM(po1.CalculatedPremium) AS premiumSum_2017,
COUNT(po1.PolicyOrderId) AS policyCount_2017,
po2.*
FROM
PolicyOrder po1
INNER JOIN (
SELECT
DATE(CreatedAt) AS dayDate_2018,
SUM(CalculatedPremium) AS premiumSum_2018,
COUNT(PolicyOrderId) AS policyCount_2018
FROM
PolicyOrder po2
WHERE
YEAR(CreatedAt) = 2018 AND
MONTH(CreatedAt) = 10 AND
PolicyOrderStatusId = 6
GROUP BY
DAY(CreatedAt)
) po2 ON (
DAY(po2.dayDate_2018) = DAY(po1.CreatedAt)
)
WHERE
YEAR(po1.CreatedAt) = 2017 AND
MONTH(po1.CreatedAt) = 10 AND
PolicyOrderStatusId = 6
GROUP BY
DAY(po1.CreatedAt)
The above query returns these results:
dayDate_2017 | premiumSum_2017 | policyCount_2017 | dayDate_2018 | premiumSum_2018 | policyCount_2018
2017-10-01 | 4699.36 | 98 | 2018-10-01 | 8524.21 | 144
2017-10-02 | 9114.55 | 168 | 2018-10-02 | 7942.25 | 140
2017-10-03 | 9512.43 | 178 | 2018-10-03 | 9399.61 | 161
2017-10-04 | 9291.77 | 155 | 2018-10-04 | 6922.83 | 137
2017-10-05 | 8063.27 | 155 | 2018-10-05 | 9278.58 | 178
2017-10-06 | 9743.40 | 184 | 2018-10-06 | 6139.38 | 136
...
2017-10-31 | ...
The problem is that now I have to add two more columns in which policies has to be counted and amounts added from the start of the year UP UNTIL each returned row.
Desired results:
dayDate_2017 | premiumSum_2017 | policyCount_2017 | sumFromYearBegining | countFromYearBegining
2017-10-01 | 4699.36 | 98 | 150000.34 | 5332
2017-10-02 | 9114.55 | 168 | 156230.55 | 5443
2017-10-03 | 9512.43 | 178 | 160232.44 | 5663
...
2017-10-31 | ...
WHERE:
sumFromYearBegining (150000.34) - SUM of premiumSum from 2017-01-01 until 2017-10-01 (excluding)
countFromYearBegining (5332) - COUNT of policies from 2017-01-01 until 2017-10-01 (excluding)
sumFromYearBegining (1566239.55) - SUM of premiumSum from 2017-01-01 until 2017-10-02 (excluding)
countFromYearBegining (5443) - COUNT of policies from 2017-01-01 until 2017-10-02 (excluding)
sumFromYearBegining (160232.44) - SUM of premiumSum from 2017-01-01 until 2017-10-02 (excluding)
countFromYearBegining (5663) - COUNT of policies from 2017-01-01 until 2017-10-02 (excluding)
I have tried inner joining same table COUNTed and SUMed which failed because I cannot specify the range up to which I need to count and sum, I have tried LEFT joining and then counting, which fails because the results are counted not untill each row result but until the last result etc...
DB Fiddle: https://www.db-fiddle.com/f/ckM8HyTD6NjLbK41Mq1gct/5
Any help from you SQL ninjas highly appreciated.
We can use User-defined variables to calculate Rolling Sum / Count, in absence of Window Functions' availability.
We will first need to determine the Sum and Count for every day in the year 2017 (even though you need rows for a particular month only). Because, in order to calculate rolling Sum for the days in March month, we would need the sum/count values from the January, and February month(s) as well. One optimization possibility is that we can restrict calculations from the first month to the require month only.
Note that ORDER BY daydate_2017 is necessary in order to be able to calculate rolling sum correctly. By default, data is in unordered fashion. Without defining the order, we cannot guarantee that Sum will be correct.
Also, we need to two levels of sub-select queries. First level is used to calculate the Rolling sum values. Second level is used to restrict the result to February month only. Since WHERE is executed before SELECT; we cannot restrict the result to February month, in the first level itself.
If you need similar rolling Sum for the year 2018 as well; similar query logic can be implemented in other set of sub-select queries.
SELECT dt2_2017.*, dt_2018.*
FROM
(
SELECT dt_2017.*,
#totsum := #totsum + dt_2017.premiumsum_2017 AS sumFromYearBegining_2017,
#totcount := #totcount + dt_2017.policycount_2017 AS countFromYearBeginning_2017
FROM (SELECT Date(po1.createdat) AS dayDate_2017,
Sum(po1.calculatedpremium) AS premiumSum_2017,
Count(po1.policyorderid) AS policyCount_2017
FROM PolicyOrder AS po1
WHERE po1.policyorderstatusid = 6 AND
YEAR(po1.createdat) = 2017 AND
MONTH(po1.createdat) <= 2 -- calculate upto February for 2017
GROUP BY daydate_2017
ORDER BY daydate_2017) AS dt_2017
CROSS JOIN (SELECT #totsum := 0, #totcount := 0) AS user_init_vars
) AS dt2_2017
INNER JOIN (
SELECT
DATE(po2.CreatedAt) AS dayDate_2018,
SUM(po2.CalculatedPremium) AS premiumSum_2018,
COUNT(po2.PolicyOrderId) AS policyCount_2018
FROM
PolicyOrder po2
WHERE
YEAR(po2.CreatedAt) = 2018 AND
MONTH(po2.CreatedAt) = 2 AND
po2.PolicyOrderStatusId = 6
GROUP BY
dayDate_2018
) dt_2018 ON DAY(dt_2018.dayDate_2018) = DAY(dt2_2017.dayDate_2017)
WHERE YEAR(dt2_2017.daydate_2017) = 2017 AND
MONTH(dt2_2017.daydate_2017) = 2;
RESULT: View on DB Fiddle
| dayDate_2017 | premiumSum_2017 | policyCount_2017 | sumFromYearBegining_2017 | countFromYearBeginning_2017 | dayDate_2018 | premiumSum_2018 | policyCount_2018 |
| ------------ | --------------- | ---------------- | ------------------------ | --------------------------- | ------------ | --------------- | ---------------- |
| 2017-02-01 | 4131.16 | 131 | 118346.77 | 3627 | 2018-02-01 | 8323.91 | 149 |
| 2017-02-02 | 2712.74 | 85 | 121059.51000000001 | 3712 | 2018-02-02 | 9469.33 | 153 |
| 2017-02-03 | 3888.59 | 111 | 124948.1 | 3823 | 2018-02-03 | 6409.21 | 97 |
| 2017-02-04 | 2447.99 | 74 | 127396.09000000001 | 3897 | 2018-02-04 | 5693.69 | 120 |
| 2017-02-05 | 1437.5 | 45 | 128833.59000000001 | 3942 | 2018-02-05 | 8574.97 | 129 |
| 2017-02-06 | 4254.48 | 127 | 133088.07 | 4069 | 2018-02-06 | 8277.51 | 133 |
| 2017-02-07 | 4746.49 | 136 | 137834.56 | 4205 | 2018-02-07 | 9853.75 | 173 |
| 2017-02-08 | 3898.05 | 125 | 141732.61 | 4330 | 2018-02-08 | 9116.33 | 144 |
| 2017-02-09 | 8306.86 | 286 | 150039.46999999997 | 4616 | 2018-02-09 | 8818.32 | 166 |
| 2017-02-10 | 6740.99 | 204 | 156780.45999999996 | 4820 | 2018-02-10 | 7880.17 | 134 |
| 2017-02-11 | 4290.38 | 133 | 161070.83999999997 | 4953 | 2018-02-11 | 8394.15 | 180 |
| 2017-02-12 | 3687.58 | 122 | 164758.41999999995 | 5075 | 2018-02-12 | 10378.29 | 171 |
| 2017-02-13 | 4939.31 | 159 | 169697.72999999995 | 5234 | 2018-02-13 | 9383.15 | 160 |
If you want a way that avoids using #variables in the select list, and also avoids analytics (only mysql 8 supports them) you can do it with a semi-cartesian product:
WITH prevYr AS(
SELECT
YEAR(CreatedAt) AS year_prev,
MONTH(CreatedAt) AS month_prev,
DAY(CreatedAt) AS day_prev,
SUM(CalculatedPremium) AS premiumSum_prev,
COUNT(PolicyOrderId) AS policyCount_prev
FROM
PolicyOrder
WHERE
CreatedAt BETWEEN '2017-02-01' AND '2017-02-28' AND
PolicyOrderStatusId = 6
GROUP BY
YEAR(CreatedAt), MONTH(CreatedAt), DAY(CreatedAt)
),
currYr AS (
SELECT
YEAR(CreatedAt) AS year_curr,
MONTH(CreatedAt) AS month_curr,
DAY(CreatedAt) AS day_curr,
SUM(CalculatedPremium) AS premiumSum_curr,
COUNT(PolicyOrderId) AS policyCount_curr
FROM
PolicyOrder
WHERE
CreatedAt BETWEEN '2018-02-01' AND '2018-02-28' AND
PolicyOrderStatusId = 6
GROUP BY
YEAR(CreatedAt), MONTH(CreatedAt), DAY(CreatedAt)
)
SELECT
*
FROM
prevYr
INNER JOIN
currYr
ON
currYr.day_curr = prevYr.day_prev
INNER JOIN
(
SELECT
main.day_prev AS dayRolling_prev,
SUM(pre.premiumSum_prev) AS premiumSumRolling_prev,
SUM(pre.policyCount_prev) AS policyCountRolling_prev
FROM
prevYr main LEFT OUTER JOIN prevYr pre ON pre.day_prev < main.day_prev
GROUP BY
main.day_prev
) rollingPrev
ON
currYr.day_curr = rollingPrev.dayRolling_prev
ORDER BY 1,2,3
We summarise the year 2017 and year 2018 data into two CTEs because it makes things a lot cleaner and neater later, particularly for this rolling count. You can probably follow the logic of the CTE easily because it's lifted more or less straight from your query - I only dropped the DATE column in favour of a year/month/date triplet because it made other things cleaner (joins) and can be recombined to a date if needed. I also swapped the WHERE clauses to use date BETWEEN x AND y because this will leverage an index on a column whereas using YEAR(date) = x AND MONTH(date) = y might not
The rolling counts works via something I referred to as a semi-cartesian. It's actually a cartesian product; any database join that results in rows from one o both tables multiplying and being represented repeatedly in the output, is a cartesian product. Rather than being a full product (every row crossed with every other row) in this case it uses a less than, so every row is only crossed with a subset of rows. As the date increases, more rows match the predicate, because a date of 30th has 29 rows that are less than it.
This thus causes the following pattern of data:
maindate predate maincount precount
2017-02-01 NULL 10 NULL
2017-02-02 2017-02-01 20 10
2017-02-03 2017-02-01 30 10
2017-02-03 2017-02-02 30 20
2017-02-04 2017-02-01 40 10
2017-02-04 2017-02-02 40 20
2017-02-04 2017-02-03 40 30
You can see that for any given main date, it repeats N - 1 times because there are N - 1 dates lower than in that satisfy the join condition predate < maindate
If we group by the maindate and sum the counts associated with each predate, we get the rolling sum of all the pre-counts on that main-date (So, on the 4th day of the month, it's SUM(pre count for dates 1st - 3rd, i.e. 10+20+30 = 60. On the 5th day, we sum the counts for days 1 to 4. On the 6th day, we sum days 1 to 5 etc)

Select max value from joined table

i need help with a mysql query. My tables:
objects
+---------+--------+
| id | name |
+---------+--------+
| 1 | house 1|
| 2 | house 2|
| 3 | house 3|
+---------+--------+
objects_expire
+----------+-----------+
| object_id| expire |
+----------+-----------+
| 1 | 2014-09-11|
| 1 | 2015-09-11|
| 2 | 2014-09-11|
| 2 | 2015-09-11|
| 2 | 2016-09-11|
| 3 | 2013-09-11|
| 3 | 2014-09-11|
| 3 | 2015-09-15|
+----------+-----------+
Now i need objects where max 'expire' is bigger then 2015-09-04 and smaller then 2015-09-18 (+/- 7 days)
Like this result:
+----------+-----------+-----------+
| object_id| expire | name |
+----------+-----------+-----------+
| 1 | 2015-09-11| house 1 |
| 3 | 2015-09-15| house 3 |
+----------+-----------+-----------+
This is what i have now:
SELECT o.id, MAX(uio.expire) AS object_expires
FROM objects AS o
LEFT JOIN objects_expire AS oe ON oe.object_id = o.id
WHERE expire < '2015-09-18'
AND expires > '2015-09-04'
GROUP BY o.id
But thats not correct.
Thanks for any help!!!
One usual approach is to do the grouping first and then join back, also if you do not want to hardcode the dates you can always use date_sub and date_add function to get -/+ 7 days from the current date.
select
o.id,
e.mexpire as expire,
o.name
from objects o
join(
select object_id,max(expire) as mexpire
from objects_expire
group by object_id
having mexpire > date_sub(curdate(),interval 7 day) and mexpire < date_add(curdate(),interval 7 day)
)e
on o.id = e.object_id
You need to group, and to use HAVING as a filter for the grouped column
select object_id, max(expire) as expire, name
from objects_expire
left join objects on objects_expire.object_id=objects.id
group by object_id, name
having max(expire) < '2015-09-17'
and max(expire) > '2015-09-03'

Getting unique record group with more than one rows

I have the following table in MySQL:
Table Scores:
id integer
score integer
scoredate date
composite (id,scoredate) is unique
id score scoredate
--+----+------
1 | 2 | 2015-01-01
1 | 2 | 2015-01-02
1 | 3 | 2015-01-03
1 | 2 | 2015-01-04
2 | 3 | 2015-01-01
2 | 3 | 2015-01-02
2 | 3 | 2015-01-03
2 | 3 | 2015-01-04
3 | 1 | 2015-01-01
3 | 1 | 2015-01-02
3 | 1 | 2015-01-03
3 | 1 | 2015-01-04
4 | 3 | 2015-01-01
4 | 3 | 2015-01-02
4 | 3 | 2015-01-03
4 | 1 | 2015-01-04
I want to identify 'ids' whose scores changed at least once.
id score
--+----
1 | 2
1 | 3
4 | 3
4 | 1
I am able to do this using a complex subquery but I was wondering if there was a way to do it using a simpler query.
For example the with following query I get the id's that we care about. I could do another subquery or join with this the output of this to get the result above but I was hoping to build a simpler query. The following is already effectively 2 passes (2 queries) and doing another join or subquery with this means a 3rd pass or query to be processed by the engine:
SELECT `id` FROM (SELECT `id`, `score` FROM `scores` GROUP BY `id`, `score`) AS sq GROUP BY `id` HAVING count(1) > 1
I am hoping to get to the result in 1-2 passes/ queries.
SELECT distinct id, score
FROM Scores
where id in (select id from Scores group by id having count(distinct score) > 1);
This would give you the result you wanted.
SELECT DISTINCT x.*
FROM my_table x
JOIN my_table y
ON y.id = x.id
AND y.date > x.date
AND y.score <> x.score;

Dynamic minimum value for specfic range (mysql)

I am looking for a solution to SELECT (or otherwise derive) the values for Column C (minimum price for last 3 days only, not for the whole column).
----------------------------------------
Date | Unit_ | Low_3_days |
| price | |
----------------------------------------
2015-01-01 | 15 | should be: 15 |
2015-01-02 | 17 | should be: 15 |
2015-01-03 | 21 | should be: 15 |
2015-01-04 | 18 | should be: 17 |
2015-01-05 | 12 | should be: 12 |
2015-01-06 | 14 | should be: 12 |
2015-01-07 | 16 | should be: 12 |
----------------------------------------
My thought revolves around the following, but yielding an error:
select S.Date,Unit_price,
(SELECT min(LOW_3_days)
FROM table
where S.DATE BETWEEN S.DATE-1
and S.DATE-3)
AS min_price_3_days
FROM table AS S
What is the correct query to get this to work? Database used MySQL.
You are pretty close. When working with correlated subqueries, always use table aliases to be absolutely clear about where the columns are coming from:
select S.Date, Unit_price,
(SELECT min(s2.Unit_Price)
FROM table s2
WHERE s2.DATE BETWEEN s.DATE - interval 3 day and
s.DATE - interval 1 day
) as min_price_3_days
FROM table S;

MySQL - query help select results with latest timestamp grouped by date

I have a table containing the following fields:
date, time, node, result
describing some numeric result for different nodes at different dates and times throughout each day. Typical listing will look something like this:
date | time | node | result
----------------------------------
2011-03-01 | 10:02 | A | 10
2011-03-01 | 11:02 | A | 20
2011-03-02 | 03:13 | A | 23
2011-03-02 | 12:15 | A | 18
2011-03-02 | 13:15 | A | 8
2011-03-01 | 13:12 | B | 2
2011-03-01 | 14:26 | B | 1
2011-03-02 | 08:00 | B | 6
2011-03-02 | 07:22 | B | 3
2011-03-02 | 21:19 | B | 4
I want to form a query that'll get the last result from each day for each node, such that I'd get something like this:
date | time | node | latest
-----------------------------------
2011-03-01 | 11:02 | A | 20
2011-03-01 | 14:26 | B | 1
2011-03-02 | 13:15 | A | 8
2011-03-02 | 21:19 | B | 4
I thought about doing a group by date, node, but then extracting the last value was a mess (I used group_concat( result order by time ) and used SUBSTRING() to get the last value. Baah, I know). Is there a simple way to do this in mysql?
I'm pretty sure I saw a similar request solving it very nice without using an INNER JOIN but I can't find it right now (and it might have been SQL Server) but following should work nevertheless.
SELECT n.*
FROM Nodes n
INNER JOIN (
SELECT MAX(time) AS Time
, Date
, Node
FROM Nodes
GROUP BY
Date
, Node
) nm ON nm.time = n.time
AND nm.Date = n.Date
AND nm.Node = n.Node
I would think that you would have to use something like the Max() function. Sorry I don't have mysql, so I can't test but I would think something like this
select t.date, t.node, t.latest, Max(time) from Table t Group By t.node, t.date
I think the aggregate function will return only the one row per grouping.