PostgreSQL Extended Query Lifecycle
This page walks through the complete journey of a PostgreSQL prepared statement as it moves through ProxySQL — from the initial Parse message sent by a client, through one or more Bind/Execute cycles, and finally to Close. All behaviour described here applies to the PostgreSQL Extended Query Protocol as supported in ProxySQL v3.0.3+.
Preparation Phase
When a client sends a Parse message, ProxySQL intercepts it before it reaches any backend server.
Hash Computation
ProxySQL computes a content hash (digest) that uniquely identifies the prepared statement within a specific context. The hash is derived from:
| Input | Included |
|---|---|
| SQL query text | Yes |
| Logged-in user | Yes |
| Target schema (database) | Yes |
Protocol-level parameter types from the Parse message | Yes |
Including parameter types is specific to the PostgreSQL protocol. The same SQL text prepared with int4 parameters and again with text parameters will produce different digests and therefore separate global cache entries, because the backend treats them as distinct prepared statements with potentially different execution plans.
Global Cache Lookup
After computing the hash, ProxySQL performs a lookup against the global prepared statement cache — a process-wide structure shared across all client connections.
Cache hit — an equivalent statement has been prepared before:
- Retrieve the existing global entry and its global ID.
- Record a mapping from the client’s statement name to that global ID in the per-connection client map.
- Increment the client reference count on the global entry.
- Return
ParseCompleteto the client immediately.
No backend connection is contacted. The client receives a valid response without any indication that the result came from the cache rather than a live backend interaction.
Cache miss — no matching entry exists:
- Forward the
Parsemessage to a backend connection selected via query rules (or the default hostgroup). ProxySQL rewrites the statement name toproxysql_ps_{n}before sending. - Receive
ParseCompletefrom the backend. - Create a new global entry in the cache, storing the SQL text, parameter type metadata, and a newly assigned global ID.
- Record the backend-side name (
proxysql_ps_{n}) in the backend map for that connection, keyed by the global ID. - Map the client’s original statement name to the new global ID in the per-connection client map.
- Return
ParseCompleteto the client.
In both cases the client receives a standard ParseComplete response and is unaware of what path was taken internally.
Execution Phase
Execution in the PostgreSQL Extended Query Protocol consists of a Bind message followed by an Execute message. ProxySQL queues these messages as they arrive and processes them together when a Sync message is received.
Statement Name Translation
When processing the queued messages on Sync, ProxySQL performs the following translation steps:
-
Resolve client name to global ID: Look up the statement name from the
Bindmessage in the per-connection client map to obtain the global ID. If no mapping exists, the statement was never prepared in this session and an error is returned. -
Select a backend connection: Choose a connection from the pool of the target hostgroup, determined by re-evaluating query rules against the statement’s SQL text and the current session context.
-
Resolve global ID to backend name: Look up the global ID in the backend map for the selected connection to find the
proxysql_ps_{n}name scoped to that connection. -
Rewrite and forward: Replace the client’s statement name in the
Bindmessage with the backend-localproxysql_ps_{n}name and forward theBindandExecutemessages to the backend.
Lazy Preparation
When step 3 finds that the selected backend connection has no entry for this global ID — because the connection is newly established, was recycled, or this is the first time the statement has been routed to this hostgroup — ProxySQL performs lazy preparation transparently before forwarding the Bind:
- Using the SQL text and parameter type metadata stored in the global cache entry, send a new
Parsemessage to the backend with a freshproxysql_ps_{n}name scoped to that connection. - Receive
ParseCompletefrom the backend. - Record the new
proxysql_ps_{n}name in the backend map for this connection. - Proceed with the
BindandExecuteas normal.
Lazy preparation is transparent to the client. The Bind/Execute response arrives without any indication that an additional Parse was sent to the backend. This mechanism means ProxySQL never needs to pre-populate all backend connections with every known statement; it prepares each statement on a given backend only when execution is actually routed there.
Close Phase
When a client sends a Close message targeting a statement name, ProxySQL handles teardown at the client level without contacting any backend.
Client-Side Cleanup
- Remove the client’s statement name from the per-connection client map.
- Decrement the client reference count on the corresponding global cache entry.
Backend Statement Retention
Backend connections are not contacted during a client Close. The statement remains prepared on any backend connections that have it in their backend map. This is intentional:
- Other active client connections may be executing the same statement concurrently.
- Keeping the backend statement alive means future clients can route executions to those backends without incurring a lazy preparation round-trip.
- Sending a
Closeto every backend that holds the statement would add latency and protocol overhead for little benefit.
Global Entry Retention
The global cache entry is retained as long as either reference count is greater than zero:
| Condition | Result |
|---|---|
| Client reference count > 0 | Entry is actively used by at least one client session; retained |
| Backend reference count > 0 | Entry is prepared on at least one backend connection; retained |
| Both reference counts reach zero | Entry becomes eligible for eviction from the global cache |
Once evicted, the next Parse for an equivalent statement (same SQL, user, schema, and parameter types) will go through the cache-miss path again.
Error Scenarios
Backend Connection Lost
If a backend connection is dropped due to a network failure, server restart, or connection pool eviction, the global cache entry is unaffected. The backend map entries for the lost connection are discarded along with the connection itself. On the next execution that routes to a new backend connection from the same hostgroup, lazy preparation runs automatically using the metadata stored in the global cache. From the client’s perspective, execution proceeds normally after any transparent reconnection; the client does not need to re-send Parse.
Schema Changes After Preparation
If a DDL statement such as ALTER TABLE or DROP COLUMN is executed against a table referenced by a cached prepared statement, the metadata stored in the global cache may no longer match the backend’s current schema. When ProxySQL detects a metadata mismatch — indicated by an error response from the backend during execution — it refreshes the cached metadata by re-preparing the statement on the backend and updating the global entry. Subsequent executions use the refreshed metadata.
Routing to a Different Backend
Because query rules are re-evaluated at execution time rather than being locked at preparation time, a Bind/Execute sequence may be routed to a different hostgroup or backend connection than the one used during the original Parse. ProxySQL handles this transparently:
- Select a connection from the newly determined hostgroup.
- Check the backend map for that connection.
- If the statement is not yet prepared on that connection, perform lazy preparation using the global cache metadata.
- Rewrite the
Bindmessage with the new backend-local name and forward normally.
This allows correct routing of prepared statements into transactions, to read replicas, or to sharded hostgroups, without requiring the client to re-send Parse.