Split and compare hour format in MySQL - mysql

I'm continuing a project from someone else where they stored the time in hh:mm/hh:mm format in the MySQL database with the delimiter /
How do I split up and compare between those hours with the TIME datatype?

Considering there are a finite and known number of fields, you could use this (admittedly) ugly solution to split the fields using SUBSTRING_INDEX and cast them to TIME for further comparison/manipulation.
SELECT
CAST(SUBSTRING_INDEX(monday,'/',1) AS TIME) AS 'monday_start',
CAST(SUBSTRING_INDEX(monday,'/',-1) AS TIME) AS 'monday_end',
CAST(SUBSTRING_INDEX(tuesday,'/',1) AS TIME) AS 'tuesday_start',
CAST(SUBSTRING_INDEX(tuesday,'/',-1) AS TIME) AS 'tuesday_end',
CAST(SUBSTRING_INDEX(wednesday,'/',1) AS TIME) AS 'wednesday_start',
CAST(SUBSTRING_INDEX(wednesday,'/',-1) AS TIME) AS 'wednesday_end',
CAST(SUBSTRING_INDEX(thursday,'/',1) AS TIME) AS 'thursday_start',
CAST(SUBSTRING_INDEX(thursday,'/',-1) AS TIME) AS 'thursday_end',
CAST(SUBSTRING_INDEX(friday,'/',1) AS TIME) AS 'friday_start',
CAST(SUBSTRING_INDEX(friday,'/',-1) AS TIME) AS 'friday_end',
CAST(SUBSTRING_INDEX(saturday,'/',1) AS TIME) AS 'saturday_start',
CAST(SUBSTRING_INDEX(saturday,'/',-1) AS TIME) AS 'saturday_end',
CAST(SUBSTRING_INDEX(sunday,'/',1) AS TIME) AS 'sunday_start',
CAST(SUBSTRING_INDEX(sunday,'/',-1) AS TIME) AS 'sunday_end'
FROM times
DB Fiddle
A tip of the cap to fellow Stack Overflow user madde74 for the inspiration in an older answer.

This will split and convert the thursday column's first time to data type TIME
SELECT TIME(SUBSTRING_INDEX(thursday,'/',1)) FROM YOUR_TABLE
Output
03:01:00

i hope you can finish the rest...
create database t
create table t (
user_id int,
dayofweek varchar(15),
timerange varchar(15)
)
insert into t (user_id, dayofweek, timerange) select 83, 'monday', '00:00/12:30'
<br>insert into t (user_id, dayofweek, timerange) select 83, 'tuesday', null
<br>insert into t (user_id, dayofweek, timerange) select 83, 'wednesday', '00:00/24:00'
<br>insert into t (user_id, dayofweek, timerange) select 83, 'thursday', '03:01/10:02'
<br>insert into t (user_id, dayofweek, timerange) select 83, 'friday', '00:00/24:00'
<br>insert into t (user_id, dayofweek, timerange) select 83, 'saturday', '00:00/24:00'
<br>insert into t (user_id, dayofweek, timerange) select 83, 'sunday', '00:00/24:00'
select *,
substring(timerange, 1, 2) as [timerange1hours],
substring(timerange, 4, 2) as [timerange1minutes],
substring(timerange, 7, 2) as [timerange2hours],
substring(timerange, 10, 2) as [timerange2minutes],
convert(int, substring(timerange, 7, 2)) - convert(int, substring(timerange, 1, 2)) as [diffhours],
convert(int, substring(timerange, 10, 2)) - convert(int, substring(timerange, 4, 2)) as [diffminutes]
from t
where timerange is not null
delete from t

On the basis of your explanation, I assume that two time elements on either side of '/' are to and from time. In other words monday column could be divided into to columns monday_from and monday_to and 00:00/12:30 could then be split with monday_from having 00:00 and monday_to having 12:30.
Following steps will help you achieve what you are after
You can split 00:00/12:30 using SUBSTRING_INDEX()
e.g. select SUBSTRING_INDEX('00:00/12:30', '/', 1) monday_to, SUBSTRING_INDEX('00:00/12:30', '/', -1) monday_from
You can then append :00 to each to represent seconds using CONCAT(). This will be insignificant as that data is not being recorded
Using str_to_date() function then you can convert this string into time
e.g. select str_to_date('12:30:00', %h:%i:%s)
- You can then compare these as time
select
str_to_date(CONCAT(SUBSTRING_INDEX('00:00/12:30', '/', 1), ':00'), %h:%i:%s) as monday_to,
str_to_date(CONCAT(SUBSTRING_INDEX('00:00/12:30', '/', -1), ':00'), %h:%i:%s) as monday_from
Note: I don't have MySQL DB handy so, SQL is not checked for syntactic error, may have small but correctable errors.

Related

MYSQL - get three or more consecutive rows by time (on same date) where amount is minimum

I have a database with the following columns. I have added some sample data to show formatting.
date, time, amount
2021-10-14, 13:00, 15.40
2021-10-14, 13:01, 9.34
2021-10-14, 13:02, 10.12
2021-10-14, 13:03, 7.44
There are 2.6 million rows in the database spanning two years.
Each row is an increment of 1 minute.
I need to write sql that will output and group rows that are continuous by minute for the same date, where the amount is greater than 8.00 and there are a minimum of 3 consecutive rows.
This would then find an example like:
2021-11-30, 14:44, 8.04
2021-11-30, 14:45, 9.41
2021-11-30, 14:46, 9.27
2021-11-30, 14:47, 10.54
2021-11-30, 14:48, 11.09
2022-03-13, 08:22, 36.44
2022-03-13, 08:23, 17.38
2022-03-13, 08:24, 11.86
So if I understand correctly you only want to select the rows that are part of a 3 minute (minimum) consecutive sequence where amount >= 8 ?
I'm not sure about the performance but this seems to work:
Setup:
CREATE TABLE series
(
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
date DATE NOT NULL,
time TIME NOT NULL,
datetime DATETIME GENERATED ALWAYS AS (TIMESTAMP(date, time)),
amount decimal(5, 2),
INDEX (amount)
);
INSERT INTO series (date, time, amount)
VALUES ('2021-11-30', '14:40', 7),
('2021-11-30', '14:41', 8),
('2021-11-30', '14:42', 8),
('2021-11-30', '14:43', 8),
('2021-11-30', '14:44', 8),
('2021-11-30', '14:45', 7),
('2021-11-30', '14:46', 8),
('2021-11-30', '14:47', 8),
('2021-11-30', '14:48', 8),
('2021-11-30', '14:49', 7),
('2021-11-30', '14:50', 8),
('2021-11-30', '14:51', 8),
('2021-11-30', '14:52', 7)
;
The solution:
SELECT date, time, T.amount
FROM (SELECT date,
time,
datetime,
amount,
LAG(datetime, 2) OVER (order by datetime) AS tmin2,
LAG(datetime, 1) OVER (order by datetime) AS tmin1,
LEAD(datetime, 1) OVER (order by datetime) AS tplus1,
LEAD(datetime, 2) OVER (order by datetime) AS tplus2
FROM series
WHERE amount >= 8) T
WHERE TIME_TO_SEC(TIMEDIFF(T.datetime, T.tmin2)) = 120
OR TIME_TO_SEC(TIMEDIFF(T.datetime, T.tplus2)) = -120
OR (TIME_TO_SEC(TIMEDIFF(T.datetime, T.tmin1)) = 60 AND TIME_TO_SEC(TIMEDIFF(T.datetime, T.tplus1)) = -60)
ORDER BY datetime;
Explanation:
First we filter out the values < 8 using a WHERE-statement.
Then we peek into the previous two and next two rows ordered by datetime to see if the current to is part of a 3 min sequence and filter based on that criteria.
Here is my solution:
Table Definition :
CREATE TABLE YourTbl (
date date DEFAULT NULL,
time time DEFAULT NULL,
amount decimal(4,2) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO YourTbl VALUES
('2021-10-12','16:30', 20.40),
('2021-10-12','14:21', 19.34),
('2021-10-14','13:00', 15.40),
('2021-10-14','13:01', 9.34),
('2021-10-14','13:02', 10.12),
('2021-10-14','13:03', 7.44),
('2021-11-30', '14:44', 8.04),
('2021-11-30', '14:45', 9.41),
('2021-11-30', '14:46', 9.27),
('2021-11-30', '14:47', 10.54),
('2021-11-30', '14:48', 11.09),
('2022-03-13', '08:22', 36.44),
('2022-03-13', '08:23', 17.38),
('2022-03-13', '08:24', 11.86);
Lets test the query:
SELECT
date,time,amount FROM
(SELECT date,time,amount,
LEAD(minute(time),1) OVER(PARTITION BY date ORDER BY time) as leadtime,
LAG(minute(time),1) OVER(PARTITION BY date ORDER BY time) as lagtime,
(minute(time) - LAG(minute(time),1) OVER(PARTITION BY date ORDER BY time)) as minute_forward_difference,
(LEAD(minute(time),1) OVER(PARTITION BY date ORDER BY time) - minute(time)) as minute_backward_difference
FROM YourTbl
WHERE amount > 8.0
) as tblder
WHERE (minute_forward_difference = 1) OR (minute_backward_difference = 1)
GROUP BY date,time,amount;
Resultset:

Running Value CountDistinct and Logic Expression?

I have original data like this.
Original Data
I need to create two report with it, this is the first report :
First Report
The running value can be achieved with this expression
RunningValue(Fields!City.Value+Fields!Month.Value,CountDistinct,"Region")
The second report i need is this:
Second Report
What can i do to add logic to the running value so it can avoid numbering row with Sum(Amount) zero ?
I'm not sure you can do this using RunningValue, other people may know of a way.
What I did was move the logic to the query.
I reproduced some data to match your final report numbers (your sample data does not match the sample report output).
Here's the sample data I used.
DECLARE #t TABLE(Region varchar(10), City varchar(10), MonthID int, Amount int)
INSERT INTO #t VALUES
('Asia', 'Tokyo', 4, 1000),
('Asia', 'Tokyo', 4, 500),
('Asia', 'Tokyo', 5, 2000),
('Asia', 'Tokyo', 5, -2000),
('Asia', 'Tokyo', 6, 1000),
('Asia', 'Tokyo', 6, -500),
('Asia', 'Bangkok', 4, 500),
('Asia', 'Bangkok', 4, 500),
('Asia', 'Bangkok', 5, 3000),
('Asia', 'Bangkok', 5, -500),
('Asia', 'Bangkok', 6, -750),
('Asia', 'Bangkok', 6, 750)
SELECT
*
, ROW_NUMBER() OVER(PARTITION BY Region, City ORDER BY MonthID) as RowN1
, ROW_NUMBER() OVER(PARTITION BY (CASE Amount WHEN 0 THEN 0 ELSE 1 END), Region, City ORDER BY MonthID) as RowN2
FROM
(
SELECT
Region, City, MonthID
, SUM(Amount) AS Amount
FROM #t
GROUP BY Region, City, MonthID
) x
ORDER BY Region, City DESC, MonthID
I used the ROW_NUMBER function to assign a row numbers for both reports.
The first one "RowN1" is a simple row number within city
The second one "RowN2" does the same thing but it partitions any zero values so they are not in the same partition as the other data.
This gives us the following dataset
Now you can use a simple table to display the result in your first report using RowN1
In your second report use RowN2 with the expression
=IIF(Fields!Amount.Value=0, Nothing, Fields!RowN2.Value)
This simply forces a blank to be displayed if the amount is zero.
I did this and got the following results.
Note: I used a month number in the data just to make sorting easier, in the report I used =MonthName(Fields!MonthID.Value) to show the actual name.

How do you aggregate a column on the day before, day of, and day after an event indicated by a flag column?

I have a table which has a date column, some self-reports of happiness in another column, and a flag column which indicates a gym day.
I want to get the average happiness scores on the day before, the day of, and the day after a gym session.
If you imagine this table, the averages should return day_before = 1, day_of = 2, and day_after = 3.
So the set up is like in this fiddle, although in my actual database the gym flag column is joined in from a separate table.
CREATE TABLE test
(`date` datetime, `gym` int, `happiness` int)
;
INSERT INTO test
(`date`, `gym`, `happiness`)
VALUES
('2019-01-06 00:00:00', NULL, 1),
('2019-02-06 00:00:00', 1, 2),
('2019-03-06 00:00:00', NULL, 3),
('2019-04-06 01:00:00', NULL, 1),
('2019-05-06 01:00:00', 1, 2),
('2019-06-06 01:00:00', NULL, 3),
('2019-07-06 01:00:00', NULL, 1),
('2019-08-06 01:00:00', 1, 2),
('2019-09-06 01:00:00', NULL, 3)
;
I tried using a subquery to return when the "gym" column in date - 1 = 1, and also use the results in a case which would have "day of", "day before", and "day after" strings. Then I could simply group by that column. I couldn't get this to work and I'm not even sure if that's something you can do.
Use two self-joins.
SELECT AVG(before.happiness) AS day_before, AVG(current.happiness) AS day_of, AVG(after.happiness) AS day_after
FROM test AS current
JOIN test AS before ON before.date = DATE_SUB(current.date, INTERVAL 1 DAY)
JOIN test AS after ON after.date = DATE_ADD(current.date, INTERVAL 1 DAY)
WHERE current.gym = 1

Sorting Dates and serial number

Date SrNo
SEL/2016APR01/002/000001 01
SEL/2016APR02/002/000001 04
SEL/2016APR03/002/000001 03
SEL/2016JAN01/002/000001 02
I need to sort the first column Date part(2016JAN01) in Descending order and if there are two same dates then I need to sort the second column in Descending order.
substr('SEL/2016APR01/002/000001', 4,12) gives me 2016APR01 but I dont know how to sort dates of that format.
My query looks something like this
SELECT *
FROM tablename
ORDER BY substr(Date) DES, SrNo DESC
Update:
This first answer covers SQL Server, while the answer belows it is for MySQL. You originally tagged your question MySQL, and so everyone answered for that.
For SQL Server you can use CONVERT:
ORDER BY CONVERT(DATETIME,
SUBSTRING(Date, 12, 2) + ' ' +
SUBSTRING(Date, 9, 3) + ' ' +
SUBSTRING(Date, 5, 4),
106) DESC,
SrNo DESC
If you were actually using MySQL, then STR_TO_DATE would be the way to go:
ORDER BY STR_TO_DATE(SUBSTR(Date, 4, 12), '%Y%b%d') DESC,
SrNo DESC
Here is what the formatting parameters mean:
%Y - four digit year
%b - three letter month (e.g. JAN, APR)
%d - two digit day of month
If the upper case format of the month names results in STR_TO_DATE not working, you can try the following (which is admittedly a bit ugly):
ORDER BY STR_TO_DATE(
CONCAT(SUBSTRING(Date, 5, 4),
SUBSTRING(Date, 9, 1),
LOWER(SUBSTRING(Date, 10, 2)),
SUBSTRING(Date, 12, 2)),
'%Y%b%d') DESC,
SrNo DESC
As #RiggsFolly mentioned, your life would made easier if you stored your dates in a standard MySQL format.
Have you tried ORDER BY DATE_FORMAT(substr(Date, 4,12)), '%Y%b%d') DESC, SrNo DESC
use STR_TO_DATE FUNCTION
SELECT *
FROM tablename
ORDER BY STR_TO_DATE(SUBSTR(Date, 4, 12), '%Y%b%d') , SrNo DESC

Order by day_of_week in MySQL

How can I order the mysql result by varchar column that contains day of week name?
Note that MONDAY should goes first, not SUNDAY.
Either redesign the column as suggested by Williham Totland, or do some string parsing to get a date representation.
If the column only contains the day of week, then you could do this:
ORDER BY FIELD(<fieldname>, 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY');
Why not this?
ORDER BY (
CASE DAYOFWEEK(dateField)
WHEN 1 THEN 7 ELSE DAYOFWEEK(dateField)
END
)
I believe this orders Monday to Sunday...
I'm thinking that short of redesigning the column to use an enum instead, there's not a lot to be done for it, apart from sorting the results after you've gotten them out.
Edit: A dirty hack is of course to add another table with id:weekday pairs and using joins or select in selects to fake an enum.
... ORDER BY date_format(order_date, '%w') = 0, date_format(order_date, '%w') ;
This looks messy but still works and seems more generic:
select day,
case day
when 'monday' then 1
when 'tuesday' then 2
when 'wednesday' then 3
when 'thursday' then 4
when 'friday' then 5
when 'saturday' then 6
when 'sunday' then 7
end as day_nr from test order by day_nr;
Using if is even more generic and messier:
select id, day,
if(day = 'monday',1,
if(day = 'tuesday',2,
if(day = 'wednesday',3,
if(day = 'thursday',4,
if(day = 'friday',5,
if(day = 'saturday',6,7)
)
)
)
)
) as day_nr from test order by day_nr;
You can also hide the details of conversion from name to int in stored procedure.
I realise that this is an old thread, but as it comes to the top of google for certain search times I will use it to share my approach.
I wanted the same result as the original question, but in addition I wanted the ordering of the results starting from the current day of the week and then progressing through the rest of the days.
I created a separate table, in which the days were listed over a fortnight, so that no matter which day you started from you could run through a sequence of 7 days.
CREATE TABLE IF NOT EXISTS `Weekdays` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=15 ;
INSERT INTO `Weekdays` (`id`, `name`) VALUES
(1, 'Monday'),
(2, 'Tuesday'),
(3, 'Wednesday'),
(4, 'Thursday'),
(5, 'Friday'),
(6, 'Saturday'),
(7, 'Sunday'),
(8, 'Monday'),
(9, 'Tuesday'),
(10, 'Wednesday'),
(11, 'Thursday'),
(12, 'Friday'),
(13, 'Saturday'),
(14, 'Sunday');
I then ran the query with a variable that determined the start point in sequence and used a join to get the order number for the days. For example to start the listing at Wednesday, I do the following:
SELECT #startnum := MIN(id) FROM Weekdays WHERE name='Wednesday';
SELECT * FROM Events INNER JOIN ( SELECT id as weekdaynum, name as dayname FROM Weekdays WHERE id>(#startnum-1) AND id<(#startnum+7) ) AS s2 ON s2.dayname=Events.day ORDER BY weekdaynum;
I hope this helps someone who stumbles onto this post.
Found another way, your can reverse order bye week
ORDER BY date_format(date_name, '%w') DESC;
Another way would be to create another table with those days and an int to order them by, join that table when searching, and order by it. Of course, joining on a varchar is not recommended.
Table DaysOfWeek
id | day
--------------------
1 | Monday
2 | Tuesday
3 | Wednesday
4 | Thursday
5 | Friday
6 | Saturday
SELECT * FROM WhateverTable
LEFT JOIN DaysOFWeek on DaysOFWeek.day = WhateverTable.dayColumn
ORDER BY DaysOfWeek.id
(Apologies if that's not correct; I've been stuck with SQL server recently)
Again, this is NOT recommended, but if you cannot alter the data you've already got... This will also work if there are non-standard values in the dayColumn field.
Found another way that works for me:
SELECT LAST_NAME, HIRE_DATE, TO_CHAR(HIRE_DATE, 'fmDAY') as 'Day' FROM EMPLOYEES
ORDER BY TO_CHAR(HIRE_DATE, 'd');
Hope it helps
In my case, since the days can be registered in several languages, to get the correct order I do like this according to Glen Solsberry:
....
....
ORDER BY
FIELD(<fieldname>, 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'),
FIELD(<fieldname>, 'LUNDI', 'MARDI', 'MERCREDI', 'JEUDI', 'VENDREDI', 'SAMEDI', 'DIMANCHE'),
FIELD(<fieldname>, 'LUNES', 'MARTES', 'MIERCOLES', 'JUEVES', 'VIERNES', 'SABADO', 'DOMINGO'),
FIELD(<fieldname>, 'MONTAGE', 'DIENSTAG', 'MITTWOCH', 'DENNERSTAG', 'FREITAG', 'SAMSTAG', 'SONNTAG')
;
Do not forget that, <fieldname> is the name of the date column in question in your case.
I saw that ...WHEN 1 THEN 7... was posted but it should be WHEN 1 THEN 8.
So...
ORDER BY (
CASE DATEPART(DW, yourdatefield)
WHEN 1 THEN 8 ELSE DATEPART(DW, yourdatefield)
END
)
Otherwise Sunday may come before Saturday because both Sunday and Saturday would equal 7. By setting Sunday to 8, it ensures it comes after Saturday.
If you try this, it should work:
SELECT ename, TO_CHAR(hiredate, 'fmDay') as "Day"
FROM my_table
ORDER BY MOD(TO_CHAR(hiredate, 'D') + 5, 7)