Skip to content

Decide how to handle scalar collections #3269

Description

@roji

When arrays get parameters (e.g. WHERE b.Foo = ANY ($1)), the cardinality of the array can be very important to choosing the right query plans; see dotnet/efcore#32394 for numerous reports of regressions on SQL Server in 8.0, because of the switch from constants to OPENJSON there.

In PG we didn't have a translation change in 8.0 - the provider always parameterized arrays. However, the same sort of problem (inefficient plans due not taking array cardinality into account) can occur with PG as well; if Npgsql is configured to do auto preparation (which is recommended), then the query SQL may end up getting a generic plan; at that point, the query is stuck with a plan that has some arbitrary array cardinality (based on the first few executions presumably).

We could mitigate this by forcing PG to use custom plans as soon as an array is parameterized, by injecting SET plan_cache_mode=force_custom_plan (docs) before the query, and RESET plan_cache_mode after (while still batching for a single roundtrip).

Thanks to @laurenz and @NinoFloris for the discussions around this (if you have anything to add to the above please comment).

Activity

  1. added this to the 9.0.0 milestone on Sep 11, 2024
  2. self-assigned this
    on Sep 11, 2024
  3. modified the milestones: 9.0.0, 10.0.0 on Oct 27, 2024
  4. added theissue type on Jun 14, 2025
  5. changed the title [-]Stop PG from using a generic plan when an array is parameterized[/-] [+]Decide how to handle scalar collections[/+] on Jul 5, 2025
  6. roji commented on Jul 5, 2025

    @roji
    MemberAuthor

    After thinking about this some more (and discussing with @NinoFloris)...

    Forcing a custom plan would mean that queries get planned every time they're executed; this is a bit similar to SQL Server's RECOMPILE hint, which we considered for a while but then decided against, since planning costs are high and there are many array usages which use roughly the same number of elements and could regress significantly.

    So it may make sense for EFCore.PG to follow the other EF providers and translate to multiple parameters - or possibly to keep the current single parameter but without forcing a custom plan (as has been the case since the beginning).

  7. NinoFloris commented on Jul 5, 2025

    @NinoFloris
    Member

    Yes though just for posterity, outputs from parsing and rewriting (expanding views and other catalog constructs into their actual sql) are reused for a custom plan. Only planning will be done from scratch to incorporate the concrete values into the planning criteria.

  8. laurenz commented on Jul 6, 2025

    @laurenz

    If there is no "one size fits all" solution, perhaps documentation can help.
    To PostgreSQL users who experience bad performance because they get the same generic plan for vastly different array parameters, you could recommend to try setting plan_cache_mode = force_custom_plan in their database configuration.
    PostgreSQL's query optimizer is quite fast, so unless all your statements are in the millisecond range, you might not notice the overhead.

  9. roji commented on Jul 7, 2025

    @roji
    MemberAuthor

    If there is no "one size fits all" solution, perhaps documentation can help.

    Yeah, this definitely seems like one of these cases. And EF already will allow users to opt into multiple parameters expansion - globally or for specific queries - if they want to (so WHERE x IN ($1, $2, $3...) instead of WHERE x = ANY ($1).

    you could recommend to try setting plan_cache_mode = force_custom_plan in their database configuration.

    True, though that would add the planning overhead to all queries, not just to those with array parameters.

    PostgreSQL's query optimizer is quite fast, so unless all your statements are in the millisecond range, you might not notice the overhead.

    Right - that's the main question here. If the overhead plan_cache_mode = force_custom_plan is really small, then it probably makes sense for EF to always set it when it sees that there's an array parameter (i.e. WHERE x = ANY ($1)); this would be much more targeted than telling users to the option globally in the database configuration.

    In the SQL Server case, we've been told that the analogous thing (RECOMPILE query hint) could regress performance in a significant way, since planning costs apparently aren't trivial (and so we shouldn't force constant re-planning, since many cases don't actually have vastly different array parameter cardinality). We also followed the "common sense" of not having EF integrate query hints in the SQL, as these are usually last-resort tweaks that users should do themselves only after careful consideration etc. (and not used automatically by an ORM).

    Would you say the same trade-offs/calculations work for PG as well? If re-planning really is negligible for the vast majority of cases, then it indeed maybe would make sense for EF to set plan_cache_mode = force_custom_plan automatically for queries with array parameters (though there's still a part of me that thinks maybe users should do this explicitly rather than the ORM doing it for them).

  10. laurenz commented on Jul 7, 2025

    @laurenz

    Well, PostgreSQL hardly ever cached execution plans, so there is a premium on the optimizer being fast.
    However, if your use case is lots of tiny, similar statements, the overhead of query planning can be significant.
    I personally feel that guessing a good default is fine, but I am against forcing the user to go with the choice.
    So how about running with plan_cache_mode = force_custom_plan by default, but giving the user a choice to override that setting?

  11. roji commented on Jul 7, 2025

    @roji
    MemberAuthor

    We could probably do that... Thanks for all the input.

    I'll look at all this at some point after August (.NET/EF go into a feature freeze at that point and I'll have more time.

  12. modified the milestones: 10.0.0, 11.0.0 on Nov 22, 2025
  13. modified the milestones: 11.0.0, Backlog on Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions