Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Binho's avatar
Level 12

SQL - WHERE IF condition in QUERY BUILDER request

Hello everyone,

Since many days I'm trying to make a complex request to filter some values but without success. I'm here now to find some help and if you have a some answers for me :)

I have a table "user_working_locations" to store default locations for each day of the week and also to store a custom location for a specific date as you can see below :

id			user_id			location_id			day			date   		    type

1			1				1				    1			NULL		  'default'
2			1				1					2			NULL		  'default'
3			1				1					3			NULL		  'default'
4			1				1					4			NULL		  'default'
5			1				1					5			NULL		  'default'
6			1				1					6			NULL		  'default'
7			1				1					7			NULL		  'default'
8			1				5					5			2021-02-19    'custom'

I want to be able to filter the request with one or several "location_id" with always the start and end date. That means that if for a specific date the user has a custom location, I want to filter on this value but if the user doesn't have a custom location I want to filter on the default value.

You can see my request bellow with what I am able to filter on "location_id = 5" but if I want to filter with "location_id = 1" the query return one result and I'm expecting to have no result because the user has a custom location id = 5 on that day.

$query->whereHas('workingLocations', function($query) use($days, $values) {
        $query->whereIn('location_id', $values)
              ->whereIn('day', $days)
              ->whereRaw('CASE WHEN date BETWEEN \''.$this->start_date.'\' AND
                          \''.$this->end_date.'\' THEN type = \'custom\'
                          ELSE type = \'default\' END');

Many thanks for your help!

0 likes
21 replies
trin's avatar

can u post, what u want to be results? a couple of examples for different input data

Binho's avatar
Level 12

Hello @trin,

For this example, if I send a request with the following data, I expect one result with the last line of my table.

  • start_date = 2021-02-19
  • end_date = 2021-02-19
  • day = 5
  • location_id = 5

If I send the following data, I expect 0 result

  • start_date = 2021-02-19
  • end_date = 2021-02-19
  • day = 5
  • location_id = 1

If I send the following data, I expect 1 result with the first line of my table :

  • start_date = 2021-02-22
  • end_date = 2021-02-22
  • day = 1
  • location_id = 1

If I send the following data, I expect 0 result :

  • start_date = 2021-02-22
  • end_date = 2021-02-22
  • day = 1
  • location_id = 5

Many thanks for your help

trin's avatar

ok, it is easy query.

  1. first, we need to find foothold. as i see, best index for this query is location_id. it is true only if location_id always exists in query
  2. when we filtering results by location_id, we need group by day. and, if u need get list of users, of course, by user_id
  3. after group, we find get MAX date from query, because anydate always > null. and we not need to select type, because if MAX(date) not null, it custom type, else it default.
  4. add filter by date, where date can be null or between required dates

raw query

SELECT
	user_id,
    location_id,
    day,
    MAX(date), 
    IF(MAX(date) IS NOT NULL, "custom", "default") type
FROM
    locations
WHERE
	location_id IN (1, 5) AND
    day IN (1,2,3,4,5,6,7) AND
    (
        date is NULL OR
        date BETWEEN '2021-02-01' AND '2021-02-19'
    )
GROUP BY
    day, user_id, location_id

as result, we got list of user_id, location_id, day and date+type, where date is MAX from all.

in laravel it is bee like

$result = Location::select(DB::raw('user_id, location_id, day, MAX(date), IF(MAX(date) IS NOT NULL, "custom", "default") type'))
    ->whereIn('location_id', $location_ids)
    ->whereIn('day', $days)
    ->where(function($query) use ($start_date, $end_date) {
        $query
            ->whereNull('date')
            ->orWhereBetween('date', [$start_date, $end_date]);
    })
    ->groupBy('day', 'user_id', 'location_id')
	->get();

easy ;) if I understood the task correctly. p.s. make sure u have index by location_id and it use in explain

sr57's avatar

Hi @binho

You could do it in 3 times,

-1- Query for this location between dates in custom location, if 1 - result 1

-2- If 0 at the previous query, query is there a custom location between dates, if 1 - result 0

-3- if 0 , query for this location in default -> result

To be understandable (in the future and by others), it's easier to create a php function with these 3 queries and the right logic.

trin's avatar

easier? pffff) only sql, only hardcore) actually u are right, if u not need paginate, ordering or both of them

sr57's avatar

Hi @trin

Everything is relative

All tastes are in nature.

So read, it's easier for me.

If needed, it's always possible to put the 3 queries in a raw sql, and to make it readable the best is to write partial sql in php variables.

trin's avatar

subquery in mysql leads to more mistakes. and I didn't try to judge you )

sr57's avatar

me too.

subquery in mysql leads to more mistakes.

I have better postgresql, and I use a lot subqueries and views.

1 like
Binho's avatar
Level 12

Hello @trin ,

Thanks for your help. I tried the your request but I still have one results with the following data :

  • start_date = 2021-02-19
  • end_date = 2021-02-19
  • day = 5
  • location_id = 1

I don't know if I missed something. Please find bellow my query :

$query->whereHas('workingLocations', function($query) use($days, $values) {
   $query->selectRaw('user_id, location_id, day, MAX(date), IF(MAX(date) IS NOT NULL, "custom", "default") type')
				->whereIn('location_id', $values)
                ->whereIn('day', $days)
                ->where(function($query) {
                         $query->whereNull('date')
                                ->orWhereBetween('date', [$this->start_date, $this->end_date]);
                            })->groupBy('day', 'user_id', 'location_id');

Many thanks for your help

trin's avatar

I probably misunderstood the task, but u have one rows in db by day=5 and location_id. what u need to result?

Binho's avatar
Level 12

Yes, I have one raw for the location_id = 1, but in this case the user has a custom location_id = 5 for this specific date. So when I filter to location_id = 1, I expect 0 result because it's location_id = 5 and not 1 for this date.

trin's avatar

It means that I did not understand the problem. u need get default forlocation_id only if there are no results for custom for a different location_id for that user? if any otherlocation_id in the selected date has custom the result should be 0? right?

Binho's avatar
Level 12

Not exactly, for a specific date/day I need to get the custom location id if there is one, or the default if there is no custom. That means if I am looking for a specific location id, I want to find in all custom ld's for this specific date/day and in the default location id if there is no result for the custom part.

rodrigo.pedra's avatar
Level 56

My try:


Route::get('/test', function () {
    $startDate = '2021-02-22';
    $endDate = '2021-02-22';
    $days = [1];
    $locations = [5];

    $constraints = function ($query) use ($locations, $days, $startDate, $endDate) {
        $query->whereIn('location_id', $locations);
        $query->whereIn('day', $days);

        $model = $query->getModel();

        $query->where(function ($query) use ($model, $startDate, $endDate) {
            $query->orWhere(function ($query) use ($startDate, $endDate) {
                $query->where('type', 'custom');
                $query->whereBetween('date', [$startDate, $endDate]);
            });

            $query->orWhere(function ($query) use ($model, $startDate, $endDate) {
                $query->where('type', 'default');
                $query->whereNotExists(function ($query) use ($model, $startDate, $endDate) {
                    $query->selectRaw(1);
                    $query->from($model->getTable(), 'inner');
                    $query->whereColumn('user_id', $model->qualifyColumn('user_id'));
                    $query->whereColumn('day', $model->qualifyColumn('day'));
                    $query->whereColumn('location_id', '<>', $model->qualifyColumn('location_id'));
                    $query->where('type', 'custom');
                    $query->whereBetween('date', [$startDate, $endDate]);
                });
            });
        });
    };


    return \App\Models\User::query()
        ->with(['workingLocations' => $constraints])
        ->whereHas('workingLocations', $constraints)
        ->get();
});

Basically it checks if there is a "custom" record within the dates OR there is not a custom record on a different location for the same date.

Hope this helps.

2 likes
rodrigo.pedra's avatar

Note:

I used a closure for the relation constraints so I could apply it both to the eager-loading and the whereHaving for debugging purposes.

But if you don't need to eager load the related model, you can inline it into the whereHaving.

trin's avatar

sleep helps. if now I understand everything correctly (how difficult it is not to know English in 2021), then the problem is solved by join.

  • first, we select by location_id, day and date, for only custom (date not null)
  • left join itself table, with the conditions:
    • a.user_id = b.user_id
    • a.day = b.day
    • a.location_id != b.location_id
  • we accept default locations only if the join returned null

raw sql

SELECT
    a.*
FROM
    locations a
    LEFT JOIN locations b ON (
        a.user_id = b.user_id AND 
        a.location_id != b.location_id AND 
        a.day = b.day
    )
WHERE 
	a.location_id IN (5) AND
    a.day IN (5) AND
	(
        a.date BETWEEN '2021-02-01' AND '2021-02-20' OR 
        (
        	a.date IS NULL AND
            b.id IS NULL
        )
    )

in laravel, wait 5-10 minut, i need to feed my bear. i check your data and what u need to result and query now correct. and EXPLAIN says all right, but my last word about indexes not quite correct. u steel need location_id for first query and day or user_id for second. depends on size of table, count day/user_id etc. in general, you need an index on all the fields that participate in the selection. and the mysql will decide for itself

trin's avatar
$result = DB::table('locations', 'a')
        ->leftJoin('locations as b', [['a.user_id', '=', 'b.user_id'], ['a.day', '=', 'b.day'], ['a.location_id', '!=', 'b.location_id']])
        ->whereIn('a.location_id', $locations)
        ->whereIn('a.day', $days)
        ->where(function($query) use ($startDate, $endDate) {
            $query
                ->whereBetween('a.date', [$startDate, $endDate])
                ->orWhere(function ($query) {
                    $query
                        ->whereNull(['a.date', 'b.id']);
                });
            })
        ->select('a.*')
        ->get();

i am not very friendly with query builder, i will be grateful if the masters will show, how best to write this request in laravel

Binho's avatar
Level 12

Thanks @trin for your precious help. I'll try this shorter request.

Many thanks and have a nice day

Please or to participate in this conversation.