Query to find an entry between dates - mysql

I have a table containing several records associated to the same entities. Two of the fields are dates - start and end dates of a specific period.
Example:
ID
Name
Start
End
3
Fred
2022/01/01
2100/12/31
2
John
2018/01/01
2021/12/31
1
Mark
2014/03/22
2017/12/31
The dates and names vary, but the only rule is that there are NO OVERLAPS - it's a succession of people in charge of a unique role, so there is only one record which is valid for any date.
I have a query returning me a date (let's call it $ThatDay) and what I am trying to do is to find a way to find which name it was at that specific date. For example, if the date was July 4th, 2019, the result of the query I am after would be "John"
I have run out of ideas on how to structure a query to help me find it. Thank you in advance for any help!

you can use a SELECT with BETWEEN as WHERE clause
The date format of MySQL is yyyy-mm-dd , if you keep that you wil never have problems
CREATE TABLE datetab (
`ID` INTEGER,
`Name` VARCHAR(4),
`Start` DATETIME,
`End` DATETIME
);
INSERT INTO datetab
(`ID`, `Name`, `Start`, `End`)
VALUES
('3', 'Fred', '2022/01/01', '2100/12/31'),
('2', 'John', '2018/01/01', '2021/12/31'),
('1', 'Mark', '2014/03/22', '2017/12/31');
SELECT `Name` FROM datetab WHERE '2019-07-04' BETWEEN `Start` AND `End`
| Name |
| :--- |
| John |
db<>fiddle here
If ou have a (Sub)- Query with a date as result,you can join it for example
SELECT `Name`
FROM datetab CROSS JOIN (SELECT '2019-07-04' as mydate FROM dual) t1
WHERE mydate BETWEEN `Start` AND `End`
| Name |
| :--- |
| John |
db<>fiddle here
Also when the query only return one row and column you can use the subquery like this
SELECT `Name`
FROM datetab
WHERE (SELECT '2019-07-04' as mydate FROM dual) BETWEEN `Start` AND `End`
| Name |
| :--- |
| John |
db<>fiddle here

Select where the result of your find-date query is between start and end:
select * from mytable
where (<my find date query>)
between start and end

Related

Need SQL Query to fetch data from a table for MYSQL

I have a table where there are columns like ID, USER, PreviousValue, CurrentValue, Date.
Need a query that will give the latest current value and the rest columns could be of any user based on user queried for.
Example: If the table has last entry for user A, and query is for User B, The ID, User, previous Value, Date should be returned for user B but the current Value should be for user A.
ID Previous Current User createdOn
1 RED BLUE System 14-MAR-2020
2 GREEN YELLOW ADMIN 12-MAR-2020
IF I query for ADMIN as user the row returned should contain below data:
ID Previous Current User createdOn
2 GREEN BLUE ADMIN 12-MAR-2020
As the latest row was added on 14 MARCH the current value should come from that row, rest all data should be of the user I queried for.
WARNING! Date Format Problem
I strongly urge you to convert the CreadedOn column to a DATE data-type, instead of VARCHAR, in order to retrieve the appropriate ordering of values by date.
Otherwise 14-MAR-2020 will be considered later than 11-DEC-2020.
Issue Example DB-Fiddle
Schema
CREATE TABLE users (
`ID` INTEGER,
`Previous` VARCHAR(5),
`Current` VARCHAR(6),
`User` VARCHAR(18),
`createdOn` VARCHAR(20)
);
INSERT INTO users
(`ID`, `Previous`, `Current`, `User`, `createdOn`)
VALUES
('1', 'RED', 'BLUE', 'System', '14-MAR-2020'),
('2', 'GREEN', 'YELLOW', 'ADMIN', '12-MAR-2020'),
('3', 'GREEN', 'PURPLE', 'System', '11-DEC-2020'),
('4', 'GREEN', 'YELLOW', 'System', '10-MAR-2020');
Other answer query https://stackoverflow.com/a/61316029/1144627
select
Id,
Previous,
User,
CreatedOn,
(
select Current
from users
order by CreatedOn desc
limit 1
) as Current
from users
where `user` = 'ADMIN'
order by createdon desc
limit 1;
| Id | Previous | User | CreatedOn | Current |
| --- | -------- | ----- | ----------- | ------- |
| 2 | GREEN | ADMIN | 12-MAR-2020 | BLUE |
Expected Current of PURPLE
To fix the issue with the date sorting, you will need to modify your table using the STR_TO_DATE() function.
It is important to note that comparing with STR_TO_DATE in your query instead of updating the column will cause a full-table scan, comparing every record in the table.
Example DB-Fiddle
ALTER TABLE users
ADD COLUMN createdOn_Date DATE NULL DEFAULT NULL;
UPDATE users
SET CreatedOn_Date = STR_TO_DATE(CreatedOn, '%d-%b-%Y')
WHERE CreatedOn_Date IS NULL;
ALTER TABLE users
DROP COLUMN CreatedOn,
CHANGE COLUMN CreatedOn_Date CreatedOn DATE;
Then display your records in your desired format, use the DATE_FORMAT() function
Other Answer Query https://stackoverflow.com/a/61316029/1144627
select
Id,
Previous,
User,
DATE_FORMAT(CreatedOn, '%d-%b-%Y') AS CreatedOn,
(
select Current
from users
order by CreatedOn desc
limit 1
) as Current
from users
where `user` = 'ADMIN'
order by createdon desc
limit 1;
Result
| Id | Previous | User | CreatedOn | Current |
| --- | -------- | ----- | ----------- | ------- |
| 2 | GREEN | ADMIN | 12-Mar-2020 | PURPLE |
A subquery to get the latest current value should do it.
select
Id,
Previous,
User,
CreatedOn,
(
select Current
from users
order by CreatedOn desc
limit 1
) as Current
from users
where user = ?
order by createdon desc
limit 1

MySQL/memSQL not using index on BETWEEN join condition

We have two tables:
A dates table that contains one date per day for the last 10 and next 10 years.
A states table that has the following columns: start_date, end_date, state.
The query we run looks like this:
SELECT dates.date, COUNT(*)
FROM dates
JOIN states
ON dates.date BETWEEN states.start_date AND states.end_date
WHERE dates.date BETWEEN '2017-01-01' AND '2017-01-31'
GROUP BY dates.date
ORDER BY dates.date;
According to the query plan, memSQL isn't using an index on the JOIN condition and this makes the query slow. Is there a way we can use an index on the JOIN condition?
We tried memSQL skiplist indexes on dates.date, states.start_date, states.end_date, (states.start_date, states.end_date)
Tables & EXPLAIN:
CREATE TABLE `dates` (
`date` date DEFAULT NULL,
KEY `date_index` (`date`)
)
CREATE TABLE `states` (
`start_date` datetime DEFAULT NULL,
`end_date` datetime DEFAULT NULL,
`state` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
KEY `start_date` (`start_date`),
KEY `end_date` (`end_date`),
KEY `start_date_end_date` (`start_date`,`end_date`),
)
+-----------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-----------------------------------------------------------------------------------------------------------------------------------------------------+
| GatherMerge [remote_0.date] partitions:all est_rows:96 alias:remote_0 |
| Project [r2.date, CAST(COALESCE($0,0) AS SIGNED) AS `COUNT(*)`] est_rows:96 |
| Sort [r2.date] |
| HashGroupBy [SUM(r2.`COUNT(*)`) AS $0] groups:[r2.date] |
| TableScan r2 storage:list stream:no |
| Repartition [r1.date, `COUNT(*)`] AS r2 shard_key:[date] est_rows:96 est_select_cost:26764032 |
| HashGroupBy [COUNT(*) AS `COUNT(*)`] groups:[r1.date] |
| Filter [r1.date <= states.end_date] |
| NestedLoopJoin |
| |---IndexRangeScan drstates_test.states, KEY start_date (start_date) scan:[start_date <= r1.date] est_table_rows:123904 est_filtered:123904 |
| TableScan r1 storage:list stream:no |
| Broadcast [dates.date] AS r1 distribution:tree est_rows:96 |
| IndexRangeScan drstates_test.dates, KEY date_index (date) scan:[date >= '2017-01-01' AND date <= '2017-01-31'] est_table_rows:18628 est_filtered:96 |
+-----------------------------------------------------------------------------------------------------------------------------------------------------+
ON dates.date BETWEEN states.start_date
AND states.end_date
is essentially un-optimizable. The only practical way to perform this test is to tediously test every row.
If you are using MySQL and don't need the dates table, consider starting with
SELECT *
FROM states
WHERE start_date >= '2017-01-01'
AND end_date < '2017-01-01' + INTERVAL 1 MONTH
Note that this works for any combination of DATE and DATETIME datatypes.
Since I am unclear on the ultimate goal, I am unclear on what to do next.

How do I get a left join with a group by clause to return all the rows?

I am trying to write a query to determine how much of my inventory is committed at a given time, i.e. current, next month, etc.
A simplified example:
I have an inventory table of items. I have an offer table that specifies the customer, when the offer starts, and when the offer expires. I have a third table that associates the two.
create table inventory
(id int not null auto_increment , name varchar(32) not null, primary key(id));
create table offer
(id int not null auto_increment , customer_name varchar(32) not null, starts_at datetime not null, expires_at datetime, primary key (id));
create table items
(id int not null auto_increment, inventory_id int not null, offer_id int not null, primary key (id),
CONSTRAINT fk_item__offer FOREIGN KEY (offer_id) REFERENCES offer(id),
CONSTRAINT fk_item__inventory FOREIGN KEY (inventory_id) REFERENCES inventory(id));
create some inventory
insert into inventory(name)
values ('item 1'), ('item 2'),('item 3');
create two offers for this month
insert into offer(customer_name, starts_at)
values ('customer 1', DATE_FORMAT(NOW(), '%Y-%m-01')), ('customer 2', DATE_FORMAT(NOW(), '%Y-%m-01'));
and one for next month
insert into offer(customer_name, starts_at)
values ('customer 3', DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01'));
Now add some items to each offer
insert into items(inventory_id, offer_id)
values (1,1), (2,1), (2,2), (3,3);
What I want is a query that will show me all the inventory and the count of the committed inventory for this month. Inventory would be considered committed if the starts_at is less than or equal to now, and the offer has not expired (expires_at is null or expires_at is in the future)
The results I would expect would look like this:
+----+--------+---------------------+
| id | name | committed_inventory |
+----+--------+---------------------+
| 1 | item 1 | 1 |
| 2 | item 2 | 2 |
| 3 | item 3 | 0 |
+----+--------+---------------------+
3 rows in set (0.00 sec)
The query that I felt should work is:
SELECT inventory.id
, inventory.name
, count(items.id) as committed_inventory
FROM inventory
LEFT JOIN items
ON items.inventory_id = inventory.id
LEFT JOIN offer
ON offer.id = items.offer_id
WHERE (offer.starts_at IS NULL OR offer.starts_at <= NOW())
AND (offer.expires_at IS NULL OR offer.expires_at > NOW())
GROUP BY inventory.id, inventory.name;
However, the results from this query does not include the third item. What I get is this:
+----+--------+---------------------+
| id | name | committed_inventory |
+----+--------+---------------------+
| 1 | item 1 | 1 |
| 2 | item 2 | 2 |
+----+--------+---------------------+
2 rows in set (0.00 sec)
I cannot figure out how to get the third inventory item to show. Since inventory is the driving table in the outer joins, I thought that it should always show.
The problem is the where clause. Try this:
SELECT inventory.id
, inventory.name
, count(offers.id) as committed_inventory
FROM inventory
LEFT JOIN items
ON items.inventory_id = inventory.id
LEFT JOIN offer
ON offer.id = items.offer_id and
(offer.starts_at <= NOW() or
offer.expires_at > NOW()
)
GROUP BY inventory.id, inventory.name;
The problem is that you get a matching offer, but it isn't currently valid. So, the where clause fails because the offer dates are not NULL (there is a match) and the date comparison fails because the offer is not current ly.
For item 3 the starts_at from offer table is set to March, 01 2014 which is greater than NOW so (offer.starts_at IS NULL OR offer.starts_at <= NOW()) condition will skip the item 3 record
See fiddle demo

Unable to INSERT ON DUPLICATE KEY UPDATE from another query

I am working on the following table:
CREATE TABLE `cons` (
`Id` char(20) NOT NULL,
`Client_ID` char(12) NOT NULL,
`voice_cons` decimal(11,8) DEFAULT '0.00000000',
`data_cons` int(11) DEFAULT '0',
`day` date DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
I need to get some data from another table, cdr, which contains a row per event. This means every call or data connection has its own row.
+-----------+--------------+----------------+-------+
| Client_ID | Data_Up_Link | Data_Down_Link | Price |
+-----------+--------------+----------------+-------+
| 1 | 23 | 56 | 0 |
| 1 | 12 | 3 | 0 |
| 1 | 0 | 0 | 5 |
+-----------+--------------+----------------+-------+
I need to compute the total voice and data consumption for each Client_ID in my new cons table, but just keeping a single record for each Client_ID and day. To keep the question simple, I will consider just one day.
+-----------+-----------+------------+
| Client_ID | data_cons | voice_cons |
+-----------+-----------+------------+
| 1 | 94 | 5 |
+-----------+-----------+------------+
I have unsuccessfully tried the following, among many other (alias, .
insert into cons_day (Id, Client_ID, voice_cons, MSISDN, day)
select
concat(Client_ID,date_format(date,'%Y%m%d')),
Client_ID,
sum(Price) as voice_cons,
date as day
from cdr
where Type_Cdr='VOICE'
group by Client_ID;
insert into cons_day (Id, Client_ID, data_cons, MSISDN, day)
select
concat(Client_ID,date_format(date,'%Y%m%d')),
Client_ID,
sum(Data_Down_Link+Data_Up_Link) as data_cons,
Calling_Number as MSISDN,
date as day
from cdr
where Type_Cdr='DATA'
group by Client_ID
on duplicate key update data_cons=data_cons;
But I keep getting the values unchanged or receiving SQL errors. I would really appreciate a piece of advice.
Thank you very much in advance.
First of all it seems that Id column in cons table is absolutely redundant. You already have ClientID and Day columns. Just make them PRIMARY KEY.
That being said the proposed table schema might look like
CREATE TABLE `cons`
(
`Client_ID` char(12) NOT NULL,
`voice_cons` decimal(11,8) DEFAULT '0.00000000',
`data_cons` int(11) DEFAULT '0',
`day` date DEFAULT NULL,
PRIMARY KEY (`Client_ID`, `day`)
);
Now you can use conditional aggregation to get your voice_cons and data_cons in one go
SELECT Client_ID,
SUM(CASE WHEN Type_CDR = 'VOICE' THEN price END) voice_cons,
SUM(CASE WHEN Type_CDR = 'DATA' THEN Data_Up_Link + Data_Down_Link END) data_cons,
DATE(date) day
FROM cdr
GROUP BY Client_ID, DATE(date)
Note: you have to GROUP BY both by Client_ID and DATE(date)
Now the INSERT statement should look like
INSERT INTO cons (Client_ID, voice_cons, data_cons, day)
SELECT Client_ID,
SUM(CASE WHEN Type_CDR = 'VOICE' THEN price END) voice_cons,
SUM(CASE WHEN Type_CDR = 'DATA' THEN Data_Up_Link + Data_Down_Link END) data_cons,
DATE(date) day
FROM cdr
GROUP BY Client_ID, DATE(date)
ON DUPLICATE KEY UPDATE voice_cons = VALUES(voice_cons),
data_cons = VALUES(data_cons);
Note: since now you simultaneously get both voice_cons and data_cons you might not need ON DUPLICATE KEY clause at all if you don't process data for the same dates multiple times.
Here is SQLFiddle demo

Create View with rank column in limited amount of data

So I have an MySQL table structured like this:
CREATE TABLE `spenttime` {
`id` int(11) NOT NULL AUTO_INCREMENT,
`userid` int(11) NOT NULL,
`serverid` int(11) NOT NULL,
`time` int(11) NOT NULL,
`day` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `dbid_sid_day` (`userid`,`serverid`,`day`)
}
Where I'm storing time spent on my game servers every day for each registered player. time is the amount of time spent, in seconds, day is an unix timestamp of each day (beginning of the day). I want to create an View on my database that will show for each user time spent on server every week, but with an column displaying rank of that time, independent for each server on each week. For example data (for clarify i will use date format Y-M-D instead of unix timestamp for day column on this example):
INSERT INTO `spenttime` (`userid`, `serverid`, `time`, `day`) VALUES
(1, 1, 200, '2013-04-01'),
(1, 1, 150, '2013-04-02'),
(2, 1, 100, '2013-04-02'),
(3, 1, 500, '2013-04-04'),
(2, 2, 400, '2013-04-04'),
(1, 1, 300, '2013-04-08'),
(3, 1, 200, '2013-04-08');
For that data in viev named spenttime_week should appear:
+--------+----------+--------+------------+------+
| userid | serverid | time | yearweek | rank |
+--------+----------+--------+------------+------+
| 1 | 1 | 350 | '2013-W14' | 2 |
| 2 | 1 | 100 | '2013-W14' | 3 |
| 3 | 1 | 500 | '2013-W14' | 1 |
| 2 | 2 | 400 | '2013-W14' | 1 |
| 1 | 1 | 300 | '2013-W15' | 1 |
| 3 | 1 | 200 | '2013-W15' | 2 |
+--------+----------+--------+------------+------+
I know how to generate view wihout rank, i have only troubles with rank column...
How can I make that happen?
//edit
Additionaly, this column MUST appear in viev, I cannot generate It in select from that view, because app where I will use it don't allow that...
First you need to create a first VIEW that sums the spent time for every user on the same week:
CREATE VIEW total_spent_time AS
SELECT userid,
serverid,
sum(time) AS total_time,
yearweek(day, 3) as week
FROM spenttime
GROUP BY userid, serverid, week;
then you can create your view as this:
CREATE VIEW spenttime_week AS
SELECT
s1.userid,
s1.serverid,
s1.total_time,
s1.week,
count(s2.total_time)+1 AS rank
FROM
total_spent_time s1 LEFT JOIN total_spent_time s2
ON s1.serverid=s2.serverid
AND s1.userid!=s2.userid
AND s1.week = s2.week
AND s1.total_time<=s2.total_time
GROUP BY
s1.userid,
s1.serverid,
s1.total_time,
s1.week
ORDER BY
s1.week, s1.serverid, s1.userid
Please see a fiddle here.
Lots of ways you could get the yearweek column, a quick lazy solution to that for clarity (because I doubt you're struggling with that). But here's how you can get the rank.
Use a self join to get dataset including rows with higher time value than current row, then count the rows with higher value:
This is much easier in MSSQL, which is where I live 99% of the time, and where you can just use the RANK() function. I hadn't realised until today there wasn't an equivalent in mysql. Fun to work out how to get the same result without MS's helping hand.
Prep stuff for context:
CREATE TABLE spenttime (userid int, serverid int, [time] int, [day] DATETIME)
CREATE TABLE weeklookup (weekname VARCHAR(10), weekstart DATETIME, weekend DATETIME)
INSERT INTO spenttime (userid, serverid, [time], [day]) VALUES
(1, 1, 200, '2013-apr-01'),
(1, 1, 150, '2013-apr-02'),
(2, 1, 100, '2013-apr-02'),
(3, 1, 500, '2013-apr-04'),
(2, 2, 400, '2013-apr-04'),
(1, 1, 300, '2013-apr-08'),
(3, 1, 200, '2013-apr-08');
INSERT INTO weeklookup(weekname, weekstart, weekend) VALUES
('2013-w14', '01/apr/2013', '08/apr/2013'),
('2013-w15', '08/apr/2013', '15/apr/2013')
GO
CREATE VIEW weekgroup AS
SELECT a.userid ,
a.serverid ,
a.[time] ,
w1.weekname
FROM spenttime a
INNER JOIN weeklookup w1 ON [day] >= w1.weekstart
AND [day] < w1.weekend
GO
Select statement for the view:
SELECT wv1.userid ,
wv1.serverid ,
wv1.[time] ,
wv1.weekname AS yearweek ,
COUNT(wv2.[time]) + 1 AS rank
FROM weekgroup wv1
LEFT JOIN weekgroup wv2 ON wv1.[time] < wv2.[time]
AND wv1.weekname = wv2.weekname
AND wv1.serverid = wv2.serverid
GROUP BY wv1.userid ,
wv1.serverid ,
wv1.[time] ,
wv1.weekname
ORDER BY wv1.weekname ,
wv1.[time] DESC
If you want to store the rank, you would use an insert trigger. The insert trigger would calculate the rank, as something like:
select count(*)
from spenttime_week w
where w.yearweek = new.yearweek and time >= new.time
However, I would not recommend this, because you then have to create an update trigger as well, and modify rank values that are already inserted.
Instead, access the table using SQL like:
select w.*,
(select count(*) from spenttime_week w2 where w2.yearweek = w.yearweek and w2.time >= w.time
) as rank
from spenttime_week w
This SQL may vary, depending on how you want to handle ties in the data. For performance reasons, you should have an index on at least yearweek, and probably on yearweek, time.