I've written a stored procedure to iterate over every week for three years. It doesn't work though and returns a vague error message.
#1064 - You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '' at line 18
DELIMITER $$
CREATE PROCEDURE loop_three_years()
BEGIN
declare y INT default 2016;
declare m int default 4;
declare d int default 20;
WHILE y <= 2019 DO
WHILE YEARWEEK(concat(y, '-', m, '-', d)) <= 53 DO
WHILE m < 12 DO
WHILE (m = 2 and d <= 29) OR (d <=30 and m in(4, 6,9,11)) OR ( m in(1,3,5,7,8,10,12) AND d <= 31) DO
set d = d + 7;
SELECT YEARWEEK(concat(y, '-', m, '-', d));
END WHILE;
set d=1;
END WHILE;
set m = 1;
SET y = y + 1;
END WHILE;
END
$$
When I used this as minimal parts they work so I'm not sure what the issue is with my reassembly. Also not sure if there's a better way to do this. (The select is just for testing, it will be an insert when I use the real code.
Slightly Altered from a previous solution
You can build your own dynamic calendar / list using ANY other table in your system that has at least as many records as you need to fake row numbers. The query below will use MySQL # variables which work like an inline program and declaration. I can start the list with a given date... such as your 2016-04-20 and then each iteration through, add 1 week using date-based functions. No need for me to know or care about how many days have a 28, 29(leap-year), 30 or 31 days.
The table reference below of "AnyTableThatHasAtLeast156Records" is just that.. Any table in your database that has at least 156 records (52 weeks per year, 3 years)
select
YEARWEEK( #startDate ) WeekNum,
#startDate as StartOfWeek,
#startDate := date_add( #startDate, interval 1 week ) EndOfWeek
from
( select #startDate := '2016-04-20') sqlv,
AnyTableThatHasAtLeast156Records
limit
156
This will give you a list of 156 records (provided your "anyTable…" has 156 records all at once. If you need to join this to some other transaction table, you could do so by making the above a JOIN table. Benefit here, Since I included the begin date and end of week, those can be part of your joining to table.
Example, on
record WeekNum StartOfWeek EndOfWeek
1 ?? 2016-04-20 2016-04-27
2 ?? 2016-04-27 2016-05-04
3 ?? 2016-05-04 2016-05-11
4 ?? 2016-04-11 2016-05-18... etc
By adding 1 week to the starting point, you can see that it would do Ex: Monday to Monday. And the JOIN Condition below I have LESS THAN the EndOfWeek. This would account for any transactions UP TO but not including the ending date... such as transactions on 2016-04-26 11:59:59PM (hence LESS than 2016-04-27, as 04/27 is the beginning of the next week's cycle of transactions)
select
Cal.WeekNum,
YT.YourColumns
from
YourTransactionTable YT
JOIN ( aboveCalendarQuery ) Cal
on YT.TransactionDate >= Cal.StartOfWeek
AND YT.TransactionDate < Cal.EndOfWeek
where
whatever else
You could even do sum() with group by such as by WeekNum if that is what you intend.
Hopefully this is a much more accurate and efficient way to build out your calendar to run with and linking to transactions if you so needed to.
Response from comment.
You could by doing a join to a ( select 1 union select 2 union … select 156 ), but your choice. The ONLY reason for the "AnyTable…" is I am sure with any reasonable database with transactions you would have 156 records or more easily. It's sole purpose is to just allow a row for cycling through the iterations to dynamically create the rows.
Also much more sound than the looping mechanism you have run into to begin with. Nothing wrong with that, especially learning purposes, but if more efficient ways, doesn't that make more sense?
Per feedback from comment
I dont exactly know your other table you are trying to insert into, but yes, you can use this for all 3000 things. Provide more of what you are trying to do and I can adjust... In the mean-time, something like this...
insert into YourOtherTable
( someField,
AnotherField,
WeekNum
)
select
x.someField,
x.AnotherField,
z.WeekNum
from
Your3000ThingTable x
JOIN (select
YEARWEEK( #startDate ) WeekNum,
#startDate as StartOfWeek,
#startDate := date_add( #startDate, interval 1 week ) EndOfWeek
from
( select #startDate := '2016-04-20') sqlv,
AnyTableThatHasAtLeast156Records
limit
156 ) z
on 1=1
where
x.SomeCodition...
By joining the the select of 156 records on 1=1 (which is always true), it will return 156 entries for whatever record is in the Your3000ThingTable. So, if you have an inventory item table with
Item Name
1 Thing1
2 Thing2
3 Thing3
Your final insert would be
Item Name WeekNum
1 Thing1 1
1 Thing1 2
1 Thing1 ...
1 Thing1 156
2 Thing2 1
2 Thing2 2
2 Thing2 ...
2 Thing2 156
3 Thing3 1
3 Thing3 2
3 Thing3 ...
3 Thing3 156
And to pre-confirm what you THINK would happen, just try the select/join on 1=1 and you'll see all the records the query WOULD be inserting into your destination table.
Related
I use the following query to calculate the age of people from their dob and then group the ages in ten year intervals to make a frequency chart.
I'd prefer the user to be able to choose the bin size instead of always having to use 10 years, eg maybe group the ages in 5 year intervals, 20 year intervals or any arbitrary range.
How should I re-write the query so that the bin size (currently 10 years) can be passed as a parameter or maybe picked up from another table that is pre-populated with the bins just before running the query?
Obviously I won't be able to use a hard coded CASE in the same way, if at all. Can it be done?
SELECT
CASE
WHEN age = -1 THEN 0 -- null dob
WHEN age >= 0 AND age < 11 THEN 1
WHEN age >= 11 AND age < 21 THEN 2
WHEN age >= 21 AND age < 31 THEN 3
WHEN age >= 31 AND age < 41 THEN 4
WHEN age >= 41 AND age < 51 THEN 5
WHEN age >= 51 AND age < 61 THEN 6
WHEN age >= 61 AND age < 71 THEN 7
WHEN age >= 71 AND age < 81 THEN 8
WHEN age >= 81 AND age < 91 THEN 9
WHEN age >= 91 AND age < 101 THEN 10
WHEN age > 100 THEN 11
END AS Age_Group,
COUNT(age) AS Number_In_Group
FROM
-- this sub query calculates the age from the dob
-- returning -1 if dob is null
(SELECT
IFNULL(
DATE_FORMAT(NOW(), '%Y')
- DATE_FORMAT(dob, '%Y')
- (DATE_FORMAT(NOW(), '00-%m-%d') < DATE_FORMAT(dob, '00-%m-%d'))
, - 1
) AS age
FROM
people
) AS table_age
GROUP BY Age_Group
This produces the following typical output
Age_Group Number_In_Group
0 55
2 1
3 37
4 47
5 51
6 112
7 139
8 70
9 30
10 6
I think I may have solved this myself so am posting my answer in case it helps anyone else. (or if somebody has a better way!)
The trick seems to be to use a stored procedure (so that the bin increment can be passed in as a parameter).
The procedure first makes a temp table for the bins and then populates it in a while loop using the desired bin_size parameter to set the minimum and maximum ages for each bin.
Finally it does a normal grouped count, grouping on the bin label by joining the table with date of births in it (from which it calculates ages) to the temp table with the frequency bins in it. A left join is used to ensure that all bin labels get returned, even if the count is zero.
Seems to run surprisingly fast as well, eg 0.03s using 1000 dates of birth and a bin frequency of 15 years.
usage:
eg to get a frequency table in steps of 15 years call this procedure using
CALL age_frequency_count(15);
This is the code
DELIMITER $$
CREATE PROCEDURE age_frequency_count(IN bin_size INT)
BEGIN
DECLARE bin_min_age INT; -- minimum age for bin
DECLARE bin_max_age INT; -- maximum age for bin
DECLARE bin_label VARCHAR(8); -- label for bin
-- #########################################
-- make a temporary table for the bins if it doesn't exist
CREATE TEMPORARY TABLE IF NOT EXISTS tbl_bins
( minage INT, maxage INT, agegroup VARCHAR(30) ) ;
-- #########################################
-- empty it, in case it did already exist
DELETE FROM tbl_bins;
-- #########################################
-- loop round, populating temp table using bin_size up to around 100 yrs old
SET bin_min_age = 0;
WHILE (bin_min_age + bin_size) < 100 DO
SET bin_max_age = bin_min_age + bin_size ;
SET bin_label = CONCAT(bin_min_age ,' to ' , bin_max_age);
INSERT INTO tbl_bins VALUES (bin_min_age, bin_max_age, bin_label);
SET bin_min_age = bin_max_age;
END WHILE;
-- now insert bin for any age above around 100 yrs old (up to 200 yrs old)
INSERT INTO tbl_bins VALUES (bin_min_age, 200, CONCAT('over ', bin_max_age));
-- and a bin for any ages of -1 that were generated due to a null dob
INSERT INTO tbl_bins VALUES (-1, -1, 'unknown');
-- #########################################
-- finally select the age counts grouped into bins by joining the 'people'
-- table containing the dob to the bins table we just made and populated
SELECT
agegroup,
COUNT(age) AS NumberInGroup
FROM
tbl_bins
LEFT JOIN -- LEFT so that we will still get zero counts if necessary
(SELECT -- next few lines calculate the age from the dob in the people table
IFNULL(
DATE_FORMAT(NOW(), '%Y')
- DATE_FORMAT(member_dob, '%Y')
- (DATE_FORMAT(NOW(), '00-%m-%d') < DATE_FORMAT(member_dob, '00-%m-%d') )
, - 1 -- if dob is null set the age to -1
) AS age
FROM people
) AS tbl_ages
ON
(tbl_ages.age > tbl_bins.minage AND tbl_ages.age <= tbl_bins.maxage) -- normal bin
OR (tbl_ages.age = tbl_bins.maxage) -- to account for age of -1
GROUP BY agegroup;
-- #########################################
END$$
DELIMITER ;
I'd like to retrieve the activity of a user from the last 7 days. The table I'm querying is a many-to-many, linking users, events and dates.
If today is the 7th of May, but the user only has activity recorded for the 4th and 2nd of May, can I build a query that will return a NULL corresponding with the dates of no activity recorded?
Source
May 2 42
May 4 88
Desired Output
May 1 NULL
May 2 42
May 3 NULL
May 4 88
May 5 NULL
May 6 NULL
May 7 NULL
How can this be done in MySQL?
SELECT DISTINCT users, events, dates
FROM table
WHERE dates between date_format(date1, '%Y-%m-%d')
and date_format(date2, '%Y-%m-%d') or NOW() or CURDATE()....
and user = 'username'
This is pretty general, but if you replace the names and the dates (probably don't need to format for your purposes), this will return all results regardless of null place holders
You could populate a date_reference_table...here is the stored proc for this:
I use a date format to only recognize the year and month, but you can alter this...
You would also need an inner join then... so you would do an inner join on the date_reference table and it will spit out the results as mentioned in the comment above.
delimiter $$
CREATE PROCEDURE `generate_date_reference_table`(d1 date, d2 date)
BEGIN
declare d datetime;
create table BLMBP.date_reference (d char(7) not null);
set d = d1;
while d <= d2 do
insert into BLMBP.date_reference (d) values (date_format(d, '%Y-%m'));
set d = date_add(d, interval 1 month);
end while;
END$$
I'm looking to create the following chart from a MySQL database. I know how to actually create the chart (using excel or similar program), my problem is how to get the data needed to create the chart. In this example, I can see that on January 1, 60 tickets were in the state illustrated by the green line.
I need to track the historical state of tickets of a project through a date range. The date range is determined by a project manager (in this case it's January 1st through January 9th).
For each ticket, I have the following set of historical data. Each time something changes in the ticket (state, description, assignee, customer update, and other attributes not shown in this problem), a "timestamp" entry is made in the database.
ticket_num status_changed_date from_state to_state
123456 2011-01-01 18:03:44 -- 1
123456 2011-01-01 18:10:26 1 2
123456 2011-01-01 14:37:10 2 2
123456 2011-01-02 07:55:44 2 3
123456 2011-01-03 06:12:18 3 2
123456 2011-01-04 19:03:43 3 3
123456 2011-01-05 02:05:24 3 4
123456 2011-01-06 18:13:28 4 4
123456 2011-01-07 13:14:48 4 5
123456 2011-01-09 01:35:39 5 5
How can I query the database for a given time (determined by my script) and find out what state each of the tickets are in?
For example: To produce the chart shown above, given the date 2011-01-02 12:00:00, how many tickets were in the state "2"?
I've tried querying the database with specific dates and ranges, but can't figure out the proper way to get the data to create the chart. Thanks in advance for any help.
I'm not exactly sure I know what you want. But . . .
Assuming a table definition like:
create table ticket_data (ticket_num int,
status_changed_date datetime,
from_state int,
to_state int);
The following, for example would give you the number of values per day:
select date(status_changed_date) as status_date, count(*)
from ticket_data
group by status_date;
Now, if you want just from_state = 2, just add a where clause in to that effect. If you want just the ones on Jan 2, then add in where date(status_changed_date) = '2011-01-02'
Or, if you you're looking for the distinct number of tickets per day then, change count(*) to count(distinct ticket_num)
Is this what you're asking? SQL Fiddle here
Ok so if you are trying to get a count of records in a certain state at a certain time, I think a stored proc might be necessary.
CREATE PROCEDURE spStatesAtDate
#Date datetime,
#StateId int
AS
BEGIN
SET NOCOUNT ON;
SELECT COUNT(*) as Count
FROM ticket_table t1
WHERE to_state = #StateId AND status_changed_date < #Date
AND status_changed_date = (SELECT MAX(status_changed_date) FROM ticket_table t2 where t2.ticket_num=t1.ticket_num AND status_changed_date < #Date)
END
then to call this for the above example, you're query would look like
EXEC spStatesAtDate #Date='2011-01-02 12:00:00', #StateId=2
You can use a subquery to select the last modification date before a given point grouped by ticket_num and then select the states at this time.
SELECT
ticket_num,
to_state,
status_changed_date
FROM
tickets
WHERE
status_changed_date IN (
SELECT MAX(status_changed_date)
FROM tickets
WHERE status_changed_date < '2012-02-01 01:00:00'
GROUP BY ticket_num
)
It all boils down to common question: how to get list of items and their most recent statuses. So. Given one issue, we can get its most recent status with query:
select to_state
from ticket_states
where ticket_num = t.ticket_num
order by status_changed_date desc
limit 1
Next, we need to get all applicable distinct issue ids, which is a simple distinct select:
select distinct ticket_num from ticket_states
With these two subqueries we can already start building. For example, current list of issues and their latest statuses before specified date would be:
select t.ticket_num
, (select to_state
from ticket_states
where ticket_num = t.ticket_num
and status_changed_date <= '2012-01-01'
order by status_changed_date desc
limit 1) as last_state
from (select distinct ticket_num
from ticket_states) t;
All issues, which were non-existant at at the specified time will have last_state set to null.
This probably isn't the best way of doing this, but it is first which came to mind. I'll leave other stuff to you. Also I should mention that this is not a very efficient solution also.
I have a table
tbl_charge
id hotel_id start_date end_date charge_per_day ( in $)
1 6 2012-02-15 2010-02-15 20
2 6 2012-02-16 2010-02-18 30
4 6 2012-02-20 2010-02-25 50
Note: if any date is not in the table then we set 25$ for each days (i.e. default charge)
now if someone wants to book a hotel from 2012-02-15 to 2012-02-22 , then I want to calculate the total charges for dates
Date : 15+16+17+18+19+20+21+22
Charge : 20+30+30+30+25+50+50+50 = 285$
what i have done so far:
this query returns all rows successfully
SELECT * FROM `tbl_charge` WHERE
start_date BETWEEN '2012-02-15' AND '2012-02-22' OR
end_date BETWEEN '2012-02-15' AND '2012-02-22' OR
( start_date <'2012-02-15' AND end_date > '2012-02-22')
HAVING property_id=6
it returns all necessary rows but how do I sum the charges??
is ther any way to count days between given date range like last row is 20 -25 but i want only upto 22 then it return 3 days and we multiply charges by 3
is it good to create procedure for this or use simple query
I think this will do the trick:
select sum(DayDifference * charge_per_day) +
(RealDayDifference - sum(DayDifference)) * 25 as TotalPerPeriod
from (
select charge_per_day, datediff(
least(end_date, '2012-02-22'),
greatest(start_date, '2012-02-15')) + 1 as DayDifference,
datediff('2012-02-22', '2012-02-15') + 1 as RealDayDifference
from t1
where
((start_date between '2012-02-15' and '2012-02-22') or
(end_date between '2012-02-15' and '2012-02-22') or
(start_date < '2012-02-15' and end_date > '2012-02-22'))
and hotel_id=6
) S1
I've had to solve this same issue previously and it's a fun one, however since then I've learnt some better methods. At the time I believe I created a procedure or function to loop over the requested dates and return a price.
To return the required rows, you can simply select using the upper and lower limits. You can do a datediff within the select criteria to return the number of iterations of each to apply.
If all you are ultimately looking for is a single price I would advise combining this logic into a function
I've assumed a second table, tbh_hotel with id (int PK == hotel_id) and default_charge (int) with row (id=6,default_charge=20)
Further assumptions are that where your dates are "2010" you meant them to be "2012", and that this is for someone that is checking in in the 15th, and checking out on the 22nd (and so needs a hotel for 15th, 16th, 17th, 18th, 19th, 20th, 21st, 7 nights). I will also assume that you have logic in place that prevents the date ranges overlapping, so that there are no 2 rows in tbl_charge which match the date 14th Feb 2012 (for example)
So to get this started, a query to select the applicable rows
SELECT
*
FROM tbl_charge AS c
WHERE
(
c.end_date >= '2012-02-15'
OR
c.start_date < '2012-02-22'
)
This is pretty much what you have already, so now will add in some more fields to get the information for how many days each rule is applied for.
SET #StartDate = '2012-02-15';
SET #EndDate = SUBDATE('2012-02-22',1);
SELECT
c.id,
c.start_date,
c.end_date,
c.charge_per_day,
DATEDIFF(IF(c.end_date>#EndDate,#EndDate,c.end_date),SUBDATE(IF(c.start_date<#StartDate,#StartDate,c.start_date),1)) AS quantityOfThisRate
FROM tbl_charge AS c
WHERE c.end_date >=#StartDate OR c.start_date < #EndDate
I am SUBDATEing the end date, because if you check out on the 22nd, your final checkin date is the 21st. I am SUBDATING the start date on each DATEDIFF because if you are staying on 15th -> 16th, the subdate on END DATE makes this 15th-15th, and so this SUBDATE makes it get 14th-15th to return the correct value of 1. Output now looks a bit like this
id start_date end_date price quantityAtThisRate
1 2012-02-10 2012-02-15 20 1
2 2012-02-16 2012-02-18 30 3
3 2012-02-20 2012-02-29 50 2
So moving on I'll put this into a subquery and combine tbl_hotel to get a default charge
SET #StartDate = '2012-02-15';
SET #EndDate = SUBDATE('2012-02-22',1);
SET #NumberOfNights = DATEDIFF(ADDDATE(#EndDate,1),#StartDate);
SET #HotelID = 6;
SELECT
SUM(specificDates.charge_per_day*specificDates.quantityAtThisRate) AS specificCharges,
#NumberOfNights-SUM(specificDates.quantityAtThisRate) AS daysAtDefault,
h.default_charge * (#NumberOfNights-SUM(specificDates.quantityAtThisRate)) AS defaultCharges
FROM tbl_hotel AS h
INNER JOIN
(
SELECT
c.charge_per_day,
DATEDIFF(IF(c.end_date>#EndDate,#EndDate,c.end_date),SUBDATE(IF(c.start_date<#StartDate,#StartDate,c.start_date),1)) AS quantityAtThisRate
FROM tbl_charge AS c
WHERE (c.end_date >=#StartDate OR c.start_date < #EndDate) AND c.hotel_id = #HotelID
) AS specificDates
WHERE h.id = #HotelID
Realistically a single query will get rather .... complex so I'd settle at a stored procedure relying on the logic above (as if there are no specific rules the above query will return null due to the inner join)
Hope this is of help
I want to get the number of Registrations back from a time period (say a week), which isn't that hard to do, but I was wondering if it is in anyway possible to in MySQL to return a zero for days that have no registrations.
An example:
DATA:
ID_Profile datCreate
1 2009-02-25 16:45:58
2 2009-02-25 16:45:58
3 2009-02-25 16:45:58
4 2009-02-26 10:23:39
5 2009-02-27 15:07:56
6 2009-03-05 11:57:30
SQL:
SELECT
DAY(datCreate) as RegistrationDate,
COUNT(ID_Profile) as NumberOfRegistrations
FROM tbl_profile
WHERE DATE(datCreate) > DATE_SUB(CURDATE(),INTERVAL 9 DAY)
GROUP BY RegistrationDate
ORDER BY datCreate ASC;
In this case the result would be:
RegistrationDate NumberOfRegistrations
25 3
26 1
27 1
5 1
Obviously I'm missing a couple of days in between. Currently I'm solving this in my php code, but I was wondering if MySQL has any way to automatically return 0 for the missing days/rows. This would be the desired result:
RegistrationDate NumberOfRegistrations
25 3
26 1
27 1
28 0
1 0
2 0
3 0
4 0
5 1
This way we can use MySQL to solve any problems concerning the number of days in a month instead of relying on php code to calculate for each month how many days there are, since MySQL has this functionality build in.
Thanks in advance
No, but one workaround would be to create a single-column table with a date primary key, preloaded with dates for each day. You'd have dates from your earliest starting point right through to some far off future.
Now, you can LEFT JOIN your statistical data against it - then you'll get nulls for those days with no data. If you really want a zero rather than null, use IFNULL(colname, 0)
Thanks to Paul Dixon I found the solution. Anyone interested in how I solved this read on:
First create a stored procedure I found somewhere to populate a table with all dates from this year.
CREATE Table calendar(dt date not null);
CREATE PROCEDURE sp_calendar(IN start_date DATE, IN end_date DATE, OUT result_text TEXT)
BEGIN
SET #begin = 'INSERT INTO calendar(dt) VALUES ';
SET #date = start_date;
SET #max = SUBDATE(end_date, INTERVAL 1 DAY);
SET #temp = '';
REPEAT
SET #temp = concat(#temp, '(''', #date, '''), ');
SET #date = ADDDATE(#date, INTERVAL 1 DAY);
UNTIL #date > #max
END REPEAT;
SET #temp = concat(#temp, '(''', #date, ''')');
SET result_text = concat(#begin, #temp);
END
call sp_calendar('2009-01-01', '2010-01-01', #z);
select #z;
Then change the query to add the left join:
SELECT
DAY(dt) as RegistrationDate,
COUNT(ID_Profile) as NumberOfRegistrations
FROM calendar
LEFT JOIN
tbl_profile ON calendar.dt = tbl_profile.datCreate
WHERE dt BETWEEN DATE_SUB(CURDATE(),INTERVAL 6 DAY) AND CURDATE()
GROUP BY RegistrationDate
ORDER BY dt ASC
And we're done.
Thanks all for the quick replies and solution.