Relational Databases: PostgreSQL & MySQL in Production
PostgreSQL is not just a SQL database — it is an extensible platform that handles semi-structured data, geospatial queries, and AI embeddings in a single query. This article covers ACID mechanics, MVCC, the extension ecosystem, data modeling discipline, Row-Level Security for multi-tenancy, and the scaling ceiling every production team will eventually hit.
Modern Database Paradigms
Relational Databases: PostgreSQL & MySQL in Production
Selecting the right database is a foundational architectural decision; data gravity ultimately dictates the scalability and resilience of an application — choose the paradigm first, the product second. Teams that decide "PostgreSQL for everything" are often right — but not for the reasons they think. Postgres is not just a SQL database. It is an extensible platform that silently absorbs use cases that would otherwise demand three separate stores: JSONB absorbs a document database, PostGIS absorbs a geospatial tier, and pgvector absorbs a vector search layer. This article shows you how to use that extension model deliberately, how to model data correctly in a relational schema, and the exact signals that tell you Postgres can no longer grow with you.
1. ACID Mechanics — What the Guarantee Actually Covers
The broken pattern: teams interpret "ACID compliant" as "safe" and stop thinking. ACID is a contract with specific boundaries — violating any of the four properties silently undermines data integrity.
| Property | What It Guarantees | What It Does NOT Guarantee |
|---|---|---|
| Atomicity | All statements in a transaction commit or none do | Application-layer logic correctness |
| Consistency | Constraints (FK, UNIQUE, CHECK) always hold after commit | Business rule correctness beyond constraints |
| Isolation | Concurrent transactions don't see each other's in-progress writes | Performance — higher isolation = lower throughput |
| Durability | Committed writes survive crash (via WAL flush to disk) | Replica lag — replicas may lag behind primary |
PostgreSQL's default isolation level is READ COMMITTED, not SERIALIZABLE. Under READ COMMITTED, a transaction can see rows committed by other transactions between its own statements — this causes non-repeatable reads. For financial calculations or inventory checks, explicitly set SET TRANSACTION ISOLATION LEVEL REPEATABLE READ.
1.1 MVCC — Concurrent Reads Without Locking
MVCC (Multi-Version Concurrency Control) is why Postgres achieves high read concurrency without read locks. Each transaction works against a consistent snapshot of the database as of its start time. Writers create new row versions (tuples) rather than modifying in place. The tradeoff: old tuple versions accumulate as "dead tuples" until VACUUM reclaims them.
Long-running transactions block VACUUM from reclaiming dead tuples. A single 4-hour analytics query can cause table bloat that degrades write performance for the entire cluster. Monitor pg_stat_activity for long-running sessions and set statement_timeout for analytics workloads.
2. The PostgreSQL Extension Ecosystem
2.1 JSONB — Semi-Structured Data Without a Document Database
Use structured columns for fields you query with WHERE, ORDER BY, or GROUP BY. Use JSONB for variable, product-specific attributes that differ per record. Never store queryable data inside a JSONB blob — the GIN index helps but it cannot replace a B-tree index on a dedicated column.
2.2 PostGIS — Geospatial Without a Separate Tier
2.3 pgvector — AI Embeddings in Your Existing Postgres Stack
pgvector's HNSW index loads the entire graph into RAM. At 10M 1536-dimension vectors, that is approximately 60GB of RAM. If your vector dataset exceeds available memory, the HNSW index degrades to disk I/O — this is the signal to migrate to a dedicated vector store (Qdrant, Pinecone).
3. Data Modeling
3.1 Normalization vs. Strategic Denormalization
The rule: normalize for update correctness; denormalize for read performance. Snapshot data that represents a moment in time (order price, invoice total) — never join to get it at render time.
3.2 Hybrid Schema — Structured Columns + JSONB
The correct pattern for product catalogs, CMS content, and multi-tenant SaaS applications where each entity type has a shared core and a variable extension:

3.3 Schema Migration Discipline
In PostgreSQL, ALTER TABLE ... DROP COLUMN takes an ACCESS EXCLUSIVE lock. On a table with millions of rows and active read traffic, this blocks all reads and writes until it completes. Always use the three-phase deprecation cycle in production.
4. Indexing Strategy
| Index Type | Storage Engine | When to Use |
|---|---|---|
| B-tree (default) | Sorted tree | Equality (=), range (<, >), ORDER BY, most WHERE clauses |
| GIN | Inverted index | JSONB containment (@>), array overlap (&&), full-text search (tsvector) |
| GiST | Generalized search tree | Geospatial (ST_DWithin), range types (tstzrange), nearest-neighbor |
| BRIN | Block range min/max | Large tables with naturally ordered data (timestamps, sequential IDs) — tiny index, fast range scan |
| Hash | Hash table | Equality only — rarely preferred over B-tree |
5. MySQL vs. PostgreSQL

| Criterion | PostgreSQL | MySQL (InnoDB) |
|---|---|---|
| ACID compliance | Full MVCC, serializable isolation | Full MVCC, serializable available |
| JSON support | JSONB — binary, indexed, queryable | JSON — text-stored, limited indexing |
| Extension ecosystem | pgvector, PostGIS, pg_search, TimescaleDB | Minimal — extensions rare |
| Replication | Streaming replication (WAL-based), logical replication | Binary log replication; Group Replication for multi-primary |
| Full-text search | tsvector + GIN — capable for moderate scale |
FULLTEXT index — less flexible than Postgres |
| Connection model | Process-per-connection (PgBouncer required at scale) | Thread-per-connection (better raw concurrency) |
| License | PostgreSQL License (permissive) | GPL v2 (requires open-source compliance or commercial license) |
| Best fit | Complex queries, extensions, multi-tenant SaaS, AI workloads | High-read web apps, SaaS, CMS, WordPress, Drupal |
Choose MySQL when your team already operates it well and your workload is read-heavy with simple queries. Choose PostgreSQL when you need JSONB, PostGIS, pgvector, complex window functions, or multi-tenant Row-Level Security. Do not choose based on benchmarks — choose based on the extension your workload needs.
6. Security
6.1 Row-Level Security for Multi-Tenancy
RLS policies are enforced even for superusers unless FORCE ROW LEVEL SECURITY is set. Always set BYPASSRLS = false on application service roles. Application-layer WHERE tenant_id = ? filtering is a defense-in-depth layer, not a replacement for RLS.
6.2 Audit Trails with pg_audit
7. When NOT to Use Relational
| Scenario | Why Relational Fails | Better Alternative |
|---|---|---|
| Rapidly evolving schema (early product iteration) | ALTER TABLE migrations block tables; schema rigidity slows experimentation |
MongoDB — schemaless documents, no migration required |
| Write-heavy append-only at extreme volume (> 500K writes/sec) | WAL becomes the bottleneck; single-primary model cannot absorb the throughput | Cassandra — LSM-tree append, multi-primary writes |
| Graph traversal with 3+ relationship hops | Recursive CTEs scale as O(n³) — impractical beyond millions of rows | Neo4j — native graph storage, Cypher in milliseconds |
| Sub-millisecond key lookups (session, rate limit) | Network + query parse overhead adds ~1–5ms minimum | Redis — in-memory O(1) hash lookups |
| Horizontal scale across regions (petabyte-scale, auto-sharding) | Single primary model; manual sharding is complex and failure-prone | CockroachDB — automatic Raft-based horizontal sharding |
8. The Scaling Ceiling
The correct escalation path when Postgres performance degrades:
Most teams reach Step 1 and stop. PgBouncer in transaction mode (not session mode) is the single highest-leverage optimization available for Postgres at scale — it multiplies the effective connection capacity by 10–50× without changing any application code.
Summary
| Concept | Rule |
|---|---|
| PostgreSQL extension model | PostgreSQL's extension model (JSONB, PostGIS, pgvector) absorbs use cases that would otherwise require 3 separate databases. |
| MVCC and concurrency | MVCC enables high read concurrency without locking — understanding it prevents incorrect 'Postgres doesn't scale' conclusions. |
| Multi-tenant security | Row-Level Security is the correct mechanism for multi-tenant isolation; application-layer filtering is a security anti-pattern. |
| Scaling ceiling | PostgreSQL's scaling ceiling is connection-count, not query performance — deploy PgBouncer before reaching for sharding. |
| When to switch | Relational databases are wrong for rapidly evolving schemas, 3+ hop graph queries, and write-throughput workloads exceeding what a single primary can absorb. |
Related: Database Foundations Series — The MVCC, constraint enforcement, and fsync behavior covered here are the physical implementation of the ACID contract. For the formal breakdown of all four ACID properties (Atomicity, Consistency, Isolation, Durability) and their failure modes, see ACID Database Properties: Atomicity, Consistency, Isolation & Durability in Practice.
What's Next
In Part 3, we cover document and NoSQL databases — MongoDB's embedding vs. referencing decision, DynamoDB's single-table design, schema versioning patterns, and the GDPR compliance challenge that document denormalization creates.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.