| |

SQL 130 🛢️ Database Driver Mechanics and Wire Protocols (PostgreSQL Protocol, MySQL Protocol)

⏱️ Estimated read: 14 min | |

A database driver is a translator. The application speaks in function calls and typed objects; the database speaks in a binary protocol over a socket. The wire protocol is the contract between them: a sequence of messages with defined framing, a startup handshake that negotiates capabilities and authenticates the user, and a query phase that differs significantly between simple and extended execution. PostgreSQL and MySQL implement different protocols with different design philosophies. PostgreSQL’s protocol is message-oriented and explicitly separates parsing, binding, and execution in its extended query mode. MySQL’s protocol has a connection phase and a command phase, with prepared statements handled through a separate binary protocol. Understanding these mechanics explains why connection poolers behave the way they do, why prepared statements sometimes perform differently than expected, and why a driver’s choice of protocol mode affects latency.

Key point: PostgreSQL’s extended query protocol splits a statement into Parse, Bind, and Execute messages, which allows prepared statements to be reused and parameters to be bound separately from the query text. MySQL’s prepared statement protocol uses a similar prepare-execute cycle but is a distinct binary protocol from the text-based simple query path.


Why wire protocols matter to application developers

The prepared statement problem. A driver that uses the extended query protocol can prepare a statement once and execute it many times with different parameters. The server parses the statement once, caches the plan, and binds new values on each execution. A driver that uses the simple query protocol concatenates the query text and parameters into a single string and sends it for parsing every time. The performance difference is small for a single query but significant for an application that executes the same statement thousands of times per second. The choice of protocol mode is a driver configuration decision, and it determines which behavior the application gets.

The connection pooler problem. A connection pooler sits between the application and the database, multiplexing many client connections onto a smaller number of server connections. The pooler must understand the protocol to some degree. If the application uses the extended query protocol and creates named prepared statements, those statements exist on a specific server connection. When the pooler reassigns that connection to a different client, the client may see prepared statements it did not create, or its own prepared statements may be missing. This is why transaction-level pooling requires either the unnamed prepared statement or a protocol-aware pooler that tracks statement state.

The type mapping problem. PostgreSQL’s protocol supports both text and binary formats for parameter values and result columns. The format is specified per-value in the Bind message. A driver that sends parameters in binary avoids the server’s text-to-type conversion and the client’s string formatting, which reduces CPU overhead for numeric and temporal types. A driver that sends everything as text is simpler to implement but pays a conversion cost on every value. The protocol allows the choice; the driver makes it.


a. PostgreSQL Protocol: Startup and Simple Query

The PostgreSQL protocol begins with a startup phase. The client sends a startup message that includes the protocol version and connection parameters such as the username and database name. The server responds with an authentication request, and the client completes the authentication exchange. After authentication succeeds, the server sends ParameterStatus messages for runtime parameters, BackendKeyData for cancellation, and finally ReadyForQuery to signal that the connection is ready for commands .

Startup message: protocol version 3.0, user, database
Server: Authentication request (MD5, SCRAM-SHA-256, etc.)
Client: Authentication response
Server: AuthenticationOk, ParameterStatus, BackendKeyData, ReadyForQuery

Every subsequent message has a one-byte type code followed by a four-byte length and the message payload. The startup message is the only exception; it has no type byte for historical reasons . This framing is what allows a client to read an entire message into a buffer before processing it, which makes error recovery possible.

The simple query protocol is the direct path. The client sends a Query message containing a text string, and the server responds with RowDescription, zero or more DataRow messages, and CommandComplete, followed by ReadyForQuery. A simple query can contain multiple SQL statements separated by semicolons, and the server processes them sequentially, sending a separate response sequence for each .

-- Simple query: the entire text is sent at once
SELECT * FROM users WHERE id = 1;

The limitation is that parameters cannot be bound. The query text must contain the literal values, which means the client must construct the SQL string. This is the mechanism that makes SQL injection possible if the values are not properly escaped.


b. PostgreSQL Extended Query: Parse, Bind, Execute

The extended query protocol breaks the simple query cycle into distinct steps. The client sends a Parse message containing a query string and optionally the data types of parameter placeholders. The server responds with ParseComplete or ErrorResponse. The parsed statement is stored as a prepared statement, either named or unnamed. A named statement lasts for the session; an unnamed statement lasts only until the next Parse message .

The Bind message creates a portal from a prepared statement. It supplies the parameter values and specifies the format (text or binary) for each parameter and for the result columns. The server responds with BindComplete. The portal is the ready-to-execute object, and it lasts until the end of the transaction for named portals, or until the next Bind for the unnamed portal .

The Execute message runs a portal. It specifies a maximum row count, which allows fetching results in batches for large result sets. The server responds with the result rows and a CommandComplete message. The Sync message closes the extended query cycle and produces a ReadyForQuery .

Parse("SELECT * FROM users WHERE id = $1") → ParseComplete
Bind(portal, statement, [42])              → BindComplete
Execute(portal)                            → DataRow, CommandComplete
Sync                                       → ReadyForQuery

The protocol allows the client to send Parse, Bind, and Execute in a single batch without waiting for the intermediate responses. This is called pipelining, and it reduces the number of network round trips. The Sync message is what forces the server to process the batch and return the results .

The extended query protocol is what makes prepared statements efficient. The server parses the statement once, and subsequent Bind messages can supply different parameter values without re-parsing. The server may also cache the query plan, though it will only reuse a generic plan if the plan is not significantly less efficient than a parameter-specific plan .


c. MySQL Protocol: Connection Phase and Prepared Statements

The MySQL protocol has two phases: the connection phase and the command phase. In the connection phase, the server sends a handshake packet with its version, connection ID, capability flags, and a random salt. The client responds with its capability flags, username, authentication response, and database name. The server validates the authentication and sends an OK or Error packet. If the connection is successful, the command phase begins .

Server: Handshake (version, capabilities, salt)
Client: HandshakeResponse (capabilities, username, auth response, database)
Server: OK packet

The command phase is a request-response cycle. The client sends a command packet with a command byte (such as COM_QUERY for a text query or COM_STMT_PREPARE for a prepared statement), and the server responds with the appropriate packets. The text protocol sends the query as a string and receives results as text strings. The binary protocol is used for prepared statements, where parameter values and result columns are transmitted in a binary format .

COM_QUERY("SELECT * FROM users") → Result set packets, EOF/OK
COM_STMT_PREPARE("SELECT * FROM users WHERE id = ?") → Prepare OK (statement ID, parameter count, column count)
COM_STMT_EXECUTE(statement ID, parameters) → Binary result set packets

The prepared statement flow in MySQL is similar to PostgreSQL’s extended query protocol but is a distinct protocol path. The client sends COM_STMT_PREPARE with the query string, and the server responds with a statement ID and metadata about the parameters and result columns. The client then sends COM_STMT_EXECUTE with the statement ID and the parameter values. The server executes the statement and returns the results in binary format .

The MySQL C API exposes this through mysql_stmt_prepare(), mysql_stmt_bind_param(), and mysql_stmt_execute(). The MYSQL_BIND structure defines the type and buffer for each parameter, and the buffer_type field determines how the value is interpreted . The binary protocol is what allows MySQL to avoid the text conversion overhead for prepared statements.

The connection pooling problem is slightly different in MySQL. The protocol is stateful within a session: temporary tables, user variables, and prepared statements are session-scoped. A connection pooler that reassigns a server connection to a different client must either reset the session state or track which state belongs to which client. The MySQL protocol does not have a built-in mechanism for session state transfer, which is why transaction-level pooling with MySQL requires careful handling of prepared statements and session variables.


Complete Example Session

// ============================================
// PART 1: POSTGRESQL STARTUP SEQUENCE
// ============================================
Client → StartupMessage(protocol 3.0, user=app, database=prod)
Server → AuthenticationSASL (SCRAM-SHA-256)
Client → SASLInitialResponse, SASLResponse
Server → AuthenticationOk, ParameterStatus, BackendKeyData, ReadyForQuery


// ============================================
// PART 2: POSTGRESQL SIMPLE QUERY
// ============================================
Client → Query("SELECT count(*) FROM users")
Server → RowDescription, DataRow, CommandComplete, ReadyForQuery


// ============================================
// PART 3: POSTGRESQL EXTENDED QUERY — PARSE
// ============================================
Client → Parse("stmt1", "SELECT * FROM users WHERE id = $1", [int4])
Server → ParseComplete


// ============================================
// PART 4: POSTGRESQL EXTENDED QUERY — BIND
// ============================================
Client → Bind("portal1", "stmt1", [42])
Server → BindComplete


// ============================================
// PART 5: POSTGRESQL EXTENDED QUERY — EXECUTE
// ============================================
Client → Execute("portal1", 0)
Server → DataRow(id=42, name=Alice), CommandComplete("SELECT 1")
Client → Sync
Server → ReadyForQuery


// ============================================
// PART 6: POSTGRESQL EXTENDED QUERY — REUSE
// ============================================
// The prepared statement "stmt1" is already parsed
Client → Bind("portal2", "stmt1", [99])
Server → BindComplete
Client → Execute("portal2", 0)
Server → DataRow(id=99, name=Bob), CommandComplete
Client → Sync
Server → ReadyForQuery


// ============================================
// PART 7: MYSQL CONNECTION HANDSHAKE
// ============================================
Server → Handshake(version=8.0, salt, capabilities)
Client → HandshakeResponse(user=app, auth=..., database=prod)
Server → OK


// ============================================
// PART 8: MYSQL TEXT QUERY
// ============================================
Client → COM_QUERY("SELECT * FROM users")
Server → Column definitions, Row data, EOF


// ============================================
// PART 9: MYSQL PREPARED STATEMENT
// ============================================
Client → COM_STMT_PREPARE("SELECT * FROM users WHERE id = ?")
Server → PrepareOK(statement_id=1, params=1, columns=2)
Client → COM_STMT_EXECUTE(statement_id=1, id=42)
Server → Binary result set


// ============================================
// PART 10: DRIVER CONFIGURATION IMPLICATIONS
// ============================================
-- PostgreSQL driver: prefer extended query for prepared statements
-- MySQL driver: use binary protocol for prepared statements
-- Pooler: transaction-level pooling requires protocol awareness

The ten parts trace the two protocols from startup through query execution. Parts 3 through 6 show the PostgreSQL extended query cycle and the reuse of a parsed statement. Parts 8 and 9 show the MySQL text and binary paths.


Quick Reference

Protocol Comparison

AspectPostgreSQLMySQL
StartupStartupMessage, authenticationHandshake, HandshakeResponse
Simple queryQuery message (text)COM_QUERY (text)
Prepared queryParse, Bind, ExecuteCOM_STMT_PREPARE, COM_STMT_EXECUTE
Parameter bindingSeparate from query textSeparate from query text
Result formatText or binary (per Bind)Text (simple) or binary (prepared)
Message framingType byte + length + payloadCommand byte + payload

PostgreSQL Extended Query Messages

MessageDirectionPurpose
ParseClientCreate prepared statement
BindClientCreate portal with parameters
ExecuteClientRun portal
SyncClientEnd cycle, get ReadyForQuery
ParseCompleteServerStatement created
BindCompleteServerPortal created
DataRowServerResult row

Driver Configuration Implications

ConcernPostgreSQLMySQL
Prepared statement reuseExtended query protocolBinary protocol
Binary parameter formatBind format codesCOM_STMT_EXECUTE types
Pooler compatibilityUnnamed statements for transaction poolingSession state reset required

Best Practices

✅ Do This:

-- Use extended query protocol for repeated statements
-- The server parses once and reuses the plan                      -- ✅
-- Use binary format for numeric and temporal parameters
-- Avoids text conversion on both client and server               -- ✅
-- For connection poolers, use unnamed statements
-- Named statements are bound to a specific server connection     -- ✅

❌ Don’t Do This:

-- Concatenate parameters into the query string
-- This is the simple query path and invites injection           -- ❌
-- Assume named prepared statements survive connection pooling
-- The statement may not exist on the reassigned connection      -- ❌
-- Use text format for all values in high-throughput paths
-- The conversion cost is measurable                              -- ❌

Common Pitfalls

PitfallWhy It HappensFix
Prepared statement not found after poolingNamed statement bound to old connectionUse unnamed statements or transaction-aware pooler
High CPU from type conversionText format for numeric valuesUse binary format in Bind
Driver uses simple query for prepared statementsDriver not configured for extended protocolCheck driver options
MySQL prepared statement slowBinary protocol overhead for one-off queriesUse text protocol for single execution

Real-World Examples

1. PostgreSQL Startup Message

StartupMessage: protocol 3.0, user=app, database=prod

2. PostgreSQL Parse

Parse("stmt1", "SELECT * FROM users WHERE id = $1", [int4])

3. PostgreSQL Bind

Bind("portal1", "stmt1", [42], result_format=binary)

4. PostgreSQL Execute

Execute("portal1", max_rows=0)

5. PostgreSQL Sync

Sync → ReadyForQuery

6. MySQL Handshake

Server: Handshake(version, salt, capabilities)
Client: HandshakeResponse(user, auth, database)

7. MySQL Text Query

COM_QUERY("SELECT * FROM users")

8. MySQL Prepare

COM_STMT_PREPARE("SELECT * FROM users WHERE id = ?")

9. MySQL Execute

COM_STMT_EXECUTE(statement_id=1, id=42)

10. Binary Format Parameter

Bind format code 1 (binary) for int4 parameter

Visual

PostgreSQL Extended Query Flow

┌──────────────────────────────────────────────────────────────┐
│  CLIENT                              SERVER                  │
│                                                              │
│  ┌─────────────────┐                                        │
│  │ Parse("stmt1",  │───────►│ ParseComplete                  │
│  │ "SELECT...$1")  │        │ (statement parsed)             │
│  └────────┬────────┘        └─────────────────┘             │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Bind("portal1", │───────►│ BindComplete                   │
│  │ "stmt1", [42])  │        │ (portal created)               │
│  └────────┬────────┘        └─────────────────┘             │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Execute(        │───────►│ DataRow, CommandComplete       │
│  │ "portal1")      │        │ (results returned)             │
│  └────────┬────────┘        └─────────────────┘             │
│           ▼                                                  │
│  ┌─────────────────┐                                        │
│  │ Sync            │───────►│ ReadyForQuery                  │
│  └─────────────────┘        └─────────────────┘             │
│                                                              │
│  Parse happens once. Bind and Execute can repeat.            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

MySQL Protocol Phases

┌──────────────────────────────────────────────────────────────┐
│  CONNECTION PHASE                    COMMAND PHASE           │
│                                                              │
│  ┌─────────────────┐                ┌─────────────────┐     │
│  │ Handshake       │                │ COM_QUERY       │     │
│  │ (server→client) │                │ (text)          │     │
│  └────────┬────────┘                └─────────────────┘     │
│           ▼                                                  │
│  ┌─────────────────┐                ┌─────────────────┐     │
│  │ HandshakeResponse│               │ COM_STMT_PREPARE│     │
│  │ (client→server) │                │ (binary)        │     │
│  └────────┬────────┘                └─────────────────┘     │
│           ▼                                                  │
│  ┌─────────────────┐                ┌─────────────────┐     │
│  │ OK/Error        │                │ COM_STMT_EXECUTE│     │
│  │ (auth result)   │                │ (binary)        │     │
│  └─────────────────┘                └─────────────────┘     │
│                                                              │
│  Connection phase negotiates capabilities and authenticates. │
│  Command phase handles queries.                              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Pooler Problem

┌──────────────────────────────────────────────────────────────┐
│  CLIENT A                      POOLER                      │
│  ┌─────────────────┐          ┌─────────────────┐         │
│  │ Parse("stmtA")  │─────────►│ Server conn 1   │         │
│  │ Bind, Execute   │          │ stmtA exists    │         │
│  └─────────────────┘          └────────┬────────┘         │
│                                        │                    │
│  CLIENT B                              │ connection        │
│  ┌─────────────────┐                   │ reassigned        │
│  │ Parse("stmtB")  │◄──────────────────┘                    │
│  │                 │          ┌─────────────────┐         │
│  │                 │          │ Server conn 1   │         │
│  │                 │          │ stmtA exists    │         │
│  │                 │          │ stmtB unknown?  │         │
│  └─────────────────┘          └─────────────────┘         │
│                                                              │
│  Named statements are bound to the server connection.        │
│  Transaction pooling must use unnamed statements or          │
│  a pooler that tracks statement state.                       │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Text vs. Binary Format

┌──────────────────────────────────────────────────────────────┐
│  TEXT FORMAT                    BINARY FORMAT                │
│                                                              │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ Parameter: 42   │            │ Parameter: 42   │         │
│  │ as "42" string  │            │ as 4-byte int   │         │
│  └────────┬────────┘            └────────┬────────┘         │
│           ▼                              ▼                   │
│  ┌─────────────────┐            ┌─────────────────┐         │
│  │ Server parses   │            │ Server uses     │         │
│  │ string to int   │            │ value directly  │         │
│  └─────────────────┘            └─────────────────┘         │
│                                                              │
│  Text is simpler. Binary is faster for numeric and temporal. │
│  Bind format codes select per-parameter.                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
PostgreSQL protocolMessage-oriented, type byte + length + payload
Simple queryText query string, multiple statements allowed
Extended queryParse, Bind, Execute, Sync
Prepared statementNamed or unnamed; named lasts for session
PortalReady-to-execute object; named lasts for transaction
MySQL protocolConnection phase + command phase
MySQL simple queryCOM_QUERY, text format
MySQL preparedCOM_STMT_PREPARE, COM_STMT_EXECUTE, binary format
Format codesText (0), Binary (1) in Bind
Pooler issueNamed statements bound to server connection

Key takeaways:

  • PostgreSQL’s extended query protocol is what makes prepared statements efficient. The Parse message parses the statement once, and subsequent Bind messages supply new parameter values without re-parsing. The Execute message runs the portal, and Sync closes the cycle .
  • MySQL separates connection and command phases. The connection phase negotiates capabilities and authenticates; the command phase handles queries. Prepared statements use a binary protocol (COM_STMT_PREPARE, COM_STMT_EXECUTE) that is distinct from the text-based COM_QUERY path .
  • The format code in PostgreSQL’s Bind message controls text versus binary transmission. Binary avoids the conversion overhead for numeric and temporal types, but requires the driver to encode values correctly. Text is simpler and portable .
  • Named prepared statements are bound to a server connection. When a connection pooler reassigns a server connection to a different client, the client may see statements it did not create or lose access to its own. Transaction-level pooling requires unnamed statements or a protocol-aware pooler.
  • The protocol is the contract between driver and database. The driver’s choice of simple versus extended query, text versus binary format, and named versus unnamed statements determines the application’s performance and compatibility with connection pooling. These are configuration decisions, not implementation details.

Remember: A database driver is not a black box. It speaks a protocol with specific messages, and the choice of which messages to send determines the performance and the behavior of the application. PostgreSQL’s extended query protocol separates parsing from execution, which makes prepared statements efficient and allows parameters to be bound separately from the query text. MySQL’s binary protocol serves the same purpose for prepared statements but through a different message set. The connection pooler problem is a protocol problem: state that lives on a server connection does not automatically move when the connection is reassigned. Understanding the protocol is what allows a developer to choose the right driver configuration and to diagnose the failures that occur when the configuration does not match the deployment.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!