CodeIgniter SQL query - how to sum values for each month? - php

I have the following table:
//table_1
record_id user_id plant_id date cost
1 1 1 2011-03-01 10
2 1 1 2011-03-02 10
3 1 1 2011-04-10 5
4 1 2 2011-04-15 5
I would like to build a query (if possible using CI Active Records, but MySQL is fine) in which I generate the following result:
[1] => [1] => [March 2011] [20]
=> [April 2011] [5]
[2] => [March 2011] [0]
=> [April 2011] [5]
I have tried using $this->db->group_by but I think I'm not using it correctly.
If anyone could give me a pointer or roadmap to get this done it would be much appreciated -- thanks!

Sample table
drop table if exists t;
create table t( record_id int, user_id int, plant_id int, date datetime, cost float);
insert t select
1 ,1, 1 ,'2011-03-01', 10 union all select
2 ,1, 1 ,'2011-03-02', 10 union all select
3 ,1, 1 ,'2011-04-10', 5 union all select
4 ,1, 2 ,'2011-04-15', 5;
Because you want to see the row with 0, you need to do a cross join between the year-month and all user-plants.
select up.user_id, up.plant_id, ym2, ifnull(sum(t.cost),0) totalcost
from (select distinct date_format(date, '%Y-%m') ym, date_format(date, '%M %Y') ym2 from t) dates
cross join (select distinct t.user_id, t.plant_id from t) up
left join t on date_format(t.date, '%Y-%m') = dates.ym
and up.user_id=t.user_id
and up.plant_id=t.plant_id
group by up.user_id, up.plant_id, ym2, ym
order by up.user_id, up.plant_id, date(concat(ym,'-1'));
The fact that Month Year does not sort correctly also requires the complex treatment of the dates for ordering purposes at the end.

This is a pretty intense way to do it and it would be far preferable to store the month-year as a column itself if you need to make these queries frequently, but this is basically how it works:
SELECT CONCAT(MONTHNAME(date), ' ', YEAR(date)) AS monthyear, COUNT(*) AS count GROUP BY YEAR(date), MONTH(date), plant_id;
That should get you the resultset you're looking for.

Related

MySql - Update the status of rows using FIFO logic

I've the below mysql table where I want to update multiple rows of approval_status field with 1 if the quantity passed is more than the qty_req value using MySql or PHP.
Requisition table:
id
part_id
qty_req
approval_status
1
16
20
0
2
17
30
0
3
16
40
0
4
17
50
0
5
17
60
0
Example:
$update_status=Array (
[0] => Array ( [part_id] => 17 [qty] => 90 )
[1] => Array ( [part_id] => 16 [qty] => 70 )
)
From the above array, 90 is the quantity available for the part_id 17. I want to update the approval_status as 1 in the requisition table for the rows with the part_id as 17 with the below scenario:
Update the approval_status to 1 as the quantity of the first row with part_id 17 is 30 which is less than 90.
Update the approval_status to 1 as the quantity of the second row with part_id 17 is 30+50=80 which is less than 90.
Third row won't update as the total 30+50+60=140 is greater than 90.
Unfortunately, I couldn't find any tutorial to achieve this.
Any help would be appreciated. Thanks in advance.
Single data:
UPDATE test t1
NATURAL JOIN ( SELECT *, SUM(qty_req) OVER (ORDER BY reg_date) cum_sum
FROM test
WHERE part_id = #part_id ) t2
SET t1.approval_status = 1
WHERE cum_sum <= #qty;
Multiple data:
UPDATE test t1
NATURAL JOIN ( SELECT test.*,
SUM(qty_req) OVER (PARTITION BY part_id ORDER BY reg_date) cum_sum,
qty
FROM test
JOIN JSON_TABLE(#json,
'$[*]' COLUMNS ( part_id INT PATH '$.part_id',
qty INT PATH '$.qty')) jsontable USING (part_id) ) t2
SET t1.approval_status = 1
WHERE cum_sum <= qty;
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=7cf062d81a138657dee78f3026623783
Column reg_date and unique index added for to provide definite and unambiguous rows ordering.
The solution applicable to MySQL 5.6:
UPDATE test t1
NATURAL JOIN ( SELECT t1.part_id,
t1.qty_req,
t1.approval_status,
t1.reg_date,
SUM(t2.qty_req) cum_sum
FROM test t1
JOIN test t2 USING (part_id)
WHERE t1.reg_date >= t2.reg_date
GROUP BY t1.part_id,
t1.qty_req,
t1.approval_status,
t1.reg_date ) t2
JOIN ( SELECT 17 part_id, 90 qty
UNION ALL
SELECT 16, 50 ) t3 USING (part_id)
SET t1.approval_status = 1
WHERE t2.cum_sum <= t3.qty
https://dbfiddle.uk/?rdbms=mysql_5.6&fiddle=0aad358941f66d1f385799072237d513
Source data must be formed as a query text - see subquery t3.

MySql - count from beginning until each day

Let's say I have the following table (keep in mind that this table will have 10000+ rows):
id total date
1 5 2015-05-16
2 8 2015-05-17
3 4 2015-05-18
4 9 2015-05-19
5 3 2015-05-20
I want the query to give the following result:
1
date => 2015-05-16
total => 5
2
date => 2015-05-17
total => 13
3
date => 2015-05-18
total => 17
4
date => 2015-05-19
total => 26
5
date => 2015-05-20
total -> 29
I can't think of any query that would do this right now, that's why I am not providing any code that I have tried.
Any thoughts? I am not sure if this is possible only with mysql, maybe I have to use and php.
This could be done using user defined variable in mysql and then get the running total as
select
id,
total,
date
date from
(
select
id,
#tot:= #tot+total as total,
date from my_table,(select #tot:=0)x
order by date
)x
You can do this -
SELECT
a.id,
a.date,
(SELECT SUM(b.total) FROM your_table WHERE b.date <= a.date) as new_total
FROM your_table a, your_table b
ORDER BY a.date ASC
This should do it:
select id, (select sum(total) from table a where a.date <= b.date) from table b

MySQL secondary query is too performance intensive

I have a query where I want to pull in ID's from another table based on the ID of the item selected in another table. I'm currently doing this with an additional query based on the results that I get from the main query. It's resulting in many many additional queries. Is there a way to condense this into 1 query?
SELECT music.id,
SUM(linked_tags.weight) AS total_weight
FROM (music)
INNER JOIN linked_tags ON linked_tags.track_id = music.id
AND linked_tags.tag_id IN (7,56,59)
GROUP BY music.id
ORDER BY total_weight DESC
Then the additional query comes from running the results from the main query through a foreach loop, where 2713 is the ID of an item in the music table.
SELECT tag_id,
weight
FROM (linked_tags)
JOIN tags_en ON tags_en.id = linked_tags.tag_id
WHERE track_id = '2713'
This results in this object, where all_tags is the data that comes from the 2nd query:
[id] => 1500
[name] => Some Track Name
[total_weight] => 10
[all_tags] => Array
(
[0] => 20
[1] => 28
[2] => 4
[3] => 13
[4] => 16
[5] => 7
[6] => 42
[7] => 56
[8] => 61
)
Is there a way to pull this all into 1 query?
You can combine them directly using join:
select tag_id, weight
from (SELECT music.id,
SUM(linked_tags.weight) AS total_weight
FROM music join
linked_tags
ON linked_tags.track_id = music.id AND linked_tags.tag_id IN (7,56,59)
GROUP BY music.id
) m join
linked_tags
on m.id = linked_tags.track_id join
tags_en
ON tags_en.id = linked_tags.tag_id;
EDIT:
If I understand the query correctly, you are trying to get all tags on "tracks" (or "music") that have one or more tags in the set of (7,56,59). And, you want to get the sum of the weights of those three tags.
You can do this in one pass, if you don't mind have the tags in a comma-delimited list:
SELECT m.id,
SUM(case when lt.tag_id IN (7,56,59) then lt.weight end) AS total_weight,
sum(lt.tag_id IN (7, 56, 59)) as NumSpecialTags,
group_concat(lt.tag_id) as AllTags
FROM music m join
linked_tags lt
ON lt.track_id = m.id
GROUP BY m.id
having NumSpecialTags > 0
order by total_weight desc;
You then have to parse the AllTags list at the application layer.

sql sort numeric then alphabetically

in this example :
10-20
20-40
50-60
v
k
r
a
12 month
1 month
how can i sort it in this order ?:
10-20
20-40
50-60
a
k
r
v
1 month
12 month
i use abs(value) but in the alphabetical case doesn't work
If you can get away with doing some processing in PHP, you could use natsort:
Standard sorting
Array
(
[3] => img1.png
[1] => img10.png
[0] => img12.png
[2] => img2.png
)
Natural order sorting
Array
(
[3] => img1.png
[2] => img2.png
[1] => img10.png
[0] => img12.png
)
Otherwise, there's another question on SO which asks the same thing: Natural Sort in MySQL
OK, thanks to the commenter, now a working version. This sorts on two cases in the order by clause:
select *
from (
select '10-20' as col1
union all select '20-40'
union all select '50-60'
union all select 'v'
union all select 'k'
union all select 'r'
union all select 'a'
union all select '12 month'
union all select '1 month'
) s1
order by
case
when col1 rlike '[0-9][0-9]-[0-9][0-9]' then 1
when col1 rlike '[0-9]+ month' then 3
else 2
end
, case
when col1 rlike '[0-9][0-9]-[0-9][0-9]' then cast(col1 as decimal)
when col1 rlike '[0-9]+ month' then cast(col1 as decimal)
else col1
end
The first case puts categories in order: 00-00 first, then other stuff, and at the end the months. The second case converts the columns to decimal if possible.

printing restaurant opening hours from a database table in human readable format using php

i have a table that lists the opening hours of restaurants. the columns are id, eateries_id, day_of_week, start_time, and end_time. each eatery is represented in the table multiple times because there is a separate entry for each day. see this previous question for more details:
determine if a restaurant is open now (like yelp does) using database, php, js
i'm wondering now how to take the data from this table and print it out in a human readable format. for example, instead of saying "M 1-3, T 1-3, W 1-3, Th 1-3, F 1-8" i would like to say "M-Th 1-3, F 1-8". similarly, i want "M 1-3, 5-8" instead of "M 1-3, M 5-8". how might i do this without a brute force method of numerous if statements?
thanks.
Thought I would have a bash at this.
Test Table
CREATE TABLE `opening_hours` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`eateries_id` int(11) DEFAULT NULL,
`day_of_week` int(11) DEFAULT NULL,
`start_time` time DEFAULT NULL,
`end_time` time DEFAULT NULL,
PRIMARY KEY (`id`)
)
Test Data
INSERT INTO `test`.`opening_hours`
(
`eateries_id`,
`day_of_week`,
`start_time`,
`end_time`)
SELECT 2 AS eateries_id, 1 AS day_of_week, '13:00' AS start_time, '15:00' as end_time union all
SELECT 2 AS eateries_id, 1 AS day_of_week, '17:00' AS start_time, '20:00' as end_time union all
SELECT 2 AS eateries_id, 2 AS day_of_week, '13:00' AS start_time, '15:00' as end_time union all
SELECT 2 AS eateries_id, 2 AS day_of_week, '17:00' AS start_time, '20:00' as end_time union all
SELECT 2 AS eateries_id, 3 AS day_of_week, '13:00' AS start_time, '15:00' as end_time union all
SELECT 2 AS eateries_id, 4 AS day_of_week, '13:00' AS start_time, '20:00' as end_time union all
SELECT 2 AS eateries_id, 5 AS day_of_week, '13:00' AS start_time, '15:00' as end_time union all
SELECT 2 AS eateries_id, 6 AS day_of_week, '13:00' AS start_time, '20:00' as end_time union all
SELECT 2 AS eateries_id, 7 AS day_of_week, '13:00' AS start_time, '21:00' as end_time
union all
SELECT 3 AS eateries_id, 1 AS day_of_week, '13:00' AS start_time, '15:00' as end_time union all
SELECT 3 AS eateries_id, 2 AS day_of_week, '13:00' AS start_time, '15:00' as end_time union all
SELECT 3 AS eateries_id, 3 AS day_of_week, '13:00' AS start_time, '15:00' as end_time union all
SELECT 3 AS eateries_id, 4 AS day_of_week, '13:00' AS start_time, '20:00' as end_time union all
SELECT 3 AS eateries_id, 5 AS day_of_week, '13:00' AS start_time, '15:00' as end_time union all
SELECT 3 AS eateries_id, 6 AS day_of_week, '13:00' AS start_time, '20:00' as end_time union all
SELECT 3 AS eateries_id, 7 AS day_of_week, '13:00' AS start_time, '21:00' as end_time
View definition to consolidate opening hours by day
CREATE VIEW `test`.`groupedhours`
AS
select `test`.`opening_hours`.`eateries_id` AS `eateries_id`,
`test`.`opening_hours`.`day_of_week` AS `day_of_week`,
group_concat(concat(date_format(`test`.`opening_hours`.`start_time`,'%l'),' - ',date_format(`test`.`opening_hours`.`end_time`,'%l %p')) order by `test`.`opening_hours`.`start_time` ASC separator ', ') AS `OpeningHours`
from `test`.`opening_hours`
group by `test`.`opening_hours`.`eateries_id`,`test`.`opening_hours`.`day_of_week`
Query to find the 'islands' of contiguous days with the same opening hours (based on one by Itzik Ben Gan)
SET #rownum = NULL;
SET #rownum2 = NULL;
SELECT S.eateries_id,
concat(CASE WHEN
S.day_of_week <> E.day_of_week
THEN
CONCAT(CASE S.day_of_week
WHEN 1 THEN 'Su'
WHEN 2 THEN 'Mo'
WHEN 3 THEN 'Tu'
WHEN 4 THEN 'We'
WHEN 5 THEN 'Th'
WHEN 6 THEN 'Fr'
WHEN 7 THEN 'Sa'
End, ' - ')
ELSE ''
END,
CASE E.day_of_week
WHEN 1 THEN 'Su'
WHEN 2 THEN 'Mo'
WHEN 3 THEN 'Tu'
WHEN 4 THEN 'We'
WHEN 5 THEN 'Th'
WHEN 6 THEN 'Fr'
WHEN 7 THEN 'Sa'
End, ' ', S.OpeningHours) AS `Range`
FROM (
SELECT
A.day_of_week,
#rownum := IFNULL(#rownum, 0) + 1 AS rownum,
A.eateries_id,
A.OpeningHours
FROM `test`.`groupedhours` as A
WHERE NOT EXISTS(SELECT * FROM `test`.`groupedhours` B
WHERE A.eateries_id = B.eateries_id
AND A.OpeningHours = B.OpeningHours
AND B.day_of_week = A.day_of_week -1)
ORDER BY eateries_id,day_of_week) AS S
JOIN (
SELECT
A.day_of_week,
#rownum2 := IFNULL(#rownum2, 0) + 1 AS rownum,
A.eateries_id,
A.OpeningHours
FROM `test`.`groupedhours` as A
WHERE NOT EXISTS(SELECT * FROM `test`.`groupedhours` B
WHERE A.eateries_id = B.eateries_id
AND A.OpeningHours = B.OpeningHours
AND B.day_of_week = A.day_of_week + 1)
ORDER BY eateries_id,day_of_week) AS E
ON S.eateries_id = E.eateries_id AND
S.OpeningHours = S.OpeningHours AND
S.rownum = E.rownum
Results
eateries_id Range
2 Su - Mo 1 - 3 PM, 5 - 8 PM
2 Tu 1 - 3 PM
2 We 1 - 8 PM
2 Th 1 - 3 PM
2 Fr 1 - 8 PM
2 Sa 1 - 9 PM
3 Su - Tu 1 - 3 PM
3 We 1 - 8 PM
3 Th 1 - 3 PM
3 Fr 1 - 8 PM
3 Sa 1 - 9 PM
You want to union a bunch of intervals for each day. Stick to 24h format (actually convert it to seconds first I guess) until you have to convert it to a human-friendlier format.
http://pyinterval.googlecode.com/svn/trunk/html/index.html
The trouble is that when you allow seconds ... a restaurant which closes 1 second earlier will be missed :( Perhaps you need to allow 15 or 5 -minute increments. Round the data in DB if you have to. So, the approach is: using an interval data structure, union all intervals for a given day together. Now reverse the dictionary. Instead of mapping days to intervals, map intervals to days. Now find a way to represent those groups of days intelligently. For instance, set(1,2,3) can be displayed as "M-W", so I would suggest: for every power set of the set {1,2,3,4,5,6,7} (or {1,2,3,4,5}) find the best human representation (by hand). Now hard-code this logic -save it into a dictionary which maps a sorted string (this is important) such as "1235" to a human representation such as "M-W,F". Displaying 1-3, 5-8 is easy, once you work with an interval object as is described in the link above. Good luck! Let me know what problems you run into.
EDIT:
This is not the best example that they have (does not show union of overlapping intervals), but you care about the "|" operator
unioned:
>>> interval[1, 4] | interval[2, 5]
interval([1.0, 5.0])
>>> interval[1, 2] | interval[4, 5]
interval([1.0, 2.0], [4.0, 5.0])
You could just implement this class yourself, but it might be prone to bugs.

Categories