How to use orWhere() optimally in the query? - mysql

Here is my query:
$notifications = \App\Notification::query()->where('users_scope', 'customer')
->with('userSeen')
->where(function ($query) use ($user) {
$query->where('user_id_to', 20288)
->orWhere(function($q) use ($user) {
$q->Where('user_id_to', null)
->Where(function($q) use ($user) {
$q->where('expire_at', null)->where('created_at', '>=', "2020-04-03 04:18:42");
$q->orWhere('expire_at', '!=', null)->where('expire_at', '>', Carbon::now());
});
});
})
->select([DB::raw('SQL_CALC_FOUND_ROWS *')])->orderBy('id', 'desc')->limit(20)->get();
Also here is the relation (used as with()):
public function userSeen()
{
return $this->belongsToMany(User::class, NotificationSeen::class, 'notification_id', 'user_id');
}
Here is the scenario: Query above gets the last 20 user's notifications (latest notifications list). Recently, the size of the notifications table is increased too much. It has over 8 million rows at the moment. And the query takes over 10 seconds to be executed.
Noted that, all needed indexes have been created as well on both tables (notifications and userSeen tables). Also, that relation (userSeen) is like a pivot table that indicates the user either has seen the notification or not.
Any idea how can I rewrite that query to be more optimal?
Explanations about the logic:
20288 is hardcoded and will be $user->id in reality.
when user_id_to is null, it means it's a bulk notification (must be visible for all users)
User can see bulk notifications if they have a bigger created_at value than the user's created_at.
Sometimes bulk notifications has an expire time (like marketing campaigns) that must be shown to the users if still not outdated.

Bases on the logic you mentioned in your question, this should get you what you need, however, it probably won't help that much with the speed:
$notifications = \App\Notification::query()
->select([DB::raw('SQL_CALC_FOUND_ROWS *')])
->with('userSeen')
->where('users_scope', 'customer')
->where(function ($query) use ($user) {
$query
->where('user_id_to', $user->id)
->orWhere(function ($query) use ($user) {
$query
->whereNull('user_id_to')
->where('created_at', '>=', $user->created_at)
->where(function ($query) {
$query->whereNull('expire_at')->orWhere('expire_at', '>=', now());
});
});
})
->orderByDesc('id')
->limit(20)
->get();
I would also suggest trying 2 separate queries instead of using SQL_CALC_FOUND_ROWS. Here are a couple of SO posts that explain why:
Which is fastest? SELECT SQL_CALC_FOUND_ROWS FROM `table`, or SELECT COUNT(*)
SELECT SQL_CALC_FOUND_ROWS Query very slow greater than 250000 records
You queries would then look something like:
$query = \App\Notification::query()
->with('userSeen')
->where('users_scope', 'customer')
->where(function ($query) use ($user) {
$query
->where('user_id_to', $user->id)
->orWhere(function ($query) use ($user) {
$query
->whereNull('user_id_to')
->where('created_at', '>=', $user->created_at)
->where(function ($query) {
$query->whereNull('expire_at')->orWhere('expire_at', '>=', now());
});
});
})
->orderByDesc('id');
$count = $query->count();
$notifications = $query->limit(20)->get();
Alternatively, you could use something like paginate().

I am not sure what condition you have but condition you can apply like below
$notifications = \App\Notification::query()->where('users_scope', 'customer')
->with('userSeen')
->where(function ($query) use ($user) {
$query->where('user_id_to', 20288);
if($user){
$query->orWhere(function($q) use ($user) {
$q->Where('user_id_to', null)
->Where(function($q) use ($user) {
$q->where('expire_at', null)->where('created_at', '>=', "2020-04-03 04:18:42");
$q->orWhere('expire_at', '!=', null)->where('expire_at', '>', Carbon::now());
});
});
}
})
->select([DB::raw('SQL_CALC_FOUND_ROWS *')])->orderBy('id', 'desc')->limit(20)->get();
Even you can use when clause
Ref:https://laravel.com/docs/8.x/queries#conditional-clauses

Related

how to make if statement in the laravel query builder

I have the following SQL query
$query
->join('cities','tickets.city_id','=','cities.id')
->select(
'tickets.id',
'tickets.biker_id',
'tickets.picked_up',
'tickets.delivered',
'tickets.service_charge',
'tickets.amount',
'tickets.cancelled',
'tickets.pre_order',
'tickets.created_by',
'tickets.created_at'
)
->whereDay('tickets.created_at',Date('d'))
->orderBy('tickets.created_at','desc')
->get();
my aim is to set the
whereday('tickets.created_at', Date('d'))
to
whereday('tickets.created_at', Date('d', strtotime("-1 day")))
whenever the tickets.pre_order = 1
but when tickets.pre_order = 0 i will stick to the
whereday('tickets.created_at',Date('d'))
is it possible using if statement or is there any better way to solve this?
make it like this
->where(function ($query){
$query->where('tickets.created_at', Carbon::now()->subDays()->format('d'))
->where('tickets.pre_order',1);
})->orWhere(function ($query){ $query->where('tickets.created_at',
Carbon::now()->format('d')) ->where('tickets.pre_order',0); })
->get();
To subtract a day and format it, use Carbon library for DateTime in PHP (as given in comments by #spartyboy )
$query
->join('cities','tickets.city_id','=','cities.id')
->select(
'tickets.id',
'tickets.biker_id',
'tickets.picked_up',
'tickets.delivered',
'tickets.service_charge',
'tickets.amount',
'tickets.cancelled',
'tickets.pre_order',
'tickets.created_by',
'tickets.created_at'
)
->where(function ($query) {
$query
->where('tickets.pre_order', 0)
->whereDay('tickets.created_at', Date('d'));
})
->orWhere(function ($query) {
$query
->where('tickets.pre_order', 1)
->whereDay('tickets.created_at', Carbon::yesterday()->format('d'));
})
->orderBy('tickets.created_at','desc')
->get();
or if you want to subtract multiple days then
instead of yesterday() use now()->subDays($days_count)

Not getting proper result but working in sql

This is the code that is already working in mysql database but I want to convert it into Laravel 5.6
SELECT *
FROM `listings`
WHERE (
country_id=1
AND (state=32 or city=8)
AND (listing_id LIKE "%6562%" OR title LIKE "%6562%"))
Supposedly you have a model called Listing which takes care of the listings table. You can write the query like this:
$listings = App\Listing::where('field1', 1)
->where(function ($query) {
$query->where('field2', 32);
$query->orWhere('field3', 8);
})
->where(function ($query) {
$query->where('field4', 'LIKE', '%6562%');
$query->orWhere('field5', 'LIKE', '%6562%');
})
->get();
The first parameter of the where method can be a callback which can achieve this type of grouping (field2=32 or field3=8)

orWhere with multiple conditions in Eloquent

I am looking to write the following query in eloquent:
select * from stocks where (symbol like '%$str%' AND symbol != '$str' ) OR name like '$str%'
Without the last condition, it's simple:
$stocks = Stock::live()
->where('symbol','like','%'.$str.'%')
->where('symbol','!=',$str)
->get();
But adding orWhere('name','like',$str.'%') after the two wheres returns incorrect results. Basically I am wondering how to emulate what I accomplished by using the (condition1 AND condition2) OR condition3 syntax in the raw query above.
Try
$stocks = Stock::live()
->where('name', 'like' , '%'.$str.'%')
->orWhere(function($query) use($str) {
$query->where('symbol','like','%'.$str.'%')
->where('symbol','!=',$str); // corrected syntax
})->get();
Try
$stocks = Stock::live()->where('name', 'like' , '%'.$str.'%')
->orWhere(function($query) use($str) {
$query->where('symbol','like','%'.$str.'%')
->where('symbol','!=',$str)
})->get();
I didn't test this, so sorry if it doesn't work. But I think one of these solutions could work.
$stocks = Stock::live()
->where([
['symbol','like','%'.$str.'%'],
['symbol', '!=', $str],
])
->orWhere('name','like', $str.'%')
->get();
and
->where(function ($query) use ($str) {
$query->where([
['symbol','like','%'.$str.'%'],
['symbol', '!=', $str],
]);
})
->orWhere(function ($query) use ($str) {
$query->where('name','like', $str.'%');
});

Querying count from Eloquent

I've got a query in eloquent correctly calculating the count as it should here:
$query = A::withCount(
['bs' =>
function ($query) use ($from, $to) {
$query->where(function ($query) use ($from) {
$query->whereDate('start', '<', $from)
->whereDate('end', '>', $from);
})->orWhere(function ($query) use ($to) {
$query->whereDate('start', '<', $to)
->whereDate('end', '>', $to);
})->orWhere(function ($query) use ($from, $to) {
$query->whereDate('start', '>', $from)
->whereDate('end', '<', $to);
});
}])
->whereBetween('cmp1', [$min_cmp1, $max_cmp1])
->whereBetween('cmp2', [$min_cmp2, $max_cmp2])
->where('cmp3', '<=', $request->cmp3)
->where('cmp4', '<=', $request->cmp4)
->with('more_relations');
return AResource::collection($query->paginate(5));
This goes to an API Controller. I use this for frontend pagination with Vue. I want to use the count to filter out all A's with a bs_count of 0, however chaining a where clause does not work as it is an aggregate, and it returns an error for not finding a column named bs_count. The solution, I found out, would be to use a having clause. I found this code, but when I try to convert it, it doesn't work, returning the same error, not finding a column named bs_count.
DB::table('bs')
->select('*', DB::raw('COUNT(*) as bs_count'))
->groupBy('a_id')
->having('bs_count', '=' , 0);
This is without adding the extra query on the count, as I wanted to try this to see if it works first, which it doesn't.
Is the having clause the correct way to go? I think I need to use Eloquent rather than the DB::table() syntax as the Resource builder uses the model structure to build the resource response.
In summary, I'm trying to use the count I calculated in my first query, and query against it: where bs_count = 0. Is there a way to do this in Laravel and still be able to pass on the results to the API Resource?
Using a HAVING clause is the correct way to go.
The problem is that Laravel pagination doesn't natively support HAVING clauses.
You have to create the pagniator manually:
How to use paginate() with a having() clause when column does not exist in table
Your test query with DB doesn't work because you can't select all columns when grouping:
->select('a_id', DB::raw('COUNT(*) as bs_count'))
Laravel does have a built-in solution for this, as I discovered. From the docs, I found the functions whereHas() and whereDoesntHave(). These functions allow you pass a closure to filter the rows based on the count it does behind the scenes. How I adapted my previous solution was:
$query = A::whereDoesntHave(
'bs',
function ($query) use ($from, $to) {
$query->where(function ($query) use ($from) {
$query->whereDate('start', '<', $from)
->whereDate('end', '>', $from);
})->orWhere(function ($query) use ($to) {
$query->whereDate('start', '<', $to)
->whereDate('end', '>', $to);
})->orWhere(function ($query) use ($from, $to) {
$query->whereDate('start', '>', $from)
->whereDate('end', '<', $to);
});
)
->whereBetween('cmp1', [$min_cmp1, $max_cmp1])
->whereBetween('cmp2', [$min_cmp2, $max_cmp2])
->where('cmp3', '<=', $request->cmp3)
->where('cmp4', '<=', $request->cmp4)
->with('more_relations');
return AResource::collection($query->paginate(5));
This automatically does a count of B's based on the query passed and only returns rows that have a count of zero, which is exactly what I needed.

Check overlaping timeslots for laravel query

While booking a time-slot I want to check if that slot is overlapping with any other already booked time-slot. Used following query for same which is not correct:
Availability::where('date',date('Y-m-d',strtotime($request->available_date)))
->where(function ($query) use($request){
$query->whereBetween('start_time', [date("H:i:s", strtotime($request->start_time)), date("H:i:s", strtotime($request->end_time))])
->orWhereBetween('end_time', [date("H:i:s", strtotime($request->start_time)), date("H:i:s", strtotime($request->end_time))]);
})
->get();
Thanks In advance!
Firstly, to be able to have access to $startTime and $endTime within the query closure you will need to pass them through using the use construct i.e.
function ($query) use ($startTime, $endTime)
Try like below:
$startTime = $request->start_time;
$endTime = $request->end_time;
$Availability = Availability::where(function ($query) use ($startTime, $endTime) {
$query
->where(function ($query) use ($startTime, $endTime) {
$query
->where('start_time', '<=', $startTime)
->where('end_time', '>', $startTime);
})
->orWhere(function ($query) use ($startTime, $endTime) {
$query
->where('start_time', '<', $endTime)
->where('end_time', '>=', $endTime);
});
})->count();
Hope this will helps you!
Little Modified Query Hope it will work for all the people searching time slot booking algorithm.
$Availability= Availability::
where(function ($query) use ($start_time, $end_time) {
$query
->whereBetween('start_time', [$start_time, $end_time])
->orWhere(function ($query) use ($start_time, $end_time) {
$query
->whereBetween('end_time', [$start_time, $end_time]);
});
})->count();