While preparing for an interview, I have come across an SQL question and I hope to get some insight as to how to better answer it.
Given timestamps, userid, how to determine the number of users who are active everyday in a week?
There's very little to it, but that's the question in front of me.
I'm going to demonstrate such an idea based on what makes most sense to me and the way I would reply if the question was presented same as here:
First, let's assume a data set as such, we will name the table logins:
+---------+---------------------+
| user_id | login_timestamp |
+---------+---------------------+
| 1 | 2015-09-29 14:05:05 |
| 2 | 2015-09-29 14:05:08 |
| 1 | 2015-09-29 14:05:12 |
| 4 | 2015-09-22 14:05:18 |
| ... | ... |
+---------+---------------------+
There may be other columns, but we don't mind those.
First of all we should determine the borders of that week, for that we can use ADDDATE(). Combined with the idea that today's date-today's week-day (MySQL's DAYOFWEEK()), is sunday's date.
For instance: If today is Wednesday the 10th, Wed - 3 = Sun, thus 10 - 3 = 7, and we can expect Sunday to be the 7th.
We can get WeekStart and WeekEnd timestamps this way:
SELECT
DATE_FORMAT(ADDDATE(CURDATE(), INTERVAL 1-DAYOFWEEK(CURDATE()) DAY), "%Y-%m-%d 00:00:00") WeekStart,
DATE_FORMAT(ADDDATE(CURDATE(), INTERVAL 7-DAYOFWEEK(CURDATE()) DAY), "%Y-%m-%d 23:59:59") WeekEnd;
Note: in PostgreSQL there's a DATE_TRUNC() function which returns the beginning of a specified time unit, given a date, such as week start, month, hour, and so on. But that's not available in MySQL.
Next, let's utilize WeekStart and weekEnd in order to clice our data set, in this example I'll just show how to filter, using hard coded dates:
SELECT *
FROM `logins`
WHERE login_timestamp BETWEEN '2015-09-29 14:05:07' AND '2015-09-29 14:05:13'
This should return our data set sliced, with only relevant results:
+---------+---------------------+
| user_id | login_timestamp |
+---------+---------------------+
| 2 | 2015-09-29 14:05:08 |
| 1 | 2015-09-29 14:05:12 |
+---------+---------------------+
We can then reduce our result set to only the user_ids, and filter out duplicates. then count, this way:
SELECT COUNT(DISTINCT user_id)
FROM `logins`
WHERE login_timestamp BETWEEN '2015-09-29 14:05:07' AND '2015-09-29 14:05:13'
DISTINCT will filter out duplicates, and count will return just the amount.
Combined, this becomes:
SELECT COUNT(DISTINCT user_id)
FROM `logins`
WHERE login_timestamp
BETWEEN DATE_FORMAT(ADDDATE(CURDATE(), INTERVAL 1- DAYOFWEEK(CURDATE()) DAY), "%Y-%m-%d 00:00:00")
AND DATE_FORMAT(ADDDATE(CURDATE(), INTERVAL 7- DAYOFWEEK(CURDATE()) DAY), "%Y-%m-%d 23:59:59")
Replace CURDATE() with any timestamp in order to get that week's user login count.
But I need to break this down to days, I hear you cry. Of course! and this is how:
First, let's translate our over-informative timestamps to just the date data. We add DISTINCT because we don't mind the same user logging in twice the same day. we count users, not logins, right? (note we step back here):
SELECT DISTINCT user_id, DATE_FORMAT(login_timestamp, "%Y-%m-%d")
FROM `logins`
This yields:
+---------+-----------------+
| user_id | login_timestamp |
+---------+-----------------+
| 1 | 2015-09-29 |
| 2 | 2015-09-29 |
| 4 | 2015-09-22 |
| ... | ... |
+---------+-----------------+
This query, we will wrap with a second, in order to count appearances of every date:
SELECT `login_timestamp`, count(*) AS 'count'
FROM (SELECT DISTINCT user_id, DATE_FORMAT(login_timestamp, "%Y-%m-%d") AS `login_timestamp` FROM `logins`) `loginsMod`
GROUP BY `login_timestamp`
We use count and a grouping in order to get the list by date, which returns:
+-----------------+-------+
| login_timestamp | count |
+-----------------+-------+
| 2015-09-29 | 1 +
| 2015-09-22 | 2 +
+-----------------+-------+
And after all the hard work, both combined:
SELECT `login_timestamp`, COUNT(*)
FROM (
SELECT DISTINCT user_id, DATE_FORMAT(login_timestamp, "%Y-%m-%d") AS `login_timestamp`
FROM `logins`
WHERE login_timestamp BETWEEN DATE_FORMAT(ADDDATE(CURDATE(), INTERVAL 1- DAYOFWEEK(CURDATE()) DAY), "%Y-%m-%d 00:00:00") AND DATE_FORMAT(ADDDATE(CURDATE(), INTERVAL 7- DAYOFWEEK(CURDATE()) DAY), "%Y-%m-%d 23:59:59")) `loginsMod`
GROUP BY `login_timestamp`;
Will give you a daily breakdown of logins per-day in this week. Again, replace CURDATE() to get a different week.
As for the users themselves who logged in, let's combine the same stuff in a different order:
SELECT `user_id`
FROM (
SELECT `user_id`, COUNT(*) AS `login_count`
FROM (
SELECT DISTINCT `user_id`, DATE_FORMAT(`login_timestamp`, "%Y-%m-%d")
FROM `logins`) `logins`
GROUP BY `user_id`) `logincounts`
WHERE `login_count` > 6
I have two inner queries, the first is logins:
SELECT DISTINCT `user_id`, DATE_FORMAT(`login_timestamp`, "%Y-%m-%d")
FROM `logins`
Will provide the list of users, and the days when they logged in on, without duplicates.
Then we have logincounts:
SELECT `user_id`, COUNT(*) AS `login_count`
FROM `logins` -- See previous subquery.
GROUP BY `user_id`) `logincounts`
Will return the same list, with a count of how many logins each user had.
And lastly:
SELECT user_id
FROM logincounts -- See previous subquery.
WHERE login_count > 6
Filtering our those who didn't login 7 times, and dropping the date column.
This kinda got long, but I think it's rife with ideas and I think it may definitely help answering in an interesting way in a work interview. :)
create table fbuser(id integer, date date);
insert into fbuser(id,date)values(1,'2012-01-01');
insert into fbuser(id,date)values(1,'2012-01-02');
insert into fbuser(id,date)values(1,'2012-01-01');
insert into fbuser(id,date)values(1,'2012-01-01');
insert into fbuser(id,date)values(1,'2012-01-01');
insert into fbuser(id,date)values(1,'2012-01-01');
insert into fbuser(id,date)values(1,'2012-01-02');
insert into fbuser(id,date)values(1,'2012-01-03');
insert into fbuser(id,date)values(1,'2012-01-04');
insert into fbuser(id,date)values(1,'2012-01-05');
insert into fbuser(id,date)values(1,'2012-01-06');
insert into fbuser(id,date)values(1,'2012-01-07');
insert into fbuser(id,date)values(4,'2012-01-08');
insert into fbuser(id,date)values(4,'2012-01-08');
insert into fbuser(id,date)values(1,'2012-01-08');
insert into fbuser(id,date)values(1,'2012-01-09');
select * from fbuser;
id | date
----+------------
1 | 2012-01-01
1 | 2012-01-02
1 | 2012-01-01
1 | 2012-01-01
1 | 2012-01-01
1 | 2012-01-01
1 | 2012-01-02
1 | 2012-01-03
1 | 2012-01-04
1 | 2012-01-05
1 | 2012-01-06
1 | 2012-01-07
2 | 2012-01-07
3 | 2012-01-07
4 | 2012-01-07
4 | 2012-01-08
4 | 2012-01-08
1 | 2012-01-08
1 | 2012-01-09
select id,count(DISTINCT date) from fbuser
where date BETWEEN '2012-01-01' and '2012-01-07'
group by id having count(DISTINCT date)=7
id | count
----+-------
1 | 7
(1 row)
Query counts unique dates logged in by user for the given period and returns id with 7 occurrences. If you have time also in your date you can use date_format.
With given data of: userid and timestamp; How does one calculate the number of "active users" on each day in a week?
The problem of course is that there might be no logins at all, or none on certain days in a week, so the basic solution to such a requirement is that you must have a series of dates to compare the logins against.
There are a wide variety of ways to generate the dates of a week and the method one chooses would depend on 2 main factors:
How often do I need these (or similar) results?
the platform I am using. (For example it is very easy to "generate a series" using Postgres but MySQL does not offer such a feature whereas recently MariaDB has introduced series tables to help solve such needs. So knowing your platform's capabilities will affect how you solve this.)
IF I need to do this regularly (which I assume will be true) then I would create a "calendar table" of one row per day for a reasonable extensive period (say 10 years) which is only approx 3652 rows, with its primary key as the date column. In this table we can also store the "week_number" using the week() function which makes week by week reporting simpler (and we can add other columns in this table as well).
So, assuming I have built the calendar table containing each date and a week number then we can take the week number from today's date, subtract 1, and gather the needed login data like this:
select
c.caldate, count(distinct l.userid) as user_logins
from calendar_table as c
left join login_table l on l.timestamp >= c.caldate and l.timestamp < date_add(c.caldate,INTERVAL 1 DAY)
where c.week_number = WEEK(curdate())-1
group by c.caldate
How did I create the calendar table?
Well as said earlier there are a variety of methods, and for MySQL there are options available here: How to populate a table with a range of dates?
I tried this in Teradata and here is the SQL. First, get the User unique to a date, then check, if the user is present for 7 days.
SELECT src.USER_ID
,COUNT(*) CNT
FROM (SELECT USER_ID
,CAST(LOGIN_TIMESTAMP AS DATE FORMAT 'YYYY-MM-DD') AS LOGIN_DT
FROM src_table
WHERE LOGIN_TIMESTAMP BETWEEN '2017-11-12 00:00:00' AND '2017-11-18 23:59:59'
GROUP BY 1,2
)src GROUP BY 1 HAVING CNT = 7;
INSERT INTO src_table VALUES (1,'2017-11-12 10:10:10');
INSERT INTO src_table VALUES (1,'2017-11-13 10:10:10');
INSERT INTO src_table VALUES (1,'2017-11-13 11:10:10');
INSERT INTO src_table VALUES (1,'2017-11-13 12:10:10');
INSERT INTO src_table VALUES (1,'2017-11-14 10:10:10');
INSERT INTO src_table VALUES (1,'2017-11-15 10:10:10');
INSERT INTO src_table VALUES (1,'2017-11-16 10:10:10');
INSERT INTO src_table VALUES (1,'2017-11-17 10:10:10');
INSERT INTO src_table VALUES (1,'2017-11-18 10:10:10');
INSERT INTO src_table VALUES (2,'2017-11-12 01:10:10');
INSERT INTO src_table VALUES (2,'2017-11-13 13:10:10');
INSERT INTO src_table VALUES (2,'2017-11-14 14:10:10');
INSERT INTO src_table VALUES (2,'2017-11-15 12:10:10');
INSERT INTO src_table VALUES (5,'2017-11-12 01:10:10');
INSERT INTO src_table VALUES (5,'2017-11-13 02:10:10');
INSERT INTO src_table VALUES (5,'2017-11-14 03:10:10');
INSERT INTO src_table VALUES (5,'2017-11-15 04:10:10');
INSERT INTO src_table VALUES (5,'2017-11-16 05:10:10');
INSERT INTO src_table VALUES (5,'2017-11-17 06:10:10');
INSERT INTO src_table VALUES (8,'2017-11-12 04:10:10');
INSERT INTO src_table VALUES (8,'2017-11-13 05:10:10');
INSERT INTO src_table VALUES (8,'2017-11-14 06:10:10');
INSERT INTO src_table VALUES (8,'2017-11-15 01:10:10');
INSERT INTO src_table VALUES (8,'2017-11-16 02:10:10');
INSERT INTO src_table VALUES (8,'2017-11-17 03:10:10');
INSERT INTO src_table VALUES (8,'2017-11-18 03:10:10');
This works for me
select a.user_id, count(a.user_id) as active_time_in_days
from
(
select user_id, login_time, lead(login_time) over (partition by user_id order by login_time asc ) as next_day
from dev.login_info
group by 1,2
order by user_id, login_time asc
)a where a.login_time + interval '1 day' = next_day
group by 1;
How about this? I tried it and it works.
select yearweek(ts) as yearwk, user_id,
count(user_id) as counts
from log
group by 1,2
having count(user_id) =7;
Related
I am creating a vehicle rental application. I was trying find overlapping booking in given dates. I come across a similar question Count maximum number of overlapping date ranges in MySQL but this only answered for MySQL 8.0.
I modified above question for my problem.
I need solution for MySQL 5.6 without window function.
create table if not exists BOOKING
(
start datetime null,
end datetime null,
vehicle_id varchar(255),
id int auto_increment
primary key
);
INSERT INTO BOOKING (start, end, vehicle_id)
VALUES
('2020-02-06 10:33:55', '2020-02-07 10:34:41', 111),
('2020-02-08 10:33:14', '2020-02-10 10:33:57', 111),
('2020-02-06 10:32:55', '2020-02-07 10:33:32', 222),
('2020-08-06 10:33:03', '2020-02-11 10:33:12', 111),
('2020-02-12 10:31:38', '2020-02-15 10:32:41', 111),
('2020-02-09 09:48:44', '2020-02-10 09:50:37', 222);
Suppose If I give start as 2020-02-05 and end as 2020-02-11, this should return 2, as maximum usage of vehicle 111, is 2 from 2020-02-06 to 2020-02-10
5 6 7 8 9 10 11
<--> <------>
<----------------> (Vehicle Id 111, ANSWER should be 2)
for vehicle id 222, (For same query)
5 6 7 8 9 10 11
<--> <---> (Vehicle Id 222, ANSWER should be 1)
So overall output I am expecting for input start(2020-02-05) and end(2020-02-11)
+---------+-------+
| vehicle | usage |
+---------+-------+
| 111 | 2 |
| 222 | 1 |
+---------+-------+
I need solution which covers followings
on passing start_date and end_date my query will return data only for that range
If no data found should return vehicle_id 0
The maximum number of overlaps occurs when a rental starts (although it might persist for a period of time, this is all you care about).
You can calculate this for each start using:
SELECT b.vehicle_id, b.start, COUNT(*)
FROM booking b JOIN
booking b2
ON b2.vehicle_id = b.vehicle_id AND
b2.start <= b.start AND
b2.end > b.start
WHERE b.start <= $end and b.end >= $start
GROUP BY b.vehicle_id, b.start;
Then for the maximum:
SELECT vehicle_id, MAX(overlaps)
FROM (SELECT b.vehicle_id, b.start, COUNT(*) as overlaps
FROM booking b JOIN
booking b2
ON b2.vehicle_id = b.vehicle_id AND
b2.start <= b.start AND b2.end > b.start
GROUP BY b.vehicle_id, b.start
) b
GROUP BY vehicle_id;
Here is a db<>fiddle.
Performance on this type of query is never going to be as good as using window functions. However, an index on (vehicle_id, start, end) would help.
SELECT vehicle_id vehicle, MAX(cnt) `usage`
FROM ( SELECT booking.vehicle_id, timepoints.dt, COUNT(*) cnt
FROM booking
JOIN ( SELECT start dt FROM booking
UNION ALL
SELECT `end` FROM booking ) timepoints ON timepoints.dt BETWEEN booking.start AND booking.`end`
GROUP BY booking.vehicle_id, timepoints.dt ) subquery
GROUP BY vehicle_id;
fiddle
PS. Misprint in 4th row is corrected.
This seemed pretty straight forward initially, but has proved to be a real headache. Below is my table, data, expected output and SQL Fiddle of where I have got to in solving my problem.
Schema & Data:
CREATE TABLE IF NOT EXISTS `meetings` (
`id` int(6) unsigned NOT NULL,
`user_id` int(6) NOT NULL,
`start_time` DATETIME,
`end_time` DATETIME,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8;
INSERT INTO `meetings` (`id`, `user_id`, `start_time`, `end_time`) VALUES
('0', '1', '2018-05-09 04:30:00', '2018-05-09 17:30:00'),
('1', '1', '2018-05-10 06:30:00', '2018-05-10 17:30:00'),
('2', '1', '2018-05-10 12:30:00', '2018-05-10 16:00:00'),
('3', '1', '2018-05-11 17:00:00', '2018-05-12 11:00:00'),
('4', '2', '2018-05-11 07:00:00', '2018-05-12 11:00:00'),
('5', '2', '2018-05-11 04:30:00', '2018-05-11 15:00:00');
What I would like to get from the above is total time worked outside of 09:00 to 17:00, grouped by day and user_id. So the result from the above data would look like:
date | user_id | overtime_hours
---------------------------------------
2018-05-09 | 1 | 05:00:00
2018-05-10 | 1 | 03:00:00
2018-05-11 | 1 | 07:00:00
2018-05-12 | 1 | 09:00:00
2018-05-11 | 2 | 13:30:00
2018-05-12 | 2 | 09:00:00
As you can see the expected results are only summing overtime for each day and user for those hours outside of 9 to 5.
Below is the query and SQL Fiddle of where I am. The main issue comes when the start and ends straddle midnight (or multiple midnight's)
SELECT
SEC_TO_TIME(SUM(TIME_TO_SEC(TIME(end_time)) - TIME_TO_SEC(TIME(start_time)))), user_id, DATE(start_time)
FROM
(SELECT
start_time, CASE WHEN TIME(end_time) > '09:00:00' THEN DATE_ADD(DATE(end_time), INTERVAL 9 HOUR) ELSE end_time END AS end_time, user_id
FROM
meetings
WHERE
TIME(start_time) < '09:00:00'
UNION
SELECT
CASE WHEN TIME(start_time) < '17:00:00' THEN DATE_ADD(DATE(start_time), INTERVAL 17 HOUR) ELSE start_time END AS start_time, end_time, user_id
FROM
meetings
WHERE
TIME(end_time) > '17:00:00') AS clamped_times
GROUP BY user_id, DATE(start_time)
http://sqlfiddle.com/#!9/77bc85/1
Pastebin for when the fiddle decides to flake: https://pastebin.com/1YvLaKbT
As you can see the query grabs the easy overtime with start and ends on the same day, but does not work with the multiple day ones.
If the meeting is going to span across n days, and you are looking to compute "work hours" daywise within a particular meeting; it rings a bell, that we can use a number generator table.
(SELECT 0 AS gap UNION ALL SELECT 1 UNION ALL SELECT 2) AS ngen
We will use the number generator table to consider separate rows for the individual dates ranging from the start_time to end_time. For this case, I have assumed that it is unlikely that meeting will span across more than 2 days. If it happens to span more number of days, you can easily extend the range by adding more UNION ALL SELECT 3 .. to the ngen Derived Table.
Based on this, we will determine "start time" and "end time" to consider for a specific "work date" in an ongoing meeting. This calculation is being done in a Derived Table, for a grouping of user_id and "work date".
Afterwards, we can SUM() up "working hours" per day of a user using some maths. Please find the query below. I have added extensive comments to it; do let me know if anything is still unclear.
Demo on DB Fiddle
Query #1
SELECT
dt.user_id,
dt.wd AS date,
SEC_TO_TIME(SUM(
CASE
/*When both start & end times are less than 9am OR more than 5pm*/
WHEN (st < TIME_TO_SEC('09:00:00') AND et < TIME_TO_SEC('09:00:00')) OR
(st > TIME_TO_SEC('17:00:00') AND et > TIME_TO_SEC('17:00:00'))
THEN et - st /* straightforward difference between the two times */
/* atleast one of the times is in 9am-5pm block, OR,
start < 9 am and end > 5pm.
Math of this can be worked out based on signum function */
ELSE GREATEST(0, TIME_TO_SEC('09:00:00') - st) +
GREATEST(0, et - TIME_TO_SEC('17:00:00'))
END
)) AS working_hours
FROM
(
SELECT
m.user_id,
/* Specific work date */
DATE(m.start_time) + INTERVAL ngen.gap DAY AS wd,
/* Start time to consider for this work date */
/* If the work date is on the same date as the actual start time
we consider this time */
CASE WHEN DATE(m.start_time) + INTERVAL ngen.gap DAY = DATE(m.start_time)
THEN TIME_TO_SEC(TIME(m.start_time))
/* We are on the days after the start day */
ELSE 0 /* 0 seconds (start of the day) */
END AS st,
/* End time to consider for this work date */
/* If the work date is on the same date as the actual end time
we consider this time */
CASE WHEN DATE(m.start_time) + INTERVAL ngen.gap DAY = DATE(m.end_time)
THEN TIME_TO_SEC(TIME(m.end_time))
/* More days to come still for this meeting,
we consider the end of this day as end time */
ELSE 86400 /* 24 hours * 3600 seconds (end of the day) */
END AS et
FROM meetings AS m
JOIN (SELECT 0 AS gap UNION ALL SELECT 1 UNION ALL SELECT 2) AS ngen
ON DATE(start_time) + INTERVAL ngen.gap DAY <= DATE(end_time)
) AS dt
GROUP BY dt.user_id, dt.wd;
Result
| user_id | date | working_hours |
| ------- | ---------- | ------------- |
| 1 | 2018-05-09 | 05:00:00 |
| 1 | 2018-05-10 | 03:00:00 |
| 1 | 2018-05-11 | 07:00:00 |
| 1 | 2018-05-12 | 09:00:00 |
| 2 | 2018-05-11 | 13:30:00 |
| 2 | 2018-05-12 | 09:00:00 |
Further Optimization Possibilities:
This query can do away with the usage of subquery (Derived Table) very easily. I just wrote it in this way, to convey the mathematics and process in a followable manner. However, you can easily merge the two SELECT blocks to a single query.
Maybe, more optimization possible in usage of Date/Time functions, as well as further simplification of mathematics in it. Function details available at: https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html
Some date calculations are done multiple times, e.g., DATE(m.start_time) + INTERVAL ngen.gap DAY. To avoid recalculation, we can utilize User-defined variables, which will also make the query less verbose.
Make this JOIN condition sargable: JOIN .. ON DATE(start_time) + INTERVAL ngen.gap DAY <= DATE(end_time)
I've searched for this topic but all I got was questions about grouping results by month. I need to retrieve rows grouped by month with summed up cost from start date to this whole month
Here is an example table
Date | Val
----------- | -----
2017-01-20 | 10
----------- | -----
2017-02-15 | 5
----------- | -----
2017-02-24 | 15
----------- | -----
2017-03-14 | 20
I need to get following output (date format is not the case):
2017-01-20 | 10
2017-02-24 | 30
2017-03-14 | 50
When I run
SELECT SUM(`val`) as `sum`, DATE(`date`) as `date` FROM table
AND `date` BETWEEN :startDate
AND :endDate GROUP BY year(`date`), month(`date`)
I got sum per month of course.
Nothing comes to my mind how to put in nicely in one query to achieve my desired effect, probably W will need to do some nested queries but maybe You know some better solution.
Something like this should work (untestet). You could also solve this by using subqueries, but i guess that would be more costly. In case you want to sort the result by the total value the subquery variant might be faster.
SET #total:=0;
SELECT
(#total := #total + q.sum) AS total, q.date
FROM
(SELECT SUM(`val`) as `sum`, DATE(`date`) as `date` FROM table
AND `date` BETWEEN :startDate
AND :endDate GROUP BY year(`date`), month(`date`)) AS q
You can use DATE_FORMAT function to both, format your query and group by.
DATE_FORMAT(date,format)
Formats the date value according to the format string.
SELECT Date, #total := #total + val as total
FROM
(select #total := 0) x,
(select Sum(Val) as Val, DATE_FORMAT(Date, '%m-%Y') as Date
FROM st where Date >= '2017-01-01' and Date <= '2017-12-31'
GROUP BY DATE_FORMAT(Date, '%m-%Y')) y
;
+---------+-------+
| Date | total |
+---------+-------+
| 01-2017 | 10 |
+---------+-------+
| 02-2017 | 30 |
+---------+-------+
| 03-2017 | 50 |
+---------+-------+
Can check it here: http://rextester.com/FOQO81166
Try this.
I use yearmonth as an integer (the year of the date multiplied by 100 plus the month of the date) . If you want to re-format, your call, but integers are always a bit faster.
It's the complete scenario, including input data.
CREATE TABLE tab (
dt DATE
, qty INT
);
INSERT INTO tab(dt,qty) VALUES( '2017-01-20',10);
INSERT INTO tab(dt,qty) VALUES( '2017-02-15', 5);
INSERT INTO tab(dt,qty) VALUES( '2017-02-24',15);
INSERT INTO tab(dt,qty) VALUES( '2017-03-14',20);
SELECT
yearmonths.yearmonth
, SUM(by_month.month_qty) AS running_qty
FROM (
SELECT DISTINCT
YEAR(dt) * 100 + MONTH(dt) AS yearmonth
FROM tab
) yearmonths
INNER JOIN (
SELECT
YEAR(dt) * 100 + MONTH(dt) AS yearmonth
, SUM(qty) AS month_qty
FROM tab
GROUP BY YEAR(dt) * 100 + MONTH(dt)
) by_month
ON yearmonths.yearmonth >= by_month.yearmonth
GROUP BY yearmonths.yearmonth
ORDER BY 1;
;
yearmonth|running_qty
201,701| 10.0
201,702| 30.0
201,703| 50.0
select succeeded; 3 rows fetched
Need explanations?
My solution has the advantage over the others that it will be re-usable without change when you move it to a more modern database - and you can convert it to using analytic functions when you have time.
Marco the Sane
Let's say I have a table that says how many items of something are valid between two dates.
Additionally, there may be multiple such periods.
For example, given a table:
itemtype | count | start | end
A | 10 | 2014-01-01 | 2014-01-10
A | 10 | 2014-01-05 | 2014-01-08
This means that there are 10 items of type A valid 2014-01-01 - 2014-01-10 and additionally, there are 10 valid 2014-01-05 - 2014-01-08.
So for example, the sum of valid items at 2014-01-06 are 20.
How can I query the table to get the sum per day? I would like a result such as
2014-01-01 10
2014-01-02 10
2014-01-03 10
2014-01-04 10
2014-01-05 20
2014-01-06 20
2014-01-07 20
2014-01-08 20
2014-01-09 10
2014-01-10 10
Can this be done with SQL? Either Oracle or MySQL would be fine
The basic syntax you are looking for is as follows:
For my example below I've defined a new table called DateTimePeriods which has a column for StartDate and EndDate both of which are DATE columns.
SELECT
SUM(NumericColumnName)
, DateTimePeriods.StartDate
, DateTimePeriods.EndDate
FROM
TableName
INNER JOIN DateTimePeriods ON TableName.dateColumnName BETWEEN DateTimePeriods.StartDate and DateTimePeriods.EndDate
GROUP BY
DateTimePeriods.StartDate
, DateTimePeriods.EndDate
Obviously the above code won't work on your database but should give you a reasonable place to start. You should look into GROUP BY and Aggregate Functions. I'm also not certain of how universal BETWEEN is for each database type, but you could do it using other comparisons such as <= and >=.
There are several ways to go about this. First, you need a list of dense dates to query. Using a row generator statement can provide that:
select date '2014-01-01' + level -1 d
from dual
connect by level <= 15;
Then for each date, select the sum of inventory:
with
sample_data as
(select 'A' itemtype, 10 item_count, date '2014-01-01' start_date, date '2014-01-10' end_date from dual union all
select 'A', 10, date '2014-01-05', date '2014-01-08' from dual),
periods as (select date '2014-01-01' + level -1 d from dual connect by level <= 15)
select
periods.d,
(select sum(item_count) from sample_data where periods.d between start_date and end_date) available
from periods
where periods.d = date '2014-01-06';
You would need to dynamically set the number of date rows to generate.
If you only needed a single row, then a query like this would work:
with
sample_data as
(select 'A' itemtype, 10 item_count, date '2014-01-01' start_date, date '2014-01-10' end_date from dual union all
select 'A', 10, date '2014-01-05', date '2014-01-08' from dual)
select sum(item_count)
from sample_data
where date '2014-01-06' between start_date and end_date;
I have a table called user_logins which tracks user logins into the system. It has three columns, login_id, user_id, and login_time
login_id(INT) | user_id(INT) | login_time(TIMESTAMP)
------------------------------------------------------
1 | 4 | 2010-8-14 08:54:36
1 | 9 | 2010-8-16 08:56:36
1 | 9 | 2010-8-16 08:59:19
1 | 3 | 2010-8-16 09:00:24
1 | 1 | 2010-8-16 09:01:24
I am looking to write a query that will determine the number of unique logins for each day if that day has a login and only for the past 30 days from the current date. So for the output should look like this
logins(INT) | login_date(DATE)
---------------------------
1 | 2010-8-14
3 | 2010-8-16
in the result table 2010-8-16 only has 3 because the user_id 9 logged in twice that day and him logging into the system only counts as 1 login for that day. I am only looking for unique logins for a particular day. Remember I only want the past 30 days so its like a snapshot of the last month of user logins for a system.
I have attempted to create the query with little success what I have so far is this,
SELECT
DATE(login_time) as login_date,
COUNT(login_time) as logins
FROM
user_logins
WHERE
login_time > (SELECT DATE(SUBDATE(NOW())-1)) FROM DUAL)
AND
login_time < LAST_DAY(NOW())
GROUP BY FLOOR(login_time/86400)
I know this is wrong and this returns all logins only starting from the beginning of the current month and doesn't group them correctly. Some direction on how to do this would be greatly appreciated. Thank you
You need to use COUNT(DISTINCT ...):
SELECT
DATE(login_time) AS login_date,
COUNT(DISTINCT login_id) AS logins
FROM user_logins
WHERE login_time > NOW() - interval 30 day
GROUP BY DATE(login_time)
I was a little unsure what you wanted for your WHERE clause because your question seems to contradict itself. You may need to modify the WHERE clause depending on what you want.
As Mark suggests you can use COUNT(DISTINCT...
Alternatively:
SELECT login_day, COUNT(*)
FROM (
SELECT DATE_FORMAT(login_time, '%D %M %Y') AS login_day,
user_id
FROM user_logins
WHERE login_time>DATE_SUB(NOW(), INTERVAL 1 MONTH)
GROUP BY DATE_FORMAT(login_time, '%D %M %Y'),
user_id
)
GROUP BY login_day