docs(0011): document the SQL function inlining trade-off - #192
Open
sidgaikwad wants to merge 1 commit into
Open
sidgaikwad wants to merge 1 commit into
sidgaikwad wants to merge 1 commit into
Conversation
PostgreSQL will not inline a `language sql` function that carries a `SET` clause, so the remediation this lint recommends turns an inlinable function into a `Function Scan`: the function runs to completion and the caller filters afterwards, instead of the caller's predicate reaching the index. On a 200,000-row table with an index on the filtered column, and two functions whose bodies are identical and fully qualified, the pinned call reads ~8.6x the shared buffers (203 -> 1748) and spills 1,219 temporary blocks the inlined plan never touches. The gap widens with table size. It is the presence of a `SET` clause rather than the empty value, so no formulation of the fix avoids it. Document when this is worth caring about, which is narrower than it first looks: PL/pgSQL functions are never inlined, so pinning costs them nothing, and a SQL function outside a hot path will not notice. For an argument-less SQL function that does rely on predicate pushdown, a view gives the same plan and does not depend on the caller's search path at all -- with the two caveats that a view cannot take arguments and defaults to the owner's privileges. Addresses supabase/supabase#33131
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds two sections to
0011_function_search_path_mutabledocumenting a cost of the remediation this lint recommends, and when it is worth caring about.Addresses supabase/supabase#33131, which reports that fixing
function_search_path_mutablemakes functions dramatically slower. It does, for one specific class of function, and the doc currently says nothing about it.The problem
PostgreSQL will not inline a
language sqlfunction that carries aSETclause. Inlining is what lets the planner fold a simple SQL function into the calling query and push the caller'swhereclause into it. Addingset search_path = ''turns that call into aFunction Scan: the function runs to completion and the caller filters the result afterwards.Measured on PostgreSQL 17.6, 200,000-row table, index on the filtered column, two functions whose bodies are identical and fully qualified so the
SETclause is the only difference:SETclauseBitmap Index Scanset search_path = ''Function Scan,Rows Removed by Filter: 199800~8.6x the shared buffers plus 1,219 temporary blocks the inlined plan never touches, and it widens with table size. Wall-clock varied a lot run to run, so the doc leads with the plan and buffer counts rather than a ratio.
It is the presence of a
SETclause, not the empty value —set search_path = 'some_schema'loses inlining identically — so there is no formulation of the fix that avoids it.Why the doc does not just say "this fix is slow"
The blast radius is much narrower than the issue makes it sound, and that seemed like the more useful thing to write down:
Function Scan. Most functions people write against this lint are PL/pgSQL, and for those the answer is simply "apply the fix".So the doc scopes the warning to the case that actually pays: an argument-less
language sqlfunction called inside a larger query that relies on predicate pushdown. For that case it suggests a view, which produces the identical plan (Bitmap Index Scan, 203 buffers) and, because a view stores resolved references rather than names, does not depend on the caller's search path at all — verified by re-running the plan underset search_path = ''.Two caveats are called out with it, since recommending the rewrite without them would be its own bug: a view cannot take arguments, and a view runs with its owner's privileges unless created with
security_invoker = true(cross-linked to 0010).Deliberately not changed
No lint logic.
0011matches onproconfigalone and does not look atprosecdef, so a SECURITY INVOKER SQL function is flagged the same as a SECURITY DEFINER one even though the escalation vector is weaker. Whether that distinction should change the lint is a policy call for maintainers, not something a docs PR should decide.Verification
Every SQL snippet and every query plan in the new sections was re-run in a clean schema built from scratch. The
create function/create viewstatements execute verbatim, and the quoted plans reproduce exactly, includingRows Removed by Filter: 199800andBuffers: shared hit=1748, temp read=1219 written=1219.bin/compile.pyandbin/check_lints.pyboth pass with no changes tosplinter.sql; the cross-link uses the same[name](NNNN_name.md)form as the existing lint-to-lint links, which the docs site rewrites to?lint=.