SUMs with GROUP WITH ROLLUP - query too slow - mysql

My Query is very slow returning results. It takes more than 30s (depending a lot on the use of the server). The table has some records.
I cannot divide this table for other reasons. I added this to the query `TYPE` IN (1,3)' and the response time has improved significantly. But it remains very slow.
It is worth mentioning that I have already add an INDEX for the PRODUCT, TYPE, STATUS columns.
Question: How can I speed up the query response time without having to divide it by many tabs and have to use the INNER JOIN?
Other Useful Information:
Records: 1,436,004
Record length: 240 Bytes
Records size: 291 Bytes
My Query:
SELECT `PRODUCT`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '11/2020' AND `TYPE` = 1) AS `SUMT1_1`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '12/2020' AND `TYPE` = 1) AS `SUMT1_2`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '01/2021' AND `TYPE` = 1) AS `SUMT1_3`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '02/2021' AND `TYPE` = 1) AS `SUMT1_4`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '03/2021' AND `TYPE` = 1) AS `SUMT1_5`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '04/2021' AND `TYPE` = 1) AS `SUMT1_6`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '11/2020' AND `TYPE` = 3) AS `SUMT3_1`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '12/2020' AND `TYPE` = 3) AS `SUMT3_2`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '01/2021' AND `TYPE` = 3) AS `SUMT3_3`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '02/2021' AND `TYPE` = 3) AS `SUMT3_4`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '03/2021' AND `TYPE` = 3) AS `SUMT3_5`,
SUM(DATE_FORMAT(`REG_DATE`,'%m/%Y') = '04/2021' AND `TYPE` = 3) AS `SUMT3_6`
FROM `MY_TABLE` WHERE
`TYPE` IN (1,3) AND
`STATUS` IN('AVAILABLE','IN PROCESS')
GROUP BY `PRODUCT` ASC
WITH ROLLUP HAVING (`SUMT1_1`+`SUMT1_2`+`SUMT1_3`+`SUMT1_4`+`SUMT1_5`+`SUMT1_6`+`SUMT3_1`+`SUMT3_2`+`SUMT3_3`+`SUMT3_4`+`SUMT3_5`+`SUMT3_6`) >= 1

With an index on some combination of (status,type,reg_date), I would write that query this way:
SELECT product
, DATE_FORMAT(reg_date,'%Y-%m') reg_ym
, type
, COUNT(*) subtotal
FROM my_table
WHERE type IN (1,3)
AND status IN('AVAILABLE','IN PROCESS')
AND reg_date >= '2020-11-01'
AND reg_date < '2020-05-01'
GROUP
BY product
, reg_ym
, type
ORDER
BY product
, reg_ym
, type
...and handle any remaining aspects of the problem in application code

The best solution was given by #Akina (in the comments), and reduced the response time from 30s to 3s (average).
Adding the condition:
`REG_DATE` BETWEEN '20220-11-01' AND '2021-04-30'
But to fit better with my php code i prefer to use:
DATE_FORMAT(`REG_DATE`,'%m/%Y') IN ('11/2020','12/2020','01/2021','02/2021','03/2021','04/2021')

Related

How to get votes with results with percent calculating

In my Laravel 5.7/mysql 5 app I have a table with votes results:
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`vote_item_id` INT(10) UNSIGNED NOT NULL,
`user_id` INT(10) UNSIGNED NOT NULL,
`is_correct` TINYINT(1) NOT NULL DEFAULT '0',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
where boolean is_correct field is if answer was correct or incorrect.
I need to get data on percents of correct answers.
Creating such request
$voteItemUsersResultsCorrect = VoteItemUsersResult:: // Grouped by vote name
getByIsCorrect(true)->
getByCreatedAt($filter_voted_at_from, ' > ')->
getByCreatedAt($filter_voted_at_till, ' <= ')->
getByUserId($filterSelectedUsers)->
getByVote($filterSelectedVotes)->
getByVoteCategories($filterSelectedVoteCategories)->
getByVoteIsQuiz(true)->
getByVoteStatus('A')->
select( \DB::raw('count(vote_item_users_result.id) as count, votes.id, votes.name as vote_name') )->
orderBy('vote_name', 'asc')->
groupBy( 'votes.id' )->
groupBy( 'vote_name' )->
join(\DB::raw('vote_items'), \DB::raw('vote_items.id'), '=', \DB::raw('vote_item_users_result.vote_item_id'))->
join(\DB::raw('votes '), \DB::raw('votes.id'), '=', \DB::raw('vote_items.vote_id'))->
get();
I can get number of correct votes with sql request.
SELECT count(vote_item_users_result.id) AS count, votes.id, votes.name AS vote_name
FROM `vote_item_users_result`
INNER JOIN vote_items on vote_items.id = vote_item_users_result.vote_item_id
INNER JOIN votes on votes.id = vote_items.vote_id
WHERE `vote_item_users_result`.`is_correct` = '1' AND vote_item_users_result.created_at > '2018-08-01' AND vote_item_users_result.created_at <= '2018-09-22 23:59:59' AND `votes`.`is_quiz` = '1' AND `votes`.`status` = 'A'
GROUP BY `votes`.`id`, `vote_name`
ORDER BY `vote_name` asc
I know a way to get 2nd similar request with is_correct = '0' and on php side to combine results with percent calculating,
but I wonder if that could be done with eloquent in 1 request?
If yes, how ?
Thanks!
One correct raw MySQL would use conditional aggregation:
SELECT
v.id,
100.0 * COUNT(CASE WHEN vir.is_correct = 1 THEN 1 END) / COUNT(*) AS pct_correct,
100.0 * COUNT(CASE WHEN vir.is_correct = 0 THEN 1 END) / COUNT(*) AS pct_incorrect
FROM votes v
INNER JOIN vote_items vi
ON v.id = vi.vote_id
INNER JOIN vote_item_users_result vir
ON vi.id = vir.vote_item_id
WHERE
vir.created_at > '2018-08-01' AND vir.created_at < '2018-09-23' AND
v.is_quiz = '1' AND
v.status = 'A'
GROUP BY
v.id;
Now we can try writing Laravel code for this:
DB::table('vote')
->select('vote.id',
DB::raw('100.0 * COUNT(CASE WHEN vir.is_correct = 1 THEN 1 END) / COUNT(*) AS pct_correct'),
DB::raw('100.0 * COUNT(CASE WHEN vir.is_correct = 0 THEN 1 END) / COUNT(*) AS pct_incorrect'))
->join('vote_items', 'votes.id', '=', 'vote_items.vote_id')
->join('vote_item_users_result', 'vote_items.id', '=', 'vote_item_users_result.vote_item_id ')
->where([
['vote_item_users_result.created_at', '>', '2018-08-01'],
['vote_item_users_result.created_at', '<', '2018-09-23'],
['vote.is_quiz', '=', '1'],
['vote.status', '=', 'A']
])
->groupBy('vote.id')
->get();

Return a row with count of 0 when GROUP BY yields no matches

Trying to write a query in MySQL that takes 4 different actions performed by a specific deviceToken and within a specific timeframe and returns the action, the deviceToken, and the count of how many times that action was taken. My problem is, in order to handle the result properly, I always need all 4 actions to be a part of the return, even if the count is 0, and GROUP BY notoriously does not play along with that. Here is the query I have currently:
SELECT `action`,
deviceToken,
CASE WHEN `action` = 'calendar' THEN COUNT(*) END AS CalCount,
CASE WHEN `action` = 'closinginfo' THEN COUNT(*) END AS CloseingCount,
CASE WHEN `action` = 'generalinfo' THEN COUNT(*) END AS GenInfoCount,
CASE WHEN `action` = 'login' THEN COUNT(*) END AS LoginCount
FROM `action`
WHERE deviceToken = '44262f82-3767-49c8-8786-ee377ea00a64'
AND (`action` = 'login' OR `action` = 'calendar' OR `action` = 'closinginfo' OR `action` = 'generalinfo')
AND `timestamp` > CONCAT(DATE(NOW()),' 23:59:59') - INTERVAL DAYOFWEEK(NOW())+14 DAY
AND `timestamp` < CONCAT(DATE(NOW()),' 23:59:59') - INTERVAL DAYOFWEEK(NOW())+7 DAY
GROUP BY `action`
For a deviceToken that has performed all 4 actions within that time frame, the result looks like this:
The way my client handles this result is already perfect. However, a deviceToken that has taken less than all 4 actions looks like this:
What I need to have for my data handling to work universally is those missing rows still returned, but just list those counts as 0. Does anyone have any idea of how to accomplish this?
In mysql boolean expressions resolve to 1 or 0 so you can SUM them:
SELECT
SUM(`action` = 'calendar') AS CalCount,
SUM(`action` = 'closinginfo') AS CloseingCount,
SUM(`action` = 'generalinfo') AS GenInfoCount,
SUM(`action` = 'login') AS LoginCount
FROM `action`
WHERE deviceToken = '44262f82-3767-49c8-8786-ee377ea00a64'
AND (`action` = 'login' OR `action` = 'calendar' OR `action` = 'closinginfo' OR `action` = 'generalinfo')
AND `timestamp` > CONCAT(DATE(NOW()),' 23:59:59') - INTERVAL DAYOFWEEK(NOW())+14 DAY
AND `timestamp` < CONCAT(DATE(NOW()),' 23:59:59') - INTERVAL DAYOFWEEK(NOW())+7 DAY
Note that this query doesn't use a GROUP BY and only aggregates are in the SELECT
Use a LEFT JOIN with a table that lists all the actions to get null values for those rows.
SELECT t1.`action`,
'44262f82-3767-49c8-8786-ee377ea00a64' AS deviceToken,
CASE WHEN t1.`action` = 'calendar' THEN COUNT(a.action) END AS CalCount,
CASE WHEN t1.`action` = 'closinginfo' THEN COUNT(a.action) END AS CloseingCount,
CASE WHEN t1.`action` = 'generalinfo' THEN COUNT(a.action) END AS GenInfoCount,
CASE WHEN t1.`action` = 'login' THEN COUNT(a.action) END AS LoginCount
FROM (SELECT 'calendar' AS action
UNION ALL
SELECT 'closinginfo'
UNION ALL
SELECT 'generalinfo'
UNION ALL
SELECT 'login') AS t1
LEFT JOIN `action` AS a
ON a.action = t1.action
AND deviceToken = '44262f82-3767-49c8-8786-ee377ea00a64'
AND `timestamp` > CONCAT(DATE(NOW()),' 23:59:59') - INTERVAL DAYOFWEEK(NOW())+14 DAY
AND `timestamp` < CONCAT(DATE(NOW()),' 23:59:59') - INTERVAL DAYOFWEEK(NOW())+7 DAY
GROUP BY t1.`action`

How to write condition for subquery alias if having null value

Here is my query,
SELECT
`h`.`hotel_id`,
(
SELECT COUNT(room_id)
FROM
`abserve_hotel_rooms` AS `rm`
WHERE
`rm`.`adults_count` >= "1" AND `rm`.`room_count` >= "1" AND "Available" = IF(
check_in_time = '2016-03-15',
'Unavailable',
(
IF(
'2016-03-15' > check_in_time,
(
IF(
'2016-03-15' < check_out_time,
'Unavailable',
'Available'
)
),
(
IF(
'2016-03-22' > check_in_time,
'Unavailable',
'Available'
)
)
)
)
) AND `room_prize` BETWEEN '174' AND '600' AND `rm`.`hotel_id` = `h`.`hotel_id`
) AS `avail_room_count`,
(
SELECT MIN(room_prize)
FROM
`abserve_hotel_rooms` AS `rm`
WHERE
`rm`.`adults_count` >= "1" AND `rm`.`room_count` >= "1" AND "Available" = IF(
check_in_time = '2016-03-15',
'Unavailable',
(
IF(
'2016-03-15' > check_in_time,
(
IF(
'2016-03-15' < check_out_time,
'Unavailable',
'Available'
)
),
(
IF(
'2016-03-22' > check_in_time,
'Unavailable',
'Available'
)
)
)
)
) AND `room_prize` BETWEEN '174' AND '600' AND `rm`.`hotel_id` = `h`.`hotel_id`
) AS `min_val`
FROM
`abserve_hotels` AS `h`
WHERE
1 AND `city` = "madurai" AND `country` = "india"
It totally return one column values from my table abserve_hotels which is hotel_id with extra two alias columns such as avail_room_count and min_val..
And I wrote those in a subquery..
Here I have to check a condition WHERE min_val IS NOT NULL .i.e; if min_val having NULL value I have to restrict it
How can I do this..
And this is my table
hotel_id avail_room_count min_val
1 0 NULL
2 0 NULL
Here I need to restrict these NULL values..
Someone please help me ..
Add a HAVING clause at the end:
HAVING min_val IS NOT NULL
The new query after WHERE looks like:
WHERE
1 AND `city` = "madurai" AND `country` = "india"
HAVING min_val IS NOT NULL
Your query is overly complex and can be much simplified:
The two correlated sub queries are exactly the same, except for the SELECT list (MIN versus COUNT), so they could be combined into one;
The aggregation done by the sub query can be done in the main query;
The condition for checking availability can be written much shorter.
In fact, you can do all of what you need with the following query:
SELECT h.hotel_id,
COUNT(rm.room_id) as avail_room_count,
MIN(rm.room_prize) AS min_val
FROM abserve_hotels AS h
INNER JOIN abserve_hotel_rooms AS rm
ON rm.hotel_id = h.hotel_id
WHERE h.city = "madurai"
AND h.country = "india"
AND rm.adults_count >= 1
AND rm.room_count >= 1
AND rm.room_prize BETWEEN 174 AND 600
AND ( rm.check_in_time >= '2016-03-22'
OR rm.check_out_time <= '2016-03-15'
OR rm.check_in_time IS NULL)
GROUP BY h.hotel_id
Because the INNER JOIN requires at least one match, you can already be sure that min_val will never be NULL.
The check for availability is just as simple as:
( rm.check_in_time >= '2016-03-22'
OR rm.check_out_time <= '2016-03-15'
OR rm.check_in_time IS NULL)
The three parts of that condition mean:
The reservation for this room is future and does not overlap with this week;
The reservation for this room is in the past, the room is free today at the latest;
The room has no known reservation.
In all three cases the room is available for reservation for the concerned week.

Mysql concat select and update

I have two queries:
1) $query = "SELECT `login`, `password` FROM `TEST_ACCS` WHERE `sold` = '0' LIMIT 3";
And then I need change sold to 1 from the result of first query
2) $setQuery = "UPDATE `chrome_ext`.`TEST_ACCS` SET `sold` = '1' WHERE `sold` = '0' LIMIT 3";
How to concat these two queries into one?
I tried
UPDATE `chrome_ext`.`TEST_ACCS` dest, (SELECT `login`, `password` FROM `TEST_ACCS` WHERE `sold` = '0' LIMIT 3) src SET dest.sold = '1' where dest.sold= '0'
But it set sold = 1 to all raws, but need just 3

same table count union

SELECT COUNT(*) as totalHappenings FROM `happenings` WHERE `userId` = ?
UNION
SELECT COUNT(*) as xHappenings FROM `happenings` WHERE `userId` = ? AND `destinationObjectType` = \'2\'
UNION
SELECT COUNT(*) as yHappenings FROM `happenings` WHERE `userId` = ? AND `destinationObjectType` = \'1\'
Since it's the same table, and I don't wanna pass through 3 times the userId parameter how can I solve this the best way?
SELECT
COUNT(*) AS totalHappenings,
SUM(CASE WHEN `destinationObjectType` = \'2\' THEN 1 ELSE 0 END) AS xHappenings,
SUM(CASE WHEN `destinationObjectType` = \'1\' THEN 1 ELSE 0 END) AS yHappenings
FROM `happendings`
WHERE `userId` = ?
Result:
totalHappenings xHappenings yHappenings
24 10 14
You can do this with if statements inside select clause:
SELECT
COUNT(userId) as totalHappenings,
SUM(IF(`destinationObjectType`='2',1,0) as xHappenings,
SUM(IF(`destinationObjectType`='1',1,0) as yHappenings
FROM `happenings`
WHERE `userId` = ?
This will surely return your results in 3 columns. Your original query was returning in 3 rows but I think that is not a problem.
try the shortest way:
SELECT COUNT(*) as totalHappenings, SUM(`destinationObjectType` = \'2\') AS xHappenings, SUM(`destinationObjectType` = \'1\') AS yHappenings FROM `happenings` WHERE `userId` = ?
comparision inside SUM returns true or false (1 or 0) so there is no need for IF or CASE statements