Skip to content

perf: Avoid hitting the database when queries can be statically analyzed as never returning any results - #4780

Closed
s3cur3 wants to merge 1 commit into
elixir-ecto:masterfrom
s3cur3:ty/skip-database-for-empty-in
Closed

perf: Avoid hitting the database when queries can be statically analyzed as never returning any results#4780
s3cur3 wants to merge 1 commit into
elixir-ecto:masterfrom
s3cur3:ty/skip-database-for-empty-in

Conversation

@s3cur3

@s3cur3 s3cur3 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

I often find myself writing code like this:

def get_users_by_ids(ids)
def get_users_by_ids([]), do: []

def get_users_by_ids(ids) do
  from(u in User, where: u.id in ^ids)
  |> Repo.all()
end

Today, if I left off that first function head, Ecto would still hit the database with a query like SELECT * FROM users WHERE false. Obviously that's not a heavy query, but to the extent that I can avoid putting trivial load on my database (not to mention the round-trip query time), I would prefer to do so. Furthermore, it would be wonderful if Ecto would do this short-circuiting for me, so that I don't need to add those function heads to check for empty queries myself.

To that end, this PR adds a gate to Ecto.Repo.Queryable's stream/3, aggregate/{4,5}, and execute/4 functions to first check whether the WHERE clauses can be statically shown to be unsatisfiable, and if so, return the appropriate empty result.

(Note that the diff looks quite large, but the delta to those existing functions is largely whitespace.)

…zed as never returning any results

I often find myself writing code like this:

```
def get_users_by_ids(ids)
def get_users_by_ids([]), do: []

def get_users_by_ids(ids) do
  from(u in User, where: u.id in ^ids)
  |> Repo.all()
end
```

Today, if I left off that first function head, Ecto would still hit the database with a query like `SELECT * FROM users WHERE false`. Obviously that's not a heavy query, but to the extent that I can avoid putting trivial load on my database (not to mention the round-trip query time), I would prefer to do so.

To that end, this PR adds a gate to `Ecto.Repo.Queryable`'s `stream/3`, `aggregate/{4,5}`, and `execute/4` functions to first check whether the WHERE clauses can be statically shown to be unsatisfiable, and if so, return the appropriate empty result.

(Note that the diff looks quite large, but the delta to those existing functions is largely whitespace.)
@s3cur3
s3cur3 force-pushed the ty/skip-database-for-empty-in branch from 66736e1 to 04b5e9a Compare August 10, 2026 12:11
@s3cur3
s3cur3 marked this pull request as ready for review August 10, 2026 12:15
@josevalim

Copy link
Copy Markdown
Member

Thanks @s3cur3! I am not sure we should go down this road because it only works from time to time. The smarter we are, the more surprising it becomes to understand when it works and when it doesn't. Perhaps we could support straight-forward patterns, like where: false, but I wouldn't go much beyond that. And even then we need to be careful when it is joined with an or.

@s3cur3

s3cur3 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Can you elaborate on "it only works from time to time"? I guess to me it feels a little like a compiler optimization... one of those things that's not guaranteed, but to the extent that the compiler can help you out, it's nice when it does.

Re: joining with an or, I believe that's covered—see test "correctly queries the database when empty in is or-ed with other filters".

@josevalim

Copy link
Copy Markdown
Member

per your own description, it is something you are actively aiming for, so we all stand to gain by making it clearer to understand when it happens vs hoping the compiler does it. It is also easier to maintain.

@gmile

gmile commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

FWIW, rather then not executing the queries silently, I'd consider logging a warning as more useful and potentially actionable signal. Just saying.

@Schultzer

Copy link
Copy Markdown
Contributor

I think what José is trying to get at is, if we go down this road we end up taking the job of the database query planner and execution. That can be a nontrivial amount of complexity as different implementation might diverge from your initial expectations, especially given join on clauses and wheres. You could imagine some queries returning results, but with this they wouldn’t as it never gets to the database.

@s3cur3 s3cur3 closed this Aug 10, 2026
@s3cur3

s3cur3 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Okay, fair enough. We'll put it in the app layer. ☺️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants