<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: JoongHyuk Shin</title>
    <description>The latest articles on DEV Community by JoongHyuk Shin (@joonghyukshin).</description>
    <link>https://dev.to/joonghyukshin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3911251%2F63b302ae-0e4c-4fa7-916b-72b2a119b393.png</url>
      <title>DEV Community: JoongHyuk Shin</title>
      <link>https://dev.to/joonghyukshin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joonghyukshin"/>
    <language>en</language>
    <item>
      <title>1.1.2 Simple vs Extended</title>
      <dc:creator>JoongHyuk Shin</dc:creator>
      <pubDate>Tue, 05 May 2026 04:35:38 +0000</pubDate>
      <link>https://dev.to/joonghyukshin/112-simple-vs-extended-4l5l</link>
      <guid>https://dev.to/joonghyukshin/112-simple-vs-extended-4l5l</guid>
      <description>&lt;p&gt;The fork visible in 1.1.1 (simple query protocol on one side, extended on the other) is the subject of this section, one level deeper. 1.1.1 set the skeleton: simple is one message, extended is four. The job here is to show how that split translates into four distinct outcomes: plan reuse, parameter safety, pipelining, and error handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Message sequence: the shape of one cycle is different
&lt;/h2&gt;

&lt;p&gt;Putting the message sequences side by side makes the difference visible at a glance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client                          Server
  │                               │
  │── 'Q' (SQL text) ────────────▶│
  │                               │ parse → analyze/rewrite → plan
  │                               │ → create portal → execute → drop portal
  │◀── RowDescription, DataRow*, CommandComplete, ReadyForQuery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Extended&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client                          Server
  │                               │
  │── 'P' (SQL template) ────────▶│ parse + analyze, store prepared statement
  │◀── ParseComplete                
  │                               │
  │── 'B' (parameter values) ────▶│ choose plan, create portal
  │◀── BindComplete                
  │                               │
  │── 'E' (execute) ─────────────▶│ run portal, send rows
  │◀── DataRow*, CommandComplete   
  │                               │
  │── 'S' (Sync) ────────────────▶│ close transaction
  │◀── ReadyForQuery               
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple finishes one cycle in a single message. Extended slices the cycle into four messages, and that slicing is what produces the four capabilities below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability one: execution plans get reused
&lt;/h2&gt;

&lt;p&gt;The central concept that lets extended split the stages is the &lt;strong&gt;prepared statement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A prepared statement is a SQL template that has already been parsed and analyzed. The places where values would go are left blank with placeholders like &lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, and at execution time only the actual values get plugged into those slots. Take &lt;code&gt;INSERT INTO users (id, name) VALUES ($1, $2)&lt;/code&gt;. Once you turn that into a prepared statement, you can run it later by sending only the values: &lt;code&gt;(1, 'Alice')&lt;/code&gt;, &lt;code&gt;(2, 'Bob')&lt;/code&gt;. The full SQL text isn't reparsed each time. Give it a name and it becomes a &lt;strong&gt;named prepared statement&lt;/strong&gt; you can call back during the session. Send it without a name and it's an &lt;strong&gt;unnamed prepared statement&lt;/strong&gt;, automatically discarded the moment the next &lt;code&gt;'P'&lt;/code&gt; arrives.&lt;/p&gt;

&lt;p&gt;The four messages of the extended protocol are exactly that flow, sliced.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;'P'&lt;/code&gt; Parse&lt;/td&gt;
&lt;td&gt;Take the SQL template, finish parse and analysis, store as a prepared statement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;'B'&lt;/code&gt; Bind&lt;/td&gt;
&lt;td&gt;Bind actual parameter values to the prepared statement and prepare for execution (create a portal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;'E'&lt;/code&gt; Execute&lt;/td&gt;
&lt;td&gt;Run the prepared portal and send result rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;'S'&lt;/code&gt; Sync&lt;/td&gt;
&lt;td&gt;End of the cycle, send ReadyForQuery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What this means is that the same prepared statement can be re-executed many times with different parameters by repeating just &lt;code&gt;'B' + 'E'&lt;/code&gt;. Take inserting 1,000 users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Driver pseudocode: 1000 INSERTs via a prepared statement
&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO users (id, name) VALUES ($1, $2)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;conn.prepare(...)&lt;/code&gt; corresponds to a single 'P' message. Parsing and analysis of the SQL text happen there. Each of the 1000 &lt;code&gt;stmt.execute(...)&lt;/code&gt; calls corresponds to a 'B' + 'E' pair. Parse and analyze run only once on the first call; the remaining 999 do bind and execute only. With simple query, the same INSERT text would be sent 1000 times and reparsed 1000 times.&lt;/p&gt;

&lt;p&gt;Internally, a prepared statement is held in a structure called &lt;code&gt;CachedPlanSource&lt;/code&gt;, which keeps the raw parse tree and the analysis result. When the same prepared statement gets another &lt;code&gt;'B' + 'E'&lt;/code&gt;, the backend starts from the saved &lt;code&gt;CachedPlanSource&lt;/code&gt;, only redecides the execution plan, and runs. Parsing and analysis are skipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generic plan vs custom plan
&lt;/h3&gt;

&lt;p&gt;One step further. Plan reuse is real, but to be precise there are two kinds of plan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom plan&lt;/strong&gt;: recomputed every time using the bound parameter values. Helpful when the optimal path differs by value. Take &lt;code&gt;WHERE status = $1&lt;/code&gt;. Suppose &lt;code&gt;status='pending'&lt;/code&gt; matches 1% of rows and &lt;code&gt;status='completed'&lt;/code&gt; matches 99%. A distribution where the value-by-value ratios are this lopsided is what's usually called a &lt;strong&gt;skewed distribution&lt;/strong&gt;. Index scan is fast for 'pending'; sequential scan is fast for 'completed'. Custom plan looks at the value on every call and picks the path that fits it. (Plan construction is the entire subject of chapter 1.4; the kinds and behavior of scan nodes are covered in 1.5.2.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic plan&lt;/strong&gt;: planned once without knowing the parameters and cached. Every EXECUTE from then on reuses the cached plan, so from each call's point of view the cost of "planning this one" is zero. The trade-off is that the same path is forced for every parameter value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL decides between the two on every EXECUTE. The decision function is &lt;code&gt;choose_custom_plan()&lt;/code&gt;, and the default policy is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For the first 5 EXECUTEs, always use a custom plan. Collect actual cost measurements.&lt;/li&gt;
&lt;li&gt;From the 6th onward, compare the average custom plan cost against the generic plan cost. The custom average includes the cost of planning every time, while the generic side has that cost as zero (for the reason above), so the comparison is intentionally asymmetric.&lt;/li&gt;
&lt;li&gt;If generic is cheaper, switch to generic. Otherwise, stay on custom.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The decision can be forced via the &lt;code&gt;plan_cache_mode&lt;/code&gt; GUC. &lt;code&gt;auto&lt;/code&gt; (default) runs the policy above; &lt;code&gt;force_custom_plan&lt;/code&gt; always uses custom; &lt;code&gt;force_generic_plan&lt;/code&gt; always uses generic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Working on another RDBMS engine, the first time I saw the "5 customs, then start comparing" rule I spent a while looking for the reason behind that 5. The conclusion: it's an arbitrary constant. The PG source comment literally says "until we have done at least 5 (arbitrary)". Other engines tend to be stricter with plan cache policy (e.g. lock the first plan in as the generic one) and let you override via a knob, while PG chose to decide dynamically on every call. The result is that a PG prepared statement isn't simply a "plan cache"; it's "automatic switching driven by statistics." This is one reason that even ORM code that uses prepared statements automatically can show much less plan caching than people expect: if a statement is called fewer than 5 times, it gets recomputed as a custom plan every time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Capability two: SQL injection is structurally blocked
&lt;/h2&gt;

&lt;p&gt;In simple query, putting a parameter into a query means embedding the value inside the SQL text, something like &lt;code&gt;f"SELECT * FROM users WHERE id = {user_input}"&lt;/code&gt;. If &lt;code&gt;user_input&lt;/code&gt; is untrusted, you've just opened the door to SQL injection.&lt;/p&gt;

&lt;p&gt;Extended separates the SQL template from the parameter values into different messages. &lt;code&gt;'P'&lt;/code&gt; carries only the template, like &lt;code&gt;SELECT * FROM users WHERE id = $1&lt;/code&gt;. &lt;code&gt;'B'&lt;/code&gt; carries the values that fill those slots, in binary or text form. Those values never go through the SQL parser. They're plugged into the already-parsed plan tree as data.&lt;/p&gt;

&lt;p&gt;When JDBC &lt;code&gt;PreparedStatement&lt;/code&gt;, libpq &lt;code&gt;PQexecParams&lt;/code&gt;, or psycopg2 supports &lt;code&gt;?&lt;/code&gt; or &lt;code&gt;$1&lt;/code&gt; placeholders, that's the path being used internally. The real mechanism for SQL injection prevention lives here. It isn't "remember to escape on the client"; it's a structure where the parser has no chance to interpret a user-supplied value as a SQL token.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability three: messages can be batched (pipelining)
&lt;/h2&gt;

&lt;p&gt;Simple sends ReadyForQuery back the moment each &lt;code&gt;'Q'&lt;/code&gt; is processed. The client can't send the next query until that response arrives. One query equals one round-trip.&lt;/p&gt;

&lt;p&gt;Extended only sends ReadyForQuery when an &lt;code&gt;'S'&lt;/code&gt; (Sync) arrives. That means a sequence like &lt;code&gt;'P', 'B', 'E', 'B', 'E', 'B', 'E', 'S'&lt;/code&gt; can go out as a single batch. 100 INSERTs in one round-trip. In environments with significant network latency (cross-region cloud calls, for instance), the throughput difference is large.&lt;/p&gt;

&lt;p&gt;Built on top of this mechanism, PG 14 introduced an official pipeline mode in libpq (&lt;code&gt;PQpipelineSync&lt;/code&gt;, &lt;code&gt;PQenterPipelineMode&lt;/code&gt;, etc.). The wire-level capability existed before, but the libpq client API for it wasn't clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability four: a partial error doesn't break the whole batch
&lt;/h2&gt;

&lt;p&gt;Simple, on error, immediately sends ErrorResponse plus ReadyForQuery. The cycle closes right away and the backend is ready for the next &lt;code&gt;'Q'&lt;/code&gt;. As noted above, simple is a 1-round-trip structure, so when the backend returns to normal mode there's nothing queued in the buffer behind the failed message. Closing out and waiting for the next &lt;code&gt;'Q'&lt;/code&gt; is enough.&lt;/p&gt;

&lt;p&gt;Where extended runs into real trouble is the batch case. As we saw in capability three, a typical client pushes &lt;code&gt;'B', 'E', 'B', 'E', ..., 'S'&lt;/code&gt; into the wire all at once. Suppose you send 100 INSERTs by pipelining: one 'P', followed by 100 pairs of 'B' + 'E' and a single 'S' all line up in the backend's buffer. While the backend is processing the 1st 'B', the 51st and 70th messages are already sitting in that buffer waiting their turn.&lt;/p&gt;

&lt;p&gt;Now suppose the 50th 'B' fails with something like a unique violation. If the backend behaved like simple (immediately sending ErrorResponse + ReadyForQuery and returning to normal mode), it would pull the 51st 'B' out of the buffer and start processing it next. But that 51st 'B' was sent by the client under the assumption that the first 50 had succeeded. The transaction is already aborted, so processing the 51st errors out too. Same for 52, 53, ..., 100. The client ends up tracking the original error plus 50 more downstream errors.&lt;/p&gt;

&lt;p&gt;PG avoids this chaos with a different strategy. The moment an error occurs, the backend enters a special state called &lt;strong&gt;ignore_till_sync&lt;/strong&gt;. While in that state, every message that arrives is dropped without being processed until the client explicitly sends an &lt;code&gt;'S'&lt;/code&gt; (Sync). No additional error responses go out. Once &lt;code&gt;'S'&lt;/code&gt; arrives, the backend finally sends ReadyForQuery and starts accepting messages normally again.&lt;/p&gt;

&lt;p&gt;The result is that the client receives exactly two responses: one ErrorResponse (the 50th failure) and one ReadyForQuery (in reply to &lt;code&gt;'S'&lt;/code&gt;). A clean boundary forms: "the batch failed somewhere, and everything past that point was discarded." ignore_till_sync is, in essence, the byproduct that makes pipelining safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  All four in one table
&lt;/h2&gt;

&lt;p&gt;Compressing the four capabilities into a single comparison.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Simple&lt;/th&gt;
&lt;th&gt;Extended&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Message count&lt;/td&gt;
&lt;td&gt;1 (&lt;code&gt;'Q'&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;4+ (&lt;code&gt;'P'&lt;/code&gt;, &lt;code&gt;'B'&lt;/code&gt;, &lt;code&gt;'E'&lt;/code&gt;, &lt;code&gt;'S'&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plan reuse&lt;/td&gt;
&lt;td&gt;None (parse + plan every time)&lt;/td&gt;
&lt;td&gt;Yes (&lt;code&gt;CachedPlanSource&lt;/code&gt; + auto generic/custom)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameters&lt;/td&gt;
&lt;td&gt;Inline in SQL text&lt;/td&gt;
&lt;td&gt;Separated as data at bind time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL injection&lt;/td&gt;
&lt;td&gt;Client is responsible for escaping&lt;/td&gt;
&lt;td&gt;Prevented at the protocol level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round-trips&lt;/td&gt;
&lt;td&gt;1 per query&lt;/td&gt;
&lt;td&gt;Batched (1 per Sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error handling&lt;/td&gt;
&lt;td&gt;Immediate ReadyForQuery&lt;/td&gt;
&lt;td&gt;ignore_till_sync (wait until Sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What this means in practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First, don't assume that "the ORM uses prepared statements" means you're getting full plan caching.&lt;/strong&gt; PG plans a custom plan for the first 5 EXECUTEs of every prepared statement. If a statement is called only once or twice inside a short transaction, the plan caching benefit is essentially zero. The real benefit shows up in workloads that call the same prepared statement dozens to hundreds of times with different parameters. The ratio of &lt;code&gt;calls&lt;/code&gt; to &lt;code&gt;plans&lt;/code&gt; in &lt;code&gt;pg_stat_statements&lt;/code&gt;, plus a forced &lt;code&gt;plan_cache_mode&lt;/code&gt; setting, are the two diagnostic tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, the answer to "why isn't my prepared statement going generic?" is the wall of 5.&lt;/strong&gt; Forcing &lt;code&gt;plan_cache_mode = force_generic_plan&lt;/code&gt; brings planning cost to zero but locks every parameter value to the same path. With skewed data this can actually be slower. The opposite, &lt;code&gt;force_custom_plan&lt;/code&gt;, pays planning cost every time. The default &lt;code&gt;auto&lt;/code&gt;, which decides dynamically from the 6th call, is usually safest, but there are environments where explicitly choosing generic is worth the GUC tweak. For example, environments where prepared statements have very short lifetimes due to PgBouncer transaction pooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, in environments with significant network latency, pipelining is the real lever.&lt;/strong&gt; Cross-region RDS calls in the cloud, or even same-region setups where there's millisecond-level latency between application and database, will turn 100 simple INSERTs into 100 round-trips. libpq pipeline mode, or JDBC &lt;code&gt;addBatch()&lt;/code&gt; + &lt;code&gt;executeBatch()&lt;/code&gt;, can collapse that to a single round-trip. Just keep in mind that the error-handling complexity goes up (you need to understand what ignore_till_sync means), so it pays to design batch-level transaction boundaries and a retry policy at the same time as the pipelining itself.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>internals</category>
      <category>protocol</category>
    </item>
    <item>
      <title>1.1.1 Life of a Query</title>
      <dc:creator>JoongHyuk Shin</dc:creator>
      <pubDate>Mon, 04 May 2026 10:13:22 +0000</pubDate>
      <link>https://dev.to/joonghyukshin/111-life-of-a-query-1phg</link>
      <guid>https://dev.to/joonghyukshin/111-life-of-a-query-1phg</guid>
      <description>&lt;p&gt;This section is the map for the rest of the book. The five stages introduced in the 1.1 chapter overview (parse, analyze/rewrite, plan, portal, execute) are traced here through the actual code: which functions implement each stage, and in what order they get called. The mechanics of each of the five stages are unpacked in later chapters. Here, only the skeleton matters: how a backend starts up, how it receives messages, and where the first fork in the road appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  One backend process owns one query
&lt;/h2&gt;

&lt;p&gt;Every time a client connects, PostgreSQL forks a &lt;strong&gt;backend process&lt;/strong&gt; for it (the parent is &lt;code&gt;postmaster&lt;/code&gt;). That process stays alive until the client disconnects, and it handles every query that client sends, by itself. Unlike the thread-pool model common in other RDBMSs, PG uses one OS process per connection. The reasons behind that decision are taken up in 6.1.1.&lt;/p&gt;

&lt;p&gt;The actual entry point of that backend is a function called &lt;code&gt;PostgresMain&lt;/code&gt;. The name is grand; what it does is unexpectedly simple. Two things, then off it goes.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;it installs signal handlers&lt;/strong&gt;. Signals are asynchronous notifications the OS delivers to a process (for example, &lt;code&gt;SIGTERM&lt;/code&gt; is a request to shut down, &lt;code&gt;SIGUSR1&lt;/code&gt; is for PG-internal communication). A backend has to react to signals from &lt;code&gt;postmaster&lt;/code&gt; and from other backends, so each signal is wired to a handler ahead of time. Signals and IPC in general are covered in 6.3.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;it initializes the transaction system&lt;/strong&gt;. Every SQL statement in PG, even without an explicit &lt;code&gt;BEGIN&lt;/code&gt;, runs inside some transaction. The transaction system is the core PG machinery that tracks &lt;code&gt;BEGIN&lt;/code&gt;/&lt;code&gt;COMMIT&lt;/code&gt; boundaries, MVCC visibility, XID assignment, and so on. Transactions and MVCC are the subject of all of chapter 3. For now, it's enough to know that this machinery is set up before the backend ever sees a SQL statement.&lt;/p&gt;

&lt;p&gt;Once those two preparations are done, the real work of the backend begins. An infinite loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(;;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;ReadyForQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;whereToSendOutput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;firstchar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ReadCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;input_message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstchar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PqMsg_Query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;// 'Q', simple query&lt;/span&gt;
            &lt;span class="n"&gt;exec_simple_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PqMsg_Parse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;// 'P', extended: parse&lt;/span&gt;
            &lt;span class="n"&gt;exec_parse_message&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PqMsg_Bind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="c1"&gt;// 'B', extended: bind&lt;/span&gt;
            &lt;span class="n"&gt;exec_bind_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;input_message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PqMsg_Execute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;// 'E', extended: execute&lt;/span&gt;
            &lt;span class="n"&gt;exec_execute_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;portal_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rows&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;PqMsg_Sync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="c1"&gt;// 'S', end of an extended cycle&lt;/span&gt;
            &lt;span class="n"&gt;finish_xact_command&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;send_ready_for_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loop is the entire life of a backend.&lt;/p&gt;

&lt;p&gt;"Announce that I'm ready, read one message, dispatch on its type." Repeat forever. When the client closes the connection, an &lt;code&gt;'X'&lt;/code&gt; (Terminate) message arrives, the loop exits, and the process dies.&lt;/p&gt;

&lt;p&gt;The first fork in the road is visible right here. There's the &lt;code&gt;'Q'&lt;/code&gt; path and the &lt;code&gt;'P' / 'B' / 'E'&lt;/code&gt; path. That split is the difference between the simple query protocol and the extended query protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple vs extended
&lt;/h2&gt;

&lt;p&gt;Simple is the case where a single message contains the SQL text in full. Type &lt;code&gt;SELECT 1;&lt;/code&gt; into &lt;code&gt;psql&lt;/code&gt; and hit enter, and that's what flies across the wire. The backend receives that one message and runs the full five-stage cycle (parse, analyze and rewrite, plan, portal, execute) before returning the result.&lt;/p&gt;

&lt;p&gt;Extended does the same job but splits it into four messages (&lt;code&gt;'P'&lt;/code&gt;, &lt;code&gt;'B'&lt;/code&gt;, &lt;code&gt;'E'&lt;/code&gt;, &lt;code&gt;'S'&lt;/code&gt;). Splitting the stages opens up plan reuse, parameter safety, and pipelining. The semantic differences between the two protocols and how they play out in practice are unpacked in section 1.1.2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizable vs utility
&lt;/h2&gt;

&lt;p&gt;Everything described so far assumes &lt;strong&gt;optimizable statements&lt;/strong&gt;: &lt;code&gt;SELECT/INSERT/UPDATE/DELETE&lt;/code&gt;. These have paths to optimize. The planner decides between sequential and index scan, hash join and nested loop, one join order or another.&lt;/p&gt;

&lt;p&gt;But statements like &lt;code&gt;CREATE TABLE&lt;/code&gt;, &lt;code&gt;VACUUM&lt;/code&gt;, &lt;code&gt;SET&lt;/code&gt;, and &lt;code&gt;BEGIN&lt;/code&gt; (the &lt;strong&gt;utility statements&lt;/strong&gt;) are different. There's nothing for a cost model to optimize. They're DDL or system commands, with no path to choose. In that case the planner produces only an empty shell of a plan and hands the actual work to a utility-statement handler. The executor never gets called on this path.&lt;/p&gt;

&lt;p&gt;The detailed branching is the subject of 1.1.3. The takeaway here is just one thing: not every query in PG goes through the planner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big picture
&lt;/h2&gt;

&lt;p&gt;We can now compress the journey of a SQL line into a single diagram.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   │
   │  'Q' (or 'P' + 'B' + 'E')
   ▼
PostgresMain main loop
   │
   ▼
exec_simple_query
   │
   ├─ pg_parse_query           → raw parse tree     (1.2.1, 1.2.3)
   │
   ├─ pg_analyze_and_rewrite   → list of Query nodes (1.2.2, 1.3)
   │
   ├─ pg_plan_queries          → execution plan      (1.4 chapter)
   │     └─ utility produces an empty shell          (1.1.3)
   │
   ├─ PortalStart + PortalRun  → tuple pulling       (1.5)
   │
   └─ PortalDrop + finish_xact_command
   │
   ▼
ReadyForQuery → back to the top of the loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each box in this diagram corresponds to a chapter in the book. 1.2 is parser and analyzer, 1.3 is rewriter, 1.4 is planner, 1.5 is the executor. All of part 1 is essentially one zoomed-in view of this diagram.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Working on another RDBMS engine, I once found this aspect of PG surprising. PG accepts a multi-statement query like &lt;code&gt;SELECT 1; SELECT 2;&lt;/code&gt; as a single simple-query message. What's even more surprising is the transaction handling. Without an explicit &lt;code&gt;BEGIN&lt;/code&gt;/&lt;code&gt;COMMIT&lt;/code&gt;, all those statements get bundled into a single implicit transaction block, and if even one of them fails, the whole batch rolls back.&lt;/p&gt;

&lt;p&gt;At first I assumed this was just standard behavior. Comparing the client protocols of other major databases made it clear this is a PG-specific decision. MySQL has &lt;code&gt;CLIENT_MULTI_STATEMENTS&lt;/code&gt; off by default, so multi-statement queries are simply rejected (you have to flip the flag explicitly because of SQL injection risk). Even with the flag on, statements are processed sequentially, and because autocommit is the default, each one commits as its own transaction. Oracle accepts only one statement per OCI call, so to bundle multiple statements you have to wrap them in an anonymous PL/SQL block (&lt;code&gt;BEGIN ... END;&lt;/code&gt;). SQL Server accepts multiple statements in a T-SQL batch, but atomic handling still requires an explicit &lt;code&gt;BEGIN TRANSACTION&lt;/code&gt;. None of the three does what PG does: bundle automatically as soon as the message arrives.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this means in practice
&lt;/h2&gt;

&lt;p&gt;This five-stage skeleton turns out to be the foundation for two diagnostic tools you'll use in operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, you can see exactly where EXPLAIN's output comes from.&lt;/strong&gt; &lt;code&gt;EXPLAIN&lt;/code&gt; runs only as far as stage 4 (plan); it skips stage 5 (execute). &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; actually runs through stage 5 and measures it. That's why &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; produces real load and shouldn't be casually run in production: an &lt;code&gt;EXPLAIN ANALYZE UPDATE ...&lt;/code&gt; actually updates rows. The familiar &lt;code&gt;BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK&lt;/code&gt; idiom exists for exactly this reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, the fact that one backend means one process means one query at a time explains why connection pooling matters so much.&lt;/strong&gt; A backend's main loop is essentially single-threaded. While one client runs a long query, that backend can't do anything else. Connection counts therefore drive memory and scheduling costs linearly, and a pooler like PgBouncer becomes effectively mandatory. The answer to "why are PostgreSQL connections so expensive?" lives inside this one-line main loop.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>internals</category>
      <category>protocol</category>
    </item>
    <item>
      <title>1.1 Where Does a Query Go?</title>
      <dc:creator>JoongHyuk Shin</dc:creator>
      <pubDate>Mon, 04 May 2026 09:57:44 +0000</pubDate>
      <link>https://dev.to/joonghyukshin/11-where-does-a-query-go-1bka</link>
      <guid>https://dev.to/joonghyukshin/11-where-does-a-query-go-1bka</guid>
      <description>&lt;p&gt;Suppose a client sends &lt;code&gt;SELECT * FROM users WHERE id = 1&lt;/code&gt;. The path that single line travels before coming back as a result row is longer than you might expect. Inside the PostgreSQL backend, that SQL goes through a five-stage pipeline. The five stages are exactly the five chapters of Chapter 1.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1.1 Where Does a Query Go?&lt;/strong&gt;: the backend decides which processing path the client message should follow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.2 Parser and Analyzer: How SQL Gets Its Meaning&lt;/strong&gt;: the SQL text is parsed, and the catalog is consulted to give it meaning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.3 Rewriter: How a Query is Rewritten&lt;/strong&gt;: the RULE system expands views, injects RLS policies, and otherwise transforms the query tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.4 Planner: Which Path to Take&lt;/strong&gt;: a cost model explores possible execution paths and picks the best one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.5 Executor: How Results Come Back&lt;/strong&gt;: the chosen plan is walked, pulling tuples up and sending them to the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chapter 1.1, the one you're reading, splits into three sections.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1.1.1 Life of a Query&lt;/strong&gt;: compresses all five stages into a single diagram. The map for the rest of the book.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.1.2 Simple vs Extended&lt;/strong&gt;: looks at the semantic difference between the two protocols.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.1.3 Optimizable vs Utility&lt;/strong&gt;: shows how &lt;code&gt;SELECT/INSERT/...&lt;/code&gt; and &lt;code&gt;CREATE/VACUUM/...&lt;/code&gt; take different paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this chapter, it should be clear how the backend's main loop receives a client message and dispatches it to the right function.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>internals</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
