Skip to content

pg: prepare+cache query statements - #115

Open
phlip9 wants to merge 4 commits into
lightningdevkit:mainfrom
phlip9:phlip9/pg-prepared-queries
Open

phlip9 wants to merge 4 commits into
lightningdevkit:mainfrom
phlip9:phlip9/pg-prepared-queries

Conversation

@phlip9

@phlip9 phlip9 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Without this change, we would re-parse and re-plan each query, potentially multiple times per request (!). For simple queries, this appears to add ~100-200 us overhead. More for more complex queries.

Instead, maintain a small prepared statement cache per connection. We'll lazily prepare a query statement the first time we execute it, then use the prepared statement thereafter.

This change reduces latency by ~20-60% on my small benchmark suite, but is particularly impactful on batch conditional updates, where we were previously re-parsing and re-planning the put query for each item in the batch. This change improved throughput there by ~2.5x (~4.8k/s -> 12.3k/s).

Using const &'static str queries is really more a stylistic preference on my part. I think it adds a nice roadblock to prevent people from accidentally adding let stmt = format!("..", untrusted_user_input) -> SQL injection.

Migrations and other admin queries are still uncached, since they're usually only executed once.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 3, 2026 •

Copy link
Copy Markdown

I've assigned @tankyleo as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@phlip9

phlip9 commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Oops, looks like the new tinyvec release accidentally broke alloc + no-std. Waiting for Lokathor/tinyvec#226

@tankyleo tankyleo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again sorry for the delay, feel free to rebase.

Comment thread impls/src/postgres_store.rs Outdated
Comment thread impls/src/postgres_store.rs Outdated
Comment thread impls/src/postgres_store.rs Outdated
So I can `POSTGRES_ENDPOINT='postgresql://%2Frun%2Fuser%2F1000' cargo test`
against my user-local, socket-activated dev postgres.
Without this change, we would re-parse and re-plan each query,
potentially multiple times per request (!).

Instead, maintain a small prepared statement cache per connection. We'll
lazily prepare a query statement the first time we execute it, then use
the prepared statement thereafter.

This change reduces latency by ~20-60% on my small benchmark suite, but
is particularly impactful on batch conditional updates, where we were
previously re-parsing and re-planning the put query for each item in the
batch. This change improved throughput there by ~1.5x (~4.8k/s ->
12.3k/s).
@phlip9
phlip9 force-pushed the phlip9/pg-prepared-queries branch from ba56010 to b4c596c Compare September 24, 2026 03:46

let statement = client.prepare(query).await?;
self.statements.insert(query, statement.clone());
Ok(statement)

@tankyleo tankyleo Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reusing the statement here can cause postgres to choose and reuse a generic plan that can perform poorly with varying parameters on our list queries.

From this documentation: https://www.postgresql.org/docs/16/sql-prepare.html

"A prepared statement can be executed with either a generic plan or a custom plan. A generic plan is the same across all executions, while a custom plan is generated for a specific execution using the parameter values given in that call. Use of a generic plan avoids planning overhead, but in some situations a custom plan will be much more efficient to execute because the planner can make use of knowledge of the parameter values. (Of course, if the prepared statement has no parameters, then this is moot and a generic plan is always used.)

By default (that is, when plan_cache_mode is set to auto), the server will automatically choose whether to use a generic or custom plan for a prepared statement that has parameters. The current rule for this is that the first five executions are done with custom plans and the average estimated cost of those plans is calculated. Then a generic plan is created and its estimated cost is compared to the average custom-plan cost. Subsequent executions use the generic plan if its cost is not so much higher than the average custom-plan cost as to make repeated replanning seem preferable."

Let me know what you think.

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.

3 participants