Relational Database Systems Concepts, Design, SQL, PostgreSQL, MySQL, and Applications

Part IV — PostgreSQL and MySQL in Practice

Chapter 17. PostgreSQL and MySQL: A Comparative Study

Chapters 14–16 treated the platforms one at a time; this chapter puts them side by side and keeps them there. Comparison is not trivia — it is the skill that lets you read either codebase, port between them knowingly, argue a platform choice with evidence, and recognize when a bug you are chasing is dialect rather than logic. Every difference cataloged here has already appeared in its home chapter; this chapter assembles the ledger, then closes Part IV with the migration method that uses it.

The book's two-platform strategy earns its payoff here: side-by-side presentation (the TOC's own phrase) turns feature lists into judgment.

After studying this chapter you will be able to:

  • Compare the platforms' architectures, processes, memory models, and configuration cultures.
  • Translate SQL spellings between dialects on sight.
  • Map data types and identifier generators across platforms.
  • Translate string, date, and JSON operations.
  • State constraint and referential-action differences, including history.
  • Compare transaction models, defaults, and DDL semantics.
  • Explain heap-plus-indexes versus clustered-PK storage and its optimization consequences.
  • Compare JSON and specialized-type support.
  • Map each platform's backup tooling to recovery requirements.
  • Run a disciplined migration assessment.

17.1 Architecture and configuration differences

AspectPostgreSQLMySQL 8.0
Process modelPostmaster + forked backend per connectionmysqld with a thread per connection
Connection costProcess spawn (pooling matters early)Thread spawn (cheaper; pooling still best practice)
Isolation between sessionsOS-level process isolationShared-address-space threads
NamespacesDatabases contain schemas (public, pg_catalog)Database = schema = directory
Cross-database queriesNo (connect per database)No (same rule, different reason)
ExtensibilityExtensions, custom types, access methodsStorage engines, plugins (server-side UDFs)
Config culturepostgresql.conf + ALTER SYSTEM; reload vs restart classesmy.cnf sections; SET PERSIST (8.0)
Memory centerpieceshared_buffers (+ work_mem per sort/hash)innodb_buffer_pool_size
Query cacheNone (plan + data caches only)Query cache removed in 8.0 (was a global bottleneck)

The honest summary: PostgreSQL's model buys robustness (a crashed backend is one dead session; the server survives) at connection cost, which modern deployment answers with poolers (Chapter 23) — while MySQL's threads trade isolation for cheap connections. Neither is a reason to choose; both are reasons to pool.

17.2 SQL compatibility and dialect differences

The Chapter 13 table, consolidated with the additions from Chapters 15–16:

NeedPostgreSQLMySQL 8.0
Concatenatea || bCONCAT(a, b)
Case-insensitive LIKEILIKELIKE (default collations) or LOWER() LIKE
LIMIT / offsetLIMIT n OFFSET mLIMIT m, n
INTERSECT / EXCEPTNative8.0.31+ (emulate with joins earlier)
FULL OUTER JOINNativeEmulate: LEFT JOIN UNION ALL RIGHT-join-orphans
FILTER (WHERE ...)NativeSUM(CASE WHEN ...)
DISTINCT ONNativeROW_NUMBER pattern
Grouped stringstring_agg(x, ', ' ORDER BY y)GROUP_CONCAT(x ORDER BY y SEPARATOR ', ') + max_len
Row generatorsgenerate_seriesRecursive CTE or a numbers table
DML feedbackRETURNINGROW_COUNT(), LAST_INSERT_ID()
Insert-or-updateON CONFLICT ... DO UPDATEON DUPLICATE KEY UPDATE
Identifier quotes"name"backquotes (ANSI_QUOTES mode honors double quotes)
Empty string vs NULLDistinct (standard)Distinct (standard) — but watch PHP-era legacy habits

Both are relationally complete, standard-first implementations; the divergence is surface vocabulary plus a few mechanism gaps — which is precisely why Chapter 13's dialect layer discipline (confine, catalog, test) works.

17.3 Data types and generated identifiers

ConceptPostgreSQLMySQL 8.0
Exact decimalNUMERIC(p,s)DECIMAL(p,s) (synonyms both)
Preferred stringtext (unlimited)VARCHAR(n) by rule; TEXT for large
BooleansbooleanNo BOOLEAN type — TINYINT(1) convention (TRUE/FALSE aliases)
Timestamps with zonestimestamptz (UTC-stored)TIMESTAMP (UTC-converted, 1970–2038)
Literal datetimetimestamp (no zone)DATETIME (1000–9999)
UUIDuuid typeBINARY(16) convention or UUID() strings
Arrays / ranges / enums / domainsNative typesENUM/SET only (no arrays, ranges, domains)
JSONjson / jsonb (binary, GIN)JSON (binary, validated)
Generated keyGENERATED ... AS IDENTITY / SERIALAUTO_INCREMENT
Last generated valueRETURNING colLAST_INSERT_ID()

Two mappings that decide migrations: TIMESTAMP means different things (zone-stored vs zone-converted — map PostgreSQL timestamptz to MySQL TIMESTAMP only within its range, else DATETIME + explicit zone discipline), and boolean becomes TINYINT(1) with a translation shim at the edge (a view or the driver's dialect setting).

17.4 String and date/time functions

TaskPostgreSQLMySQL
Length in charactersLENGTHCHAR_LENGTH
Length in bytesOCTET_LENGTHLENGTH
Find substringPOSITION(sub IN s)LOCATE(sub, s)
Case-insensitive matchILIKE / LOWER() LIKELIKE (collation) / LOWER() LIKE
Slice by delimitersplit_part(s, ',', 2)SUBSTRING_INDEX(s, ',', 2)
Date addd + INTERVAL '1 year'DATE_ADD(d, INTERVAL 1 YEAR)
Date diffAGE(a, b) (interval)DATEDIFF (days), TIMESTAMPDIFF(unit, a, b)
Truncate to unitdate_trunc('month', ts)no direct: DATE_FORMAT(ts, '%Y-%m-01') cast
Format for humansto_char(d, 'YYYY-MM')DATE_FORMAT(d, '%Y-%m')
Parse from stringto_date(s, 'YYYY-MM-DD')STR_TO_DATE(s, '%Y-%m-%d')
NowCURRENT_TIMESTAMP / now()NOW() / CURRENT_TIMESTAMP

The pattern is consistent: same capabilities, different spellings — except date truncation, where MySQL has no date_trunc and the format-and-cast idiom is the portable bridge. Any migration runs this table as a find-and-replace pass over the query layer; Appendix I prints it expanded.

17.5 Constraints and referential actions

AspectPostgreSQLMySQL 8.0
CHECK enforcementAlways8.0.16+ (parsed-and-ignored before)
Deferred checkingDEFERRABLE INITIALLY DEFERREDNone — immediate only
FK requires engineN/AInnoDB on both tables
NO ACTION vs RESTRICTNO ACTION defers to end of statementRESTRICT immediate; NO ACTION equivalent today
Adding FK onlineNOT VALID + VALIDATE CONSTRAINTOnline DDL (ALGORITHM=INPLACE, LOCK=NONE) for many cases
Domain-as-typeDomains, enums, rangesENUM/SET column types
NULL in UNIQUENULLs distinct (multiple NULLs allowed)NULLs distinct by default (functional indexes can tighten)
SHOW DDL\d tableSHOW CREATE TABLE table

The behavioral gap to memorize is deferral: PostgreSQL's deferred constraints enable in-transaction choreography MySQL simply cannot express — MySQL scripts do it with statement ordering or staging tables. The historical CHECK gap is nearly closed by 8.0.16 but still bites migrations into older fleets.

17.6 Transactions and isolation levels

AspectPostgreSQLMySQL (InnoDB)
Default isolationREAD COMMITTEDREPEATABLE READ
MVCC mechanismTuple versions + VACUUM cleanupUndo logs + purge threads
DDL in transactionsFully transactional (rollback-able ALTER)Implicit COMMIT on DDL
SavepointsYesYes
Deadlock handlingDetects, aborts one (retry logic in app)Detects immediately, rolls back victim
Write conflicts at READ COMMITTEDLocking/serialization errors surface as 40001-classGap/next-key locks prevent phantoms at RR

Two headline differences. Default isolation: PostgreSQL's READ COMMITTED means two reads in one transaction can see committed changes from others; MySQL's REPEATABLE READ means the transaction sees the snapshot it started with (Chapter 18 demonstrates both behaviors in experiments). Applications written on one default and moved to the other change observable behavior without any code change — the quietest migration hazard in this chapter. DDL transactionality: PostgreSQL can roll back a failed migration script mid-flight; MySQL must compensate (restore, or design migrations as idempotent steps) — the reason Chapter 24's migration tooling treats platforms differently.

17.7 Indexing and query optimization

AspectPostgreSQLMySQL (InnoDB)
Table storageHeap; indexes reference row locationsClustered: table is the PK B-tree
Secondary indexPoints to the heap rowStores the PK (two lookups unless covering)
Covering trickINCLUDE columnsCovering secondary (Extra: Using index)
Index familiesbtree, hash, GIN, GiST, SP-GiST, BRINB-tree, (adaptive hash), FULLTEXT, spatial
Partial indexesNativeNone (partition pruning approximates)
Expression indexesNativeFunctional key parts (8.0.13)
Plan displayEXPLAIN / EXPLAIN ANALYZE (JSON)EXPLAIN / EXPLAIN ANALYZE (8.0.18; FORMAT=JSON/TREE)
StatisticsANALYZE (auto via autovacuum)ANALYZE TABLE; histograms (8.0)
Random PK penaltyMild (heap unaffected)Severe (clustered splits); use ordered keys

The storage-model difference drives everything else: in PostgreSQL a bad index costs a scan; in MySQL a primary key choice is a physical decision (Chapter 16), and the covering-index payoff is larger because it skips the PK detour. Both optimizers are cost-based and both respond to fresh statistics — the shared maintenance verb is literally ANALYZE on both platforms, a rare naming agreement.

17.8 JSON support and specialized data types

AspectPostgreSQLMySQL 8.0
Typesjson (text) + jsonb (binary)JSON (binary, validated)
Extract-> jsonb / ->> text-> / ->> (and JSON_EXTRACT/JSON_UNQUOTE)
Containment@>, ?, ?&, ?|JSON_CONTAINS, JSON_OVERLAPS
Update in placejsonb_set, || mergeJSON_SET, JSON_MERGE_PATCH
Rows from JSONjsonb_array_elements, jsonb_populate_recordJSON_TABLE (8.0)
JSON from rowsjsonb_agg, jsonb_build_objectJSON_ARRAYAGG, JSON_OBJECTAGG
IndexingGIN on jsonb (containment-class queries)Multi-valued indexes (8.0.17) on JSON arrays
Full-text searchtsvector/tsquery + GINFULLTEXT index + MATCH ... AGAINST
SpatialPostGIS (the industry standard extension)InnoDB spatial R-tree indexes
Arrays / rangesNativeNone (JSON arrays approximate)

Both engines crossed the JSON Rubicon years ago; the philosophical difference remains: PostgreSQL leans on containment + inverted index (queries that ask "does this document have..."), MySQL on path extraction + function calls. Neither model is richer — they reward different query shapes, which is why Chapter 27's document-store comparison treats them as one family.

17.9 Backup and restoration methods

AspectPostgreSQLMySQL 8.0
Logical backuppg_dump (custom/plain/dir) → pg_restore/psqlmysqldump, mysqlpump → mysql client
Physical backuppg_basebackup (WAL archiving)Clone plugin, XtraBackup-family, or stopped-copy
IncrementalWAL archiving (replay to any point)Binary logs (point-in-time recovery)
Parallel dumpspg_dump -j (directory format)mysqlpump parallel; mysqldump single
Consistent snapshotRepeatable-read snapshot during dump--single-transaction (InnoDB)
Cross-platformPlain SQL is portable SQLSame, with dialect caveats

The conceptual mapping is exact: pg_dump ↔ mysqldump, WAL ↔ binary log, pg_basebackup ↔ physical copies — each platform couples logical dumps with a physical + log-replay story for point-in-time recovery. The full method is Chapter 21's subject; the comparative point is that neither platform lacks any recovery capability the other has. Differences are operational maturity and tooling ergonomics, not model.

17.10 Portability considerations when migrating databases

The ledger becomes a method. Migrating the university database PostgreSQL → MySQL (or back), in the order that works:

  1. Assess the schema. Run the type map (17.3): NUMERIC→DECIMAL, text-length rules, boolean shim, timestamptz→TIMESTAMP/DATETIME decisions, identity→AUTO_INCREMENT, and the constraint audit (17.5) — CHECKs need 8.0.16+, deferrables need redesign.
  2. Assess the query layer. Extract every query (they live in the Chapter 12/24 query library); run the dialect tables (17.2, 17.4) as transforms; flag the structural gaps — FULL JOIN, FILTER, DISTINCT ON, deferrable choreography — and design replacements once, in one module.
  3. Assess the behavior defaults. Isolation default (17.6) — decide explicitly whether the application wants READ COMMITTED or REPEATABLE READ and set it on both platforms; NULL ordering and LIKE case sensitivity (Chapter 10) — pin them in the query layer, not the platform.
  4. Move the data. CSV is the neutral format: \copy out (or COPY TO), LOAD DATA in — constraints enforced, counts verified (5, 6, 12, 10, 13, 28), EXCEPT-equality checked table by table.
  5. Prove parity. Run the Chapter 11/13 dashboards on both; diff outputs. Differences in rounding or ordering are expected (AVG precision, NULL placement) and must be normalized deliberately — differences in rows are bugs.
  6. Cut over with a rollback plan. Dual-run against both databases, compare at the application layer, and keep the old platform warm until the parity window closes — the same discipline Chapter 26 brings to replication.

The closing judgment, stated once for Part IV: PostgreSQL and MySQL are 95% the same model with different accents. The 5% — storage layout, transactional DDL, isolation defaults, deferrable constraints, the richer type system — is where all the thinking happens. Teams that treat the platforms as interchangeable are surprised by exactly this chapter's tables; teams that carry the ledger are never surprised at all.


Chapter Summary

  • Architecturally: process-per-connection (isolation, pooling early) versus thread-per-connection (cheap, still pooled); databases-with-schemas versus database-as-schema; extensions versus engines.
  • Dialect: the consolidated spelling table (concatenation, LIMIT, ILIKE, INTERSECT/EXCEPT, FULL JOIN, FILTER, DISTINCT ON, string_agg/GROUP_CONCAT, RETURNING, upsert, quoting) — surface vocabulary, same model.
  • Types: NUMERIC/DECIMAL, text conventions, boolean shim, timestamptz/TIMESTAMP-DATETIME mapping, identity/AUTO_INCREMENT, PostgreSQL's natives unmatched by MySQL.
  • Functions: same capabilities, different names — except date_trunc, the one real capability gap.
  • Constraints: CHECK history, deferrable-versus-immediate, InnoDB-only FKs, NOT VALID versus online DDL.
  • Transactions: READ COMMITTED versus REPEATABLE READ defaults change observable behavior silently; PostgreSQL's transactional DDL versus MySQL's implicit commit reshapes migration tooling.
  • Optimization: heap-plus-indexes versus clustered PK — covering indexes pay differently; partial indexes unmatched; ANALYZE is the shared maintenance verb.
  • JSON: containment+GIN versus extraction+functions; full-text and spatial both present, different lineages.
  • Backups map exactly: pg_dump/mysqldump, WAL/binlog, basebackup/physical — capability parity, ergonomic differences.
  • Migration method: schema → queries → behavior defaults → CSV move → parity proof → dual-run cutover; the 95/5 rule explains every surprise.

Key Terms

TermDefinition
Process-per-connection vs thread-per-connectionThe platforms' session models
Database vs schema (naming)PostgreSQL two-level namespaces; MySQL synonym
Dialect layerThe confined module holding platform-specific SQL
Type mappingThe migration translation table for types
Boolean shimTINYINT(1) convention replacing native boolean
Deferrable (PG-only)In-transaction constraint choreography
Implicit DDL commit (MySQL)DDL ends transactions; migrations must compensate
Default isolation differenceREAD COMMITTED (PG) vs REPEATABLE READ (MySQL)
Heap vs clustered storagePostgreSQL rows in heap files vs rows in the PK B-tree
Covering index payoffLarger in MySQL (skips PK lookup); INCLUDE in PG
Containment vs extraction JSON modelsGIN/@> versus paths/functions
WAL / binary logThe platforms' point-in-time recovery logs
Parity proofDashboard diffing across platforms during migration
95/5 ruleSame model, different accents — the 5% is the thinking

Laboratory Exercises

  1. Build the dialect transformer: take your Chapter 12 queries.sql library and mechanically port every query to the other platform using Sections 17.2–17.4 — logging each transform in a two-column ledger (original → ported). Expected result: ten queries, most renamed functions only; two to four structural rewrites (FULL JOIN, FILTER, DISTINCT ON) — the ledger documents exactly where.
  2. Isolation experiment: on both platforms, run Chapter 18's preview (two sessions, read-commit/see-changes versus repeatable-snapshot) with each platform's default, and record which behavior each showed without any settings. Expected result: PostgreSQL session sees committed changes within its transaction; MySQL default session does not — the silent behavioral difference demonstrated.
  3. Storage-model proof: in MySQL, EXPLAIN a query answered by a secondary index and note the PK detour (rows via two ref/range steps or the key_len evidence); in PostgreSQL, show the same query's index scan pointing at the heap. Expected result: both plans correct; MySQL's shows the clustered-PK consequence, PostgreSQL's a plain index scan.
  4. JSON parity: store the same course-catalog document on both platforms; answer "which departments offer CSE221" with @> on one and JSON_CONTAINS on the other; compare plans (GIN vs multi-valued/functional index). Expected result: same one row (CSE department); different mechanisms in the plans.
  5. Type-migration drill: produce the MySQL DDL for the canonical schema from Appendix H with every type/constraint decision annotated (17.3, 17.5), then load it and verify the six counts. Expected result: a running MySQL university database — DECIMAL for NUMERIC, same CHECKs (8.0.16+), immediate FKs — counts 5, 6, 12, 10, 13, 28.
  6. Full migration rehearsal: execute Section 17.10's six steps for university → MySQL (or the reverse), ending with the Chapter 11 dashboard diff and a written parity report of every rounding/ordering difference found. Expected result: identical rows everywhere; documented differences limited to AVG precision (3.4754545454545455 vs 3.4755) and NULL ordering — all normalized.

Review Questions and Exercises

  1. Why does PostgreSQL's process model push deployments toward poolers earlier than MySQL's? Each connection is an OS process — memory and spawn cost; threads are cheaper, though pooling is still the right practice on both.
  2. Translate: SELECT a \|\| b, LIMIT 10 OFFSET 5, and string_agg(t, ', ') into MySQL. *CONCAT(a, b); LIMIT 5, 10; GROUP_CONCAT(t SEPARATOR ', ').*
  3. Which type mapping needs a policy decision, not a mechanical translation, and why? Timestamps with zones — PostgreSQL timestamptz stores UTC; MySQL TIMESTAMP converts but is range-limited, DATETIME stores literals — the semantic intent (event vs wall-clock) picks the target.
  4. State the deferrable-constraint gap and the MySQL-side workaround. PostgreSQL can defer checks to COMMIT for choreography; MySQL checks immediately — restructure as statement order or staging tables.
  5. An application moves PostgreSQL → MySQL with no code change. Name two behaviors that silently differ. Default isolation (READ COMMITTED → REPEATABLE READ semantics), NULL sort placement, LIKE case sensitivity, AVG decimal precision — any two.
  6. Why is a random UUID primary key a graver decision in MySQL than PostgreSQL? Clustered PK: random keys scatter inserts through the B-tree; PostgreSQL's heap tolerates them (indexes point to rows, order-free).
  7. What does Extra: Using index mean in MySQL, and why is its payoff structurally bigger than a covering index in PostgreSQL? The secondary index alone answered the query; in InnoDB it also skips the PK B-tree detour that every non-covering secondary hit pays.
  8. Compare the two JSON models in one sentence each. PostgreSQL: containment queries (@>) accelerated by GIN inverted indexes; MySQL: path extraction (->) with function predicates and multi-valued indexes.
  9. Map the backup triple across platforms. pg_dump ↔ mysqldump (logical); WAL ↔ binary log (replay); pg_basebackup ↔ clone/XtraBackup-style physical.
  10. In the migration method, why prove parity with dashboards rather than row counts alone? Counts catch loss; dashboards catch semantics — rounding, ordering, NULL rendering — the differences that change what users see.
  11. Which platform feature set in this chapter has no counterpart at all on the other side? PostgreSQL: deferrable constraints, partial indexes, native arrays/ranges/domains; MySQL: functional gaps of date_trunc — each list is short and stable.
  12. State the 95/5 rule and its practical consequence for a team choosing between the platforms. 95% shared model, 5% real differences (storage, DDL transactions, isolation defaults, types); choose on the 5% and on operational fit — and carry the ledger either way.

Mini-Project

Write the platform decision dossier for a fictional product — a multi-tenant course-platform serving 200 universities, JSON API payloads per enrollment, transcript reporting, and a 5-year horizon — arguing PostgreSQL or MySQL with evidence drawn only from this chapter's tables and Part IV's chapters: storage model and PK strategy, the JSON query shapes you expect, transaction/DDL needs of your migration pipeline, the constraint choreography your registrar flows require, operational tooling, and hiring pool. Required sections: the 5% that decides (explicit, cited to sections), the 95% you will hold portable (the dialect-layer plan), the migration escape hatch (Section 17.10's method, applied to your schema sketch), and the honest counter-argument — the case for the other platform, steelmanned in one paragraph. Then swap dossiers with a classmate and write the rebuttal. The dossier is the final artifact of Part IV because it is the skill itself: platform judgment on evidence.