Laravel groupBy performance slow - php

I'm working in one of my Laravel 8 projects and need to parse dates of my returned query for formatting with my graph. My query, which returns around 70,000 rows takes around 100ms, but upon further debugging, the getUptimeTimeline function in my project which takes the results of my query as $uptimeChecks is taking 800ms for just the groupBy part.
How can I improve this performance or maybe exclude it entirely?
The query results passed to my function are:
$uptimeChecks = UptimeCheck::where('user_id', $user->id)
->where('monitor_id', $monitor['id'])
->where('checked_at', '>=', $from)
->where('checked_at', '<=', $to)
->orderBy('checked_at', 'asc')
->select('event', 'response_time', 'checked_at')
->get();
Here's my function:
/**
* Get uptime timeline
*
* #return Response
*/
protected function getUptimeTimeline($user, $id, $uptimeChecks, $period, $days)
{
try {
$start = microtime(true);
$dates = collect($period->toArray())->mapWithKeys(function ($date) {
return [$date->format('Y-m-d') => [
'total_events' => 0,
'down_events' => 0,
'up_events' => 0,
'uptime' => 'No Data',
'fill' => '#ced1d7',
]];
});
$end = microtime(true);
Log::debug('timeline_1', [
'diff' => ($end - $start) * 1000
]);
$start = microtime(true);
$uptimeDates = $uptimeChecks->groupBy(function ($item, $key) {
$date = Carbon::parse($item->checked_at);
return $date->format('Y-m-d');
});
$end = microtime(true);
Log::debug('timeline_2', [
'diff' => ($end - $start) * 1000
]);
return $uptimeDates;
} catch (\Exception $e) { }
}
It's the timeline_2 results which is slow.
UPDATE
$responseTimes = UptimeCheck::where('user_id', $user->id)
->where('monitor_id', $monitor['id'])
->where('checked_at', '>=', $from)
->where('checked_at', '<=', $to)
->orderBy('checked_at', 'asc')
->select('event', 'response_time', 'checked_at', DB::raw('DATE(checked_at) as check_date'))
->groupBy('check_date')
->limit(10)
->get();
Attempting to run a select containing DB::raw() throws an error:
[2023-01-30 20:42:31] local.ERROR: SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'domainmonitor_db.uptime_checks.event' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by (SQL: select event, response_time, checked_at, DATE(checked_at) as check_date from uptime_checks where user_id = 1 and monitor_id = 1 and checked_at >= 2022-12-26 20:42:31 and checked_at <= 2023-01-30 20:42:31 group by check_date order by checked_at asc limit 10) {"userId":1,"exception":"[object] (Illuminate\Database\QueryException(code: 42000): SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'domainmonitor_db.uptime_checks.event' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by (SQL: select event, response_time, checked_at, DATE(checked_at) as check_date from uptime_checks where user_id = 1 and monitor_id = 1 and checked_at >= 2022-12-26 20:42:31 and checked_at <= 2023-01-30 20:42:31 group by check_date order by checked_at asc limit 10) at C:\Users\Ryan\Desktop\web-projects\domain-monitor\domain-monitor-api\vendor\laravel\framework\src\Illuminate\Database\Connection.php:760)

You should group the item during the query and handle other column properly on how you want like combine, sum, min, max or whatever that belongs to a group
here's an example grouping your query by checked_at and combing the values of other columns you want to select.
$uptimeChecks = UptimeCheck::select(
DB::raw('group_concat(event) as events'), // this combines event column values belongs to a group
DB::raw('group_concat(response_time) as responses_time'), // this combines response_time column values belongs to a group
'checked_at'
)
->where('user_id', $user->id)
->where('monitor_id', $monitor['id'])
->whereBetween('checked_at', [$from, $to])
->groupBy('checked_at')
->orderBy('checked_at', 'asc')
->get();

Related

Laravel: Eloquent select returning different results

I'm trying to understand how Eloquent select changes the results of a query. For example:
$startDate = Carbon::createFromFormat('Y-m-d H:i:s', '2023-01-26 00:00:00', 'America/Chicago')->timezone('UTC');
$endDate = Carbon::createFromFormat('Y-m-d H:i:s', '2023-01-26 23:59:59', 'America/Chicago')->timezone('UTC');
$data = Orders::where('canceled', 0)
->whereBetween('created_at', [$startDate->toDateTimeString(), $endDate->toDateTimeString()])
->where(function ($query) {
$query->where('is_branding', 1)
->orWhere('is_premium_branding', 1);
})
->get();
I have some other code running a foreach on this data to calculate and I end up with:
{
"branding_order_items_count": 12,
"branding_order_items_sales": 799.98,
"branding_order_items_margin": 169.71,
"branding_order_items_margin_percent": 0
}
However, if I run the same query but with an added select and calculate through the same foreach loop, I get a different result:
$startDate = Carbon::createFromFormat('Y-m-d H:i:s', '2023-01-26 00:00:00', 'America/Chicago')->timezone('UTC');
$endDate = Carbon::createFromFormat('Y-m-d H:i:s', '2023-01-26 23:59:59', 'America/Chicago')->timezone('UTC');
$data = Orders::where('canceled', 0)
->whereBetween('created_at', [$startDate->toDateTimeString(), $endDate->toDateTimeString()])
->where(function ($query) {
$query->where('is_branding', 1)
->orWhere('is_premium_branding', 1);
})
->select('*', DB::raw("count(*) as count")) // <<<<<<< Added this
->get();
With that added select, I get the following:
{
"branding_order_items_count": 11,
"branding_order_items_sales": 649.99,
"branding_order_items_margin": 142.12,
"branding_order_items_margin_percent": 0
}
The first result is the correct numbers, 12 items / 799.98, etc. So why does adding the select to the eloquent query return a different result, and how can I have it return the correct results while still using the select?
TIA
I'm assuming this is using a MySQL database. PostgreSQL would throw a grouping error with that query.
The reason your results differ is most likely due to DB::raw("count(*) as count"). Without a GROUP BY clause, the results will only be 1 row of data.
If your orders table looks like this:
id
cancelled
1
1
2
0
3
0
4
1
SELECT * FROM orders WHERE cancelled = 0 will return
id
cancelled
2
0
3
0
SELECT *, COUNT(*) AS count FROM orders WHERE cancelled = 0 will return
id
cancelled
count
2
0
2
SELECT *, COUNT(*) AS count FROM orders WHERE cancelled = 0 GROUP BY id will return
id
cancelled
count
2
0
1
3
0
1

How to transform sql query into Query Builder Laravel way?

I need to convert this query into Laravel query builder or ORM
SET #start_date = '2020-11-01';
SET #end_date = '2020-11-08';
SET #duration = CONVERT(#end_date, DATE) - CONVERT(#start_date, DATE);
SELECT item_id, days
FROM (
SELECT item_id, sum(end_date - start_date) AS days
FROM schedule WHERE start_date >= #start_date AND end_date <= #end_date
GROUP BY item_id) AS virtual
WHERE days = #duration;
(i use Laravel 8)
i could not find similar example I could analize and try by myself :(
i try this :
$res = DB::table('schedule')
->select('schedule.item_id' , DB::raw("SUM(schedule.end_date - schedule.start_date) as days"))
->where('start_date', '>=', $start_date)
->where('end_date', '<=', $end_date)
->groupBy('item_id')
->where('days', '=', $duration)
->get();
but i get error :
Column not found: 1054 Unknown column 'days' in 'where clause'
ok i know what is wrong
i tried to access 'days' column before it is created with AS
that column will be available after line ->get() is executed.
So I changed order of these 2 lines :
->where('days', '=', $duration)
->get();
to
->get()
->where('days', '=', $duration);
and now works :)

Laravel 5.4: Converting a raw SQL query in Laravel Eloquent

I'm trying to rewrite this SQL query but I'm stuck at this point
The query is meant to join the projects table to the project_progress table by using a sub-query to only join on the latest entry
SELECT * FROM projects
JOIN project_progress ON project_progress.id =
(
SELECT id FROM project_progress
WHERE project_progress.project_id = projects.id
ORDER BY project_progress.created_at DESC
LIMIT 1
)
WHERE project_progress.next_action_date < NOW()
AND projects.status != 'Complete'
AND projects.member_id = 1
ORDER BY projects.title ASC
To:
$projects = App\Project::where('member_id', 1)
->join('project_progress', function ($join) {
$join->on('project_progress.id', '=', function ($query) {
$query->select('project_progress.id')
->from('project_progress')
->where('project_progress.project_id', 'projects.id')
->orderBy('project_progress.created_at', 'desc')
->limit(1);
});
})
->where('project_progress.next_action_date', '<', Carbon\Carbon::now())
->notCompleted()
->orderBy('projects.project_title', 'asc')
->get();
I think some thing is wrong with this line but I'm not sure how to write it
$join->on('project_progress.id', '=', function ($query) {
ErrorException (E_ERROR) strtolower() expects parameter 1 to be string, object given \vendor\laravel\framework\src\Illuminate\Database\Grammar.php
Use where():
$join->where('project_progress.id', '=', function ($query) {

Return data ordered by date

I have a function that returns the count of sales each day. The problem with this approach is that I want the data to be stored by date but I am getting them in the following order:
01-Dec
02-Dec
03-Dec
03-Nov
04-Nov
05-Nov
etc.
I understand why that happens but I am not sure how to solve it. I can replace subMonth(1) with startofmonth which woul partially solve my problem but this is not what I want. I instead want to return the last 30 days ordered.
return DB::table('sales')
->select(\DB::RAW('DATE_FORMAT(created_at, "%d-%M") as date'), \DB::raw('COUNT(*) as count'))
->where('created_at', '>=', Carbon::now()->subMonth(1))
->orderBy('date')
->groupBy('date')
->get(['date', 'count'])
->keyBy('date')
->transform(function ($data) {
return $data->count;
});
I also tried orderBy('created_at') but it gives me the error below and I'd like to avoid changing the sql mode.
Syntax error or access violation: 1055 Expression #3 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'x.sales.created_at' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
EDIT:
As requested, this the sql query of the return statement
select DATE_FORMAT(created_at, "%d-%M") as date, COUNT(*) as count from `sales` where `created_at` >= ? group by `date` order by `date` asc
i don't have much idea about your framework query syntax. but you can take one more column DATE_FORMAT(created_at, "%Y%m%d") AS order_column and apply order, and group by on column "order_column" and use column "data" while display data.
select DATE_FORMAT(created_at, "%Y%m%d") AS order_column, DATE_FORMAT(created_at, "%d-%M") as date, COUNT(*) as count from `sales` where `created_at` >= ? group by `order_column` order by `order_column` asc
return DB::table('sales')
->select(\DB::RAW('DATE_FORMAT(created_at, "%Y%m%d") AS order_column'),\DB::RAW('DATE_FORMAT(created_at, "%d-%M") as date'), \DB::raw('COUNT(*) as count'))
->where('created_at', '>=', Carbon::now()->subMonth(1))
->orderBy('order_column')
->groupBy('order_column')
->get(['date', 'count'])
->keyBy('date')
->transform(function ($data) {
return $data->count;
});
If you make this in 2 steps does it work?
Step1 in which you create date column:
$step1 = DB::table('sales') ->select(\DB::RAW('DATE_FORMAT(created_at, "%d-%M") as date')))
->where('created_at', '>=', Carbon::now()->subMonth(1))
->orderBy('date')
->get(['date', 'count'])
and after that you make the agregation:
$step2 = $step1->select('date'), \DB::raw('COUNT(date) as count'))
->groupBy('date') ->get(['date', 'count']) ->keyBy('date')
Hope it helps!

convert SQL query to query builder style

Im trying days to understand how I can convert a SQL query to a query builder style in laravel.
My SQL query is:
$tagid = Db::select("SELECT `id` FROM `wouter_blog_tags` WHERE `slug` = '".$this->param('slug')."'");
$blog = Db::select("SELECT *
FROM `wouter_blog_posts`
WHERE `published` IS NOT NULL
AND `published` = '1'
AND `published_at` IS NOT NULL
AND `published_at` < NOW()
AND (
SELECT count( * )
FROM `wouter_blog_tags`
INNER JOIN `wouter_blog_posts_tags` ON `wouter_blog_tags`.`id` = `wouter_blog_posts_tags`.`tags_id`
WHERE `wouter_blog_posts_tags`.`post_id` = `wouter_blog_posts`.`id`
AND `id`
IN (
'".$tagid[0]->id."'
)) >=1
ORDER BY `published_at` DESC
LIMIT 10
OFFSET 0");
Where I now end up to convert to the query builder is:
$test = Db::table('wouter_blog_posts')
->where('published', '=', 1)
->where('published', '=', 'IS NOT NULL')
->where('published_at', '=', 'IS NOT NULL')
->where('published_at', '<', 'NOW()')
->select(Db::raw('count(*) wouter_blog_tags'))
->join('wouter_blog_posts_tags', function($join)
{
$join->on('wouter_blog_tags.id', '=', 'wouter_blog_posts_tags.tags_id')
->on('wouter_blog_posts_tags.post_id', '=', 'wouter_blog_posts.id')
->whereIn('id', $tagid[0]->id);
})
->get();
I have read that I can't use whereIn in a join. The error i now get:
Call to undefined method Illuminate\Database\Query\JoinClause::whereIn()
I realy dont know how I can convert my SQL to query builder. I hope when I see a good working conversion of my query I can understand how I have to do it next time.
This work for me:
DB::table('wouter_blog_posts')
->whereNotNull('published')
->where('published', 1)
->whereNotNull('published_at')
->whereRaw('published_at < NOW()')
->whereRaw("(SELECT count(*)
FROM wouter_blog_tags
INNER JOIN wouter_blog_posts_tags ON wouter_blog_tags.id = wouter_blog_posts_tags.tags_id
WHERE wouter_blog_posts_tags.post_id = wouter_blog_posts.id
AND id
IN (
'".$tagid."'
)) >=1")
->orderBy('published_at', 'desc')
->skip(0)
->take(10)
->paginate($this->property('postsPerPage'));
The following Query Builder code will give you the exact SQL query you have within your DB::select:
DB::table('wouter_blog_posts')
->whereNotNull('published')
->where('published', 1)
->whereNotNull('published_at')
->whereRaw('`published_at` < NOW()')
->where(DB::raw('1'), '<=', function ($query) use ($tagid) {
$query->from('wouter_blog_tags')
->select('count(*)')
->join('wouter_blog_posts_tags', 'wouter_blog_tags.id', '=', 'wouter_blog_posts_tags.tags_id')
->whereRaw('`wouter_blog_posts_tags`.`post_id` = `wouter_blog_posts`.`id`')
->whereIn('id', [$tagid[0]->id]);
})
->orderBy('published_at', 'desc')
->skip(0)
->take(10)
->get();
The subquery condition had to be reversed because you can't have a subquery as the first parameter of the where method and still be able to bind the condition value. So it's 1 <= (subquery) which is equivalent to (subquery) >= 1. The query generated by the above code will look like this:
SELECT *
FROM `wouter_blog_posts`
WHERE `published` IS NOT NULL
AND `published` = 1
AND `published_at` IS NOT NULL
AND `published_at` < Now()
AND 1 <= (SELECT `count(*)`
FROM `wouter_blog_tags`
INNER JOIN `wouter_blog_posts_tags`
ON `wouter_blog_tags`.`id` =
`wouter_blog_posts_tags`.`tags_id`
WHERE `wouter_blog_posts_tags`.`post_id` =
`wouter_blog_posts`.`id`
AND `id` IN ( ? ))
ORDER BY `published_at` DESC
LIMIT 10 offset 0
My process when creating more complex queries is to first create them and try them out in a SQL environment to make sure they work as indended. Then I implement them step by step with the Query Builder, but instead of using get() at the end of the query, I use toSql() which will give me a string representation of the query that will be generated by the Query Builder, allowing me to compare that to my original query to make sure it's the same.

Categories