Fill missing dates in mysql query range - mysql

I have the following query:
select date(updated_at) as data, COUNT(id) as numar
from `coupons`
where `user_id` = 5 and `won_by` != 0 and `updated_at` >= '2016-04-01'
group by DAY(updated_at), month(updated_at), year(updated_at)
and the result is this:
2016-04-01- 229
2016-04-03- 30
2016-04-04- 6
2016-04-07- 1
2016-04-08- 1
2016-04-10- 1
What can I do to receive something like this:
2016-04-01- 229
2016-04-02- 0
2016-04-03- 30
2016-04-04- 6
2016-04-05- 0
2016-04-06- 0
2016-04-07- 1
2016-04-08- 1
2016-04-10- 1

The best way that I've found to do this is to simply create (and maintain) a secondary table with a single column, containing all of the dates that you care about. Something like:
CREATE TABLE date_join (
date date not null primary key
);
Then insert records for each date in whatever way is convenient (by hand, if it's a one-off, as part of your daily process, via stored procedure, etc).
At that point, it's simply a left join of date_join and your initial query, with a CASE statement to translate NULLs to 0s:
SELECT dj.date, q.numar
FROM date_join dj
LEFT JOIN (select date(updated_at) as date, COUNT(id) as numar
from `coupons`
where `user_id` = 5 and `won_by` != 0 and `updated_at` >= '2016-04-01'
group by DATE(updated_at)
) q
ON dj.date = q.date
ORDER BY dj.date;

Related

SQL Infinite loading

I have a database with 1 million records, it's working fine with around to 1.2s response time for simple queries using JOIN, GROUP BY, ORDER, .. It's ok and there are no problems with that. I'm working to simplify my queries using table aliases, but when I execute a simple query with two table aliases or more, the request never ends and MariaDB doesn't respond anymore, I have to restart the service manually.
Whats is going wrong ?
Here it's structure:
CREATE TABLE `values` (
`id` mediumint(11) UNSIGNED NOT NULL,
`date` int(11) NOT NULL DEFAULT '0',
`indexVar` int(11) NOT NULL,
`value` float NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Data:
exemple
Working query:
SELECT
v.date,
v.value
FROM
`values` AS v
WHERE
v.date > 1548460800 AND v.indexVar = 6 OR v.indexVar = 2
expected result
Infinite loading query:
SELECT DISTINCT
v.date,
v1.value,
v2.value
FROM
`values` AS v,
`values` AS v1,
`values` AS v2
WHERE
v.date > 1548460800 AND v1.indexVar = 6 AND v2.indexVar = 2
expected result
You aren't including any join conditions in your query.
If the values table has 1 million rows, then including it twice gives you a result set with 1 million * 1 million = 1 trillion rows. You are applying some conditions, but you're still going to wind up with a huge number of results. (And you're including the values table three times!)
Let's say you have a table with a million rows, and each row is just an integer from 1 to 1 million. If you do select value from values where value > 900000 then you'll get 100,000 rows. But if you say select value from values v, values v2 where v.value > 900000 then for each of 100,000 rows matched by the v.value > 900000 condition you'll get all million rows from v2. Even if you apply the same filter to v2 (i.e., v2.value > 900000) the query will still return 100,000 v2 rows for each row in the original values table--10 billion rows in all.
If date is the primary key of the table, then you must make sure that all the date values in each result row are the same:
select v.date, v1.value, v2.value
from values v, values v1, values v2
where v.date = v1.date and v.date = v2.date
and v1.indexVar = 6 and v2.indexVar = 2
or better yet:
select v.date, v1.value, v2.value
from values v
inner join values v1 on (v1.date = v.date)
inner join values v2 on (v2.date = v.date)
where v1.indexVar = 6 and v2.indexVar = 2
If the primary key is id then just do the same with id. (You said you wanted to align rows based on the date, so not sure which column is most significant.)
You could try using a fake aggregation function and group for reduce the result of a case when for filter
SELECT date
, max(case when v.indexVar = 6 then v.values end) v1_value
, max(case when v.indexVar = 2 then v.values end) v2_value
FROM values
WHERE
date > 1548460800 AND indexVar = 6 OR indexVar = 2
group by date
you should also add a proper composite index
create index idx1 on values ( indexVar, date)

First sort rows then add temporary auto-increment column in MySQL

What I like to do:
First: sort columns according to a rule.
Second: add an auto-increment column, so that each row will have correct sequential numbering.
Issues so far:
My code below can only create the auto-increment column at first and only then it will start sorting. This leads to non-sequential numbering within the auto-increment column.
Code:
SET #i:=0;
SELECT
#i:=#i+1 AS autoincr_id,
billings.id AS bill_id,
daily_reports.id AS report_id,
billings.billingDate AS billing_date
FROM lineitems
INNER JOIN billings
ON billings.order_id=lineitems.orderID
INNER JOIN daily_reports
ON billings.`billingDate` BETWEEN DATE_ADD(daily_reports.`referenceDate`, INTERVAL 7 HOUR ) AND DATE_ADD(daily_reports.`referenceDate`, INTERVAL 31 HOUR )
ORDER BY billings.id, autoincr_id
LIMIT 200
see pic that illustrates my issue
Guess using a delivered table to enforce the sorting then use the variable will fix your problem.
You can also use a CROSS JOIN to init the user variable #i without using two queries.
Query
SELECT
#i := #i + 1 AS autoincr_id,
bill_id,
report_id,
billing_date
FROM (
SELECT
billings.id AS bill_id,
daily_reports.id AS report_id,
billings.billingDate AS billing_date
FROM lineitems
INNER JOIN billings
ON billings.order_id=lineitems.orderID
INNER JOIN daily_reports
ON billings.`billingDate` BETWEEN DATE_ADD(daily_reports.`referenceDate`, INTERVAL 7 HOUR ) AND DATE_ADD(daily_reports.`referenceDate`, INTERVAL 31 HOUR )
ORDER BY billings.id
LIMIT 200
)
AS ordered
CROSS JOIN ( SELECT #i := 0 ) AS init_user_param
To create a new table with the rows numbered in the desired order seq_num:
CREATE TABLE new (
seq_num INT UNSIGNED AUTO_INCREMENT,
PRIMARY KEY seq_num
) ENGINE=InnoDB
SELECT ...
ORDER BY ...
Caveat: AUTO_INCREMENT will not have consecutive ids if you are in a multi-Master replication setup. This is because of auto_increment_increment won't be 1.

MySQL get records beetween tables with conditions

I've got a big problem in my hands, I have the following SQL structure, where the contracts tables are dinamically generated, with random names, like _xyz, _xxx, etc:
CREATE TABLE contract_xyz(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL
);
CREATE TABLE contract_events(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
id_contract INT(11) NOT NULL,
table_contract VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL
);
INSERT INTO contract_xyz (id,created_at) VALUES (1,'2016-11-01');
INSERT INTO contract_xyz (id,created_at) VALUES (2,'2016-10-21');
INSERT INTO contract_xyz (id,created_at) VALUES (3,'2016-11-04');
INSERT INTO contract_events(id,id_contract,table_contract,created_at) VALUES (1,1,'contract_xyz','2016-11-03');
INSERT INTO contract_events(id,id_contract,table_contract,created_at) VALUES (2,3,'contract_xyz','2016-11-04');
Each contract can have his own events. I need to solve the following issue:
Get all contracts that don't have new events in 2 days, or don't have any event at all, and was created over 2 days ago.
I've tried with LET JOIN but it wasn't the correct result. The nearest I get was the following query:
SELECT `contract_xyz`.*
FROM `contract_xyz`
WHERE EXISTS(SELECT 1
FROM `contract_events`
WHERE
`contract_events`.id_contract = `contract_xyz`.id AND `contract_events`.table_contract = 'contract_xyz'
AND DATEDIFF(CURDATE(), `contract_events`.created_at) >= 2
ORDER BY `contract_events`.created_at DESC
LIMIT 1)
OR (NOT EXISTS(SELECT 1
FROM `contract_events`
WHERE `contract_events`.id_contract = `contract_xyz`.id AND
`contract_events`.table_contract = 'contract_xyz') AND
DATEDIFF(CURDATE(), `contract_xyz`.created_at) >= 2);
But I still can't find the contracts that doesn't have any events, and was created over 2 days ago.
I would create a subquery with the max event date for each contract. I would left join the contracts table on this subquery. You can filter based on the max event date and the created date fields to achieve the expected outcome:
select c.*
from contract_xyz c
left join
(select id_contract,
max(created_at) max_event_date
from contract_events
group by id_contract) t on c.id-t.id_contract
where
DATEDIFF(CURDATE(), t.max_event_date) >= 2
or (t.max_event_date is null and DATEDIFF(CURDATE(), c.created_at) >= 2)
Alternatively, you do not use a subquery, but join the 2 tables directly with group by and do the filtering in the having clause.
LEFT OUTER JOIN with an ON condition could help here:
select c.id, c.created_at,count(e.id) as contract_events_less_than_2_days_old
from contract_xyz c
left outer join contract_events e on e.id_contract = c.id
and e.table_contract = 'contract_xyz'
and e.created_at > now() - interval 2 day
where c.created_at < now() - interval 2 day
and e.id is null
group by c.id, c.created_at;
If you have any control over it I would advise against dynamically-generated table names!

How to bypass a reference to an outer table in subquery?

I've been dealing with these two tables:
Document
id company_id etc
=======================
1 2 x
2 2 x
Version
id document_id version date_created date_issued date_accepted
==========================================================================
1 1 1 2013-04-29 2013-04-30 NULL
2 2 1 2013-05-01 NULL NULL
3 1 2 2013-05-01 2013-05-01 2013-05-03
There's a page where I want to list all documents with their attributes.
And I would like to add a single have status from each document.
The status can be derived from the most present date that corresponding Versions have.
It is possible that an older version is being accepted.
The query result I am looking for is like this:
id company_id etc status
==================================
1 2 x accepted
2 2 x created
I started out by making a query which combines all dates and add a status next to it.
It works as expected and when I add the document_id things look alright.
SELECT `status`
FROM (
SELECT max(date_created) as `date`,'created' as `status` FROM version WHERE document_id = 1
UNION
SELECT max(date_issued),'issued' FROM version WHERE document_id = 1
UNION
SELECT max(date_accepted),'accepted' FROM version WHERE document_id = 1
ORDER BY date DESC
LIMIT 1
) as maxi
When I try to incorporate this query as a subquery, I can't make it work.
SELECT *, (
SELECT `status` FROM (
SELECT max(date_created) as `date`,'created' as `status`FROM version WHERE document_id = document.id
UNION
SELECT max(date_issued),'issued' FROM version WHERE document_id = document.id
UNION
SELECT max(date_accepted),'accepted' FROM version WHERE document_id = document.id
ORDER BY date DESC
LIMIT 1
) as maxi
) as `status`
FROM `document`
This will get me the error Unknown column 'document.id' in 'where clause'. So I've read around at SO and figured it simply can't reach the value offer.id since it's a subquery in a subquery. So I tried to take another approach and get all the statuses at once, to avoid the WHERE statement, and JOIN them. I ended up with the next query.
SELECT MAX(`date`),`status`, document_id
FROM (
SELECT datetime_created as `date`, 'created' as `status`,document_id FROM `version`
UNION
SELECT datetime_issued, 'issued',document_id FROM `version`
UNION
SELECT datetime_accepted, 'accepted',document_id FROM `version`
) as dates
GROUP BY offer_id
No error this time but I realized that the status couldn't be the correct one since it got lost during the GROUP BY. I've tried combinations of the two but both flaws keep hindering me. Could any one suggest how to do this in a single query without changing my database? (I know that saving the dates in a separate table would simply things)
I have not tested this, but you can do it like this (you might need to tweak the details)
It is basically looking at it from a completely different angle.
select
d.*,
(CASE GREATEST(ifnull(v.date_created, 0), ifnull(v.date_issued,0), ifnull(v.date_accepted,0) )
WHEN null THEN 'unknown'
WHEN v.date_accepted THEN 'accepted'
WHEN v.date_issued THEN 'issued'
WHEN v.date_created THEN 'created'
END) as status
from document d
left join version v on
v.document_id = d.document_id and
not exists (select 1 from (select * from version) x where x.document_id = v.document_id and x.id <> v.id and x.version > v.version)
Can you normalise your table designs to move the status / dates onto a different table from the Versions?
If no possibly something like this:-
SELECT Document.id, Document.company_id, Document.etc, CASE WHEN Sub1.status = 3 THEN 'accepted' WHEN Sub1.status = 2 THEN 'issued' WHEN Sub1.status = 1 THEN 'created' ELSE NULL END AS status
FROM Document
INNER JOIN (
SELECT document_id, MAX(CASE WHEN date_accepted IS NOT NULL THEN 3 WHEN date_issued IS NOT NULL THEN 2 WHEN date_created IS NOT NULL THEN 1 ELSE NULL END) AS status
FROM Version
GROUP BY document_id
) Sub1
ON Document.id = Sub1.document_id
The subselect gets the highest status for any document from the version table. Each possible versions highest status is returned as a number, and by grouping that on the document id it will get the highest status of any version. This is joined back against the Document table and the number for the version number converted into the text description.
select Doc.document_id,Doc.company_id,Doc.etc,f.status
from Document Doc
inner join
(select Ver.document_id,
case when Ver.date_accepted is not null then 'Accepted'
when Ver.date_issued is not null then 'Issued'
when Ver.date_created is not null then 'Created'
end as status
from version Ver
inner join (
select document_id,MAX(version) VersionId
from version
group by document_id
)t on t.document_id=Ver.document_id
where t.VersionId=Ver.version
)f on Doc.document_id=f.document_id
SQL Fiddle

mySQL query with date range and selecting only the most recent records for each ID

I have the following table:
Record ID Status Date Timestamp
----------------------------------------------------------
1 1 waiting 2010-02-02 2010-02-02 12:00:00
2 1 finished 2010-02-02 2010-02-02 12:30:00
3 2 waiting 2009-02-02 2009-02-02 12:00:00
I want to get the records where Date is between 2010-01-01 and 2010-03-03.
(this should give me Records 1 and 2)
Further I want to get only the latest (having most recent timestamp) for each ID.
(this should give me Record 2 only).
I am not sure how I need to structure my query. I have managed to build the following query:
SELECT `Record` `ID`, MAX( `Timestamp` )
FROM `myTable`
WHERE `Date`
BETWEEN '2010-01-01'
AND '2011-03-03'
GROUP BY `ID`
The problem with the above query is that for some reason it is giving me the following result:
Record ID Timestamp
---------------------------------
1 1 2010-02-02 12:30:00
which is correct except that the Record field should have the value 2 and not 1.
SELECT t1.*
FROM `myTable` t1
INNER JOIN
(
SELECT MAX(`Timestamp`) as Timestamp
FROM `myTable`
WHERE `Date`
BETWEEN '2010-01-01'
AND '2011-03-03'
GROUP BY `ID`
) t2
ON t1.Timestamp = t2.Timestamp
Try the below query
SELECT `Record` `ID`,Timestamp
FROM `myTable`
WHERE `Date`
BETWEEN '2010-01-01'
AND '2011-03-03'
Order by `Timestamp` desc
limit 1