PostgreSQL vs MySQL | Deep Comparison | When to Use Each
이 글의 핵심
PostgreSQL and MySQL are both excellent relational databases, but they excel in different scenarios. This guide covers concrete differences in types, JSON, full-text search, locking, and replication ??so you can make the right choice for your project.
Quick Decision Matrix
Use PostgreSQL when:
??Complex queries with JOINs and aggregations
??JSON/JSONB document storage alongside relational data
??Full-text search without external engine
??Custom data types, enums, arrays
??Geospatial data (PostGIS extension)
??Strict SQL standard compliance matters
??Strong ACID requirements with complex transactions
Use MySQL when:
??Simple read-heavy workloads
??Existing MySQL infrastructure
??PlanetScale (MySQL-compatible serverless DB)
??WordPress, Drupal, or other MySQL-native CMS
??Team expertise is MySQL-based
??Simpler replication/cluster setup is a priority
Executive summary
If you are choosing a relational engine for a new system, the honest answer is that both PostgreSQL and MySQL are credible choices at scale, but they optimize for different pain points. PostgreSQL tends to win when the database must behave like a programmable analytical engine inside your stack: rich types, standards?�oriented SQL, strong indexing for semi?�structured data, and extensibility. MySQL (with InnoDB) tends to win when the workload is dominated by simple, predictable primary?�key and indexed lookups on a stack where operational familiarity and host ecosystem breadth matter as much as raw SQL feature depth. The sections below are written to be comparative, not competitive: pick the tool whose failure modes and operational model match your team and product.
| Dimension | PostgreSQL (typical strengths) | MySQL 8 + InnoDB (typical strengths) |
|---|---|---|
| SQL & analytics | Window functions, CTEs, FILTER, LATERAL, rich planner | Solid core SQL; fewer niceties; optimizer improves steadily |
| Semi?�structured data | JSONB, GIN/GiST, powerful containment | JSON type + generated columns; path indexes; less flexible |
| Full?�text | Tsearch2 / FTS with dictionaries, weights, rank | FULLTEXT indexes; good for ?�good enough??search |
| Extensibility | Extensions (PostGIS, pg_trgm, etc.) | Server plugins exist; different culture (fewer in?�DB extensions) |
| Ops culture | Patroni, logical replication, strong HA patterns | Mature async replication, Group Replication, wide hosting |
Performance: OLTP vs OLAP and what benchmarks actually show
Workload shape matters more than the logo on the box. In practice, OLTP systems are dominated by short transactions, B?�tree point lookups, and a stable working set in buffer cache. Both engines perform well when indexes match access paths and the application avoids ?�chatty??SQL. OLAP and reporting?�large scans, complex joins, windowed aggregates?�tend to favor PostgreSQL?�s query planner and feature set, though either database can be tuned for read replicas and separate reporting schemas.
Public benchmarks (for example, TPC?�C-style and sysbench tests you will find in vendor blogs) are useful only as a sanity check because they are sensitive to hardware, buffer pool size, fsync policy, and tuning. A representative pattern in published comparisons is that MySQL can lead on narrow, read?�heavy, single?�row microbenchmarks when the dataset is small and hot in memory, while PostgreSQL more often leads on query shapes that require sophisticated joins, sorting, and aggregation?�but your indexes and statistics will dominate either result.
| Scenario | What to measure | Fair interpretation |
|---|---|---|
| Key?�value?�like reads | Point selects by PK, p95/p99 latency | Often similar; verify connection pooling and client batching |
| Write throughput | Sustained inserts/updates, checkpoint stalls | Both need tuning (checkpoint_timeout, innodb_flush_*, autovacuum) |
| Analytical SQL | Multi?�way joins, large sorts, CTEs | PostgreSQL frequently easier to express and tune; consider DuckDB/ClickHouse for heavy OLAP |
| Mixed JSON + SQL | Filter + sort on JSON paths | PostgreSQL JSONB + GIN is usually less painful at scale |
Rule of thumb for fairness: if performance is a gate, build a benchmark clone of your top five queries and your top two write patterns on identical hardware, then compare p95 and disk bytes written, not a single ?�QPS??headline number.
Native arrays, constraints, and modeling flexibility
PostgreSQL ships first?�class array types and rich constraints (including EXCLUDE for non?�overlapping ranges) that are awkward or impossible in stock MySQL. MySQL 8.0+ closes many gaps (check constraints, functional indexes, better JSON functions), but array semantics and exclusion constraints still push people toward PostgreSQL for complex domain modeling (scheduling, tagging, and multi?�valued attributes without join tables). Neither choice is ?�wrong??if you can model the domain cleanly; the cost is where it shows up: join complexity in MySQL vs. more PostgreSQL?�specific knowledge on the team.
| Feature | PostgreSQL | MySQL 8 (InnoDB) |
|---|---|---|
| Arrays | Native text[], int[], operators, GIN | Workarounds: JSON, separate tables, or strings |
| Range / exclusion | tstzrange, EXCLUDE USING gist | Manual checks or triggers |
| Generated columns | GENERATED columns (stored) | GENERATED columns + functional indexes (good parity) |
| Check constraints | Long?�standing, widely used | Supported (historically uneven adoption in apps) |
Data Types Comparison
-- Auto-increment primary keys
-- MySQL
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
...
);
-- PostgreSQL (modern way)
CREATE TABLE users (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
...
);
-- PostgreSQL (traditional, still works)
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- shorthand for INTEGER + sequence
...
);
-- String types
-- MySQL
VARCHAR(255) -- variable length, up to 255 chars
TEXT -- up to 65KB
MEDIUMTEXT -- up to 16MB
LONGTEXT -- up to 4GB
-- PostgreSQL
VARCHAR(255) -- variable length (rarely needed ??use TEXT)
TEXT -- unlimited length (no performance difference from VARCHAR)
-- PostgreSQL has NO size limit on TEXT ??use it everywhere
-- PostgreSQL extra types MySQL lacks:
CITEXT -- case-insensitive text (extension)
UUID -- native UUID type (MySQL stores as VARCHAR)
INET -- IP address
MACADDR -- MAC address
MONEY -- monetary amounts
BYTEA -- binary data (MySQL: BLOB)
-- Temporal types
-- MySQL
DATETIME -- YYYY-MM-DD HH:MM:SS (no timezone)
TIMESTAMP -- stored as UTC, displayed in server timezone
DATE, TIME
-- PostgreSQL
TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ) -- stores UTC, shows with offset
TIMESTAMP WITHOUT TIME ZONE -- no timezone info
DATE, TIME, INTERVAL -- interval: '2 hours 30 minutes'
-- PostgreSQL is stricter: use TIMESTAMPTZ for any production timestamp
-- MySQL TIMESTAMP has 2038 problem (32-bit Unix timestamp)
JSON Support
PostgreSQL’s JSONB is a significant advantage for mixed relational/document storage.
-- PostgreSQL JSONB
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
attributes JSONB -- Binary JSON ??indexed, queryable
);
-- Insert
INSERT INTO products (name, attributes) VALUES
('Laptop', '{"brand": "Dell", "specs": {"ram": 16, "storage": 512}, "tags": ["gaming", "work"]}');
-- Query JSONB fields
SELECT name, attributes->>'brand' AS brand
FROM products
WHERE attributes->>'brand' = 'Dell';
-- Deep path access
SELECT name, attributes->'specs'->>'ram' AS ram_gb
FROM products
WHERE (attributes->'specs'->>'ram')::int > 8;
-- Array containment
SELECT * FROM products
WHERE attributes->'tags' ? 'gaming'; -- contains key/element
SELECT * FROM products
WHERE attributes @> '{"specs": {"ram": 16}}'; -- contains subset
-- Index on JSONB ??critical for performance
CREATE INDEX idx_products_brand ON products USING GIN (attributes);
-- Or for specific field:
CREATE INDEX idx_products_brand_btree ON products ((attributes->>'brand'));
-- Update specific field (without replacing entire JSON)
UPDATE products
SET attributes = jsonb_set(attributes, '{specs, ram}', '32')
WHERE id = 1;
-- MySQL JSON (added in 5.7, improved in 8.0)
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
attributes JSON
);
-- Query
SELECT name, JSON_EXTRACT(attributes, '$.brand') AS brand
FROM products
WHERE JSON_EXTRACT(attributes, '$.brand') = '"Dell"';
-- Shorthand
SELECT name, attributes->>'$.brand' AS brand
FROM products;
-- MySQL lacks JSONB ??JSON stored as text, parsed on every access
-- No GIN index support ??only functional indexes on specific paths
CREATE INDEX idx_brand ON products ((attributes->>'$.brand'));
Winner: PostgreSQL ??JSONB is stored binary (faster reads), supports GIN indexes (fast containment queries), and has richer operators.
Full-Text Search
-- PostgreSQL built-in FTS
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT,
body TEXT,
search_vector TSVECTOR GENERATED ALWAYS AS (
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(body, '')), 'B')
) STORED
);
CREATE INDEX idx_articles_search ON articles USING GIN (search_vector);
-- Search
SELECT id, title,
ts_rank(search_vector, query) AS rank
FROM articles,
to_tsquery('english', 'javascript & (tutorial | guide)') AS query
WHERE search_vector @@ query
ORDER BY rank DESC;
-- Highlight matching terms
SELECT title,
ts_headline('english', body, to_tsquery('javascript'), 'MaxWords=20') AS excerpt
FROM articles
WHERE search_vector @@ to_tsquery('english', 'javascript');
-- MySQL FULLTEXT (simpler, less powerful)
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255),
body TEXT,
FULLTEXT INDEX idx_search (title, body)
);
-- Boolean mode search
SELECT id, title,
MATCH(title, body) AGAINST('javascript tutorial' IN BOOLEAN MODE) AS score
FROM articles
WHERE MATCH(title, body) AGAINST('+javascript +tutorial' IN BOOLEAN MODE)
ORDER BY score DESC;
-- MySQL FULLTEXT limitations:
-- Minimum word length (default 4 chars)
-- No phrase proximity search
-- English-only stemming
-- No custom ranking control
Winner: PostgreSQL ??More powerful with configurable dictionaries, proximity search, rich ranking, and the ability to index JSONB alongside text.
Transactions and Locking
-- PostgreSQL: MVCC (Multi-Version Concurrency Control)
-- Readers never block writers, writers never block readers
-- Isolation levels
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED; -- default
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- strictest
-- PostgreSQL serializable actually detects and prevents anomalies
-- (MySQL serializable uses locks, not true serializability)
-- Explicit row locking
SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- lock for update
SELECT * FROM orders WHERE id = 1 FOR UPDATE SKIP LOCKED; -- skip locked rows (queue pattern)
SELECT * FROM orders WHERE id = 1 FOR SHARE; -- shared lock
-- MySQL InnoDB also uses MVCC
-- But has some differences:
-- Phantom reads in REPEATABLE READ:
-- MySQL prevents them with gap locks (can cause deadlocks)
-- PostgreSQL prevents them with MVCC snapshot (no gap locks)
-- Table locking:
-- MySQL MyISAM: table-level locks (avoid for writes)
-- MySQL InnoDB: row-level locks (same as PostgreSQL)
ACID and ?�strictness??in the real world
Atomicity, consistency, isolation, durability are not marketing labels?�they are the contract your application leans on when you say ?�one logical operation.??InnoDB and PostgreSQL both offer durable commits with MVCC-style read isolation, but the details of isolation behavior and locking differ enough that you should not assume one engine?�s REPEATABLE READ means the same as the other?�s in edge cases (phantom reads, write skew, and deadlock patterns).
| Topic | PostgreSQL (practical note) | MySQL 8 + InnoDB (practical note) |
|---|---|---|
| Default isolation | READ COMMITTED (configurable) | REPEATABLE READ in InnoDB (snapshot semantics with gap locks) |
| ?�Strong??serializability | SERIALIZABLE + SSI?�style protection (fails fast on dangerous patterns) | SERIALIZABLE exists; verify behavior for your specific edge cases in tests |
| Partial writes | Statement-level atomicity; multi?�stmt transactions as expected with proper client usage | InnoDB groups row changes; avoid mixing transactional engines in one statement |
| *Replication vs ?�truth?? | Async replicas can be stale; use sync rep or read?�your?�writes strategies | Classic async topologies: measure replica lag; avoid assuming instant reads on replicas |
| Foreign keys | Enforced with clear errors | FOREIGN_KEY_CHECKS and engine choice must be right (InnoDB) |
A fair take: if your app truly requires serious isolation proofs, the database choice is necessary but not sufficient?�you still need clear transaction boundaries, idempotency, and test coverage for race conditions. PostgreSQL?�s richer isolation documentation and SKIP LOCKED queue patterns are frequently cited in systems that process ordered work; MySQL is widely proven for very high simple read/write QPS with careful tuning and pooling.
Window Functions and CTEs
Both support these, but PostgreSQL has broader support.
-- Window functions (both support)
SELECT
name,
department,
salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS rank,
LAG(salary, 1) OVER (ORDER BY hire_date) AS prev_salary
FROM employees;
-- CTEs (both support)
WITH ranked_sales AS (
SELECT
salesperson_id,
SUM(amount) AS total,
RANK() OVER (ORDER BY SUM(amount) DESC) AS rank
FROM sales
GROUP BY salesperson_id
)
SELECT * FROM ranked_sales WHERE rank <= 10;
-- Recursive CTEs (both support)
WITH RECURSIVE org_chart AS (
SELECT id, name, manager_id, 0 AS depth
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, depth + 1
FROM employees e
JOIN org_chart o ON e.manager_id = o.id
)
SELECT * FROM org_chart ORDER BY depth;
-- PostgreSQL extras:
-- FILTER clause in aggregations
SELECT
COUNT(*) FILTER (WHERE status = 'active') AS active_count,
COUNT(*) FILTER (WHERE status = 'inactive') AS inactive_count
FROM users;
-- LATERAL joins
SELECT u.name, recent.order_id
FROM users u,
LATERAL (
SELECT order_id FROM orders WHERE user_id = u.id
ORDER BY created_at DESC LIMIT 3
) AS recent;
Indexes
-- Standard B-tree index (both)
CREATE INDEX idx_users_email ON users (email);
-- Partial index ??PostgreSQL (index subset of rows)
CREATE INDEX idx_active_users ON users (email) WHERE active = true;
-- Only indexes active users ??smaller, faster for queries with WHERE active = true
-- Composite index
CREATE INDEX idx_orders_user_date ON orders (user_id, created_at DESC);
-- GIN index ??PostgreSQL (arrays, JSONB, full-text)
CREATE INDEX idx_tags ON posts USING GIN (tags); -- array column
CREATE INDEX idx_meta ON posts USING GIN (metadata); -- JSONB column
-- GiST index ??PostgreSQL (geometric, range types, full-text)
CREATE INDEX idx_location ON places USING GIST (coordinates); -- PostGIS
-- MySQL-specific
-- FULLTEXT index
CREATE FULLTEXT INDEX idx_search ON articles (title, body);
-- Invisible index (MySQL 8.0) ??test impact without dropping
ALTER TABLE users ALTER INDEX idx_email INVISIBLE;
-- Both: covering index (include extra columns to avoid table lookups)
-- PostgreSQL:
CREATE INDEX idx_orders_covering ON orders (user_id) INCLUDE (status, total);
-- MySQL:
CREATE INDEX idx_orders_covering ON orders (user_id, status, total);
Replication
PostgreSQL replication options:
Streaming replication (built-in)
- Primary ??one or more standbys
- Synchronous or asynchronous
- Standbys can be used for read queries
Logical replication (PostgreSQL 10+)
- Table-level, row-level filtering
- Replicate to different PostgreSQL versions
Third-party: Patroni (HA), pgpool-II (connection pooling + HA)
MySQL replication options:
Classic async replication (simple, widely used)
Semi-synchronous replication (primary waits for at least one replica)
Group Replication (multi-primary, built-in)
InnoDB Cluster (MySQL Shell + Group Replication)
ProxySQL (connection routing to primary/replicas)
-- PostgreSQL: check replication status
SELECT
client_addr,
state,
sent_lsn - write_lsn AS write_lag,
sent_lsn - flush_lsn AS flush_lag,
sent_lsn - replay_lsn AS replay_lag
FROM pg_stat_replication;
-- MySQL: check replication status
SHOW REPLICA STATUS\G
-- Shows: Seconds_Behind_Source, Replica_IO_Running, Replica_SQL_Running
High availability, failover, and what ?�zero downtime??really costs
Neither database magically eliminates outages. The difference is in patterns your team is comfortable running. PostgreSQL is often paired with Patroni, repmgr, or managed RDS/Aurora/Cloud SQL/Neon-style services that provide automated failover, backups, and PITR. MySQL has decades of experience with async primaries and replicas, ProxySQL routing, and InnoDB Cluster / Group Replication for multi?�primary (with operational complexity). A candid comparison: MySQL?�s replication ecosystem is ubiquitous; PostgreSQL?�s logical replication is powerful for selective upgrades and data movement but requires explicit monitoring of slots and LSNs.
| HA need | Common PostgreSQL approach | Common MySQL approach |
|---|---|---|
| Automatic failover | Patroni, cloud HA agents | Orchestrator, cloud HA, InnoDB Cluster tooling |
| Read scaling | Hot standbys, logical subscribers | One primary + N replicas, routes via proxy |
| Geographic distribution | Logical replication, careful conflict rules | Async replication, multi?�region with lag awareness |
| Online schema changes | pg_repack, managed blue/green, careful locks | pt-online-schema-change, gh-ost culture is strong in MySQL shops |
Fair warning: the fastest way to a bad day is a split?�brain or a replica that answers stale reads as if fresh. If you are building payments or inventory, invest in explicit session routing and monitor lag p99.
Scalability, sharding, and when to look beyond a single node
Vertical scaling (bigger instance, better disks, more RAM) remains the first lever for most OLTP. Read replicas help read?�heavy, eventually?�consistent use cases. Sharding (partitioning data across many primaries) is a product decision as much as a database decision. Application?�level sharding (Vitess, custom tenant keys) is common in large MySQL estates; in PostgreSQL, Citus and distributed patterns exist but are not universal defaults.
| Path | When it fits | Cost / trade?�off |
|---|---|---|
| Bigger node + good indexes | Sub?�ms reads, working set in memory | Simpler ops; eventual ceiling on CPU/IO |
| Read replicas | Dashboards, search indexes, report queries | Staleness, routing complexity, duplicate cache invalidation work |
| Application sharding | Very large, tenant?�isolated data | Migrations, cross?�shard queries, heavy engineering |
| Specialized store | Pure OLAP, time?�series at huge scale | Second system to operate (often worth it) |
A balanced judgment: if you are a typical web product, a single well?�tuned instance plus replicas takes you very far. If you know you will outgrow it, start with a sharding key in your data model even before you split physical nodes?�it is painful to retro?�fit.
Ecosystem: drivers, hosting, and operations
| Layer | PostgreSQL | MySQL / MariaDB |
|---|---|---|
| Drivers (Node, Go, etc.) | pg / pgx / libpq family | mysql2, official connectors; massive examples online |
| ORMs | Prisma, Drizzle, SQLAlchemy, Rails?�first?�class | Same ORMs, excellent Laravel/WordPress heritage |
| Migrations | sqitch, flyway, liquibase, Prisma Migrate | Same; MySQL?�s DDL + locking behavior differs (measure on large tables) |
| Managed cloud | RDS, Aurora PG, Cloud SQL, Azure PG, Neon, etc. | RDS, Aurora MySQL, Cloud SQL, PlanetScale (MySQL protocol) |
| Observability | pg_stat_statements, auto_explain, great logs | performance_schema, EXPLAIN ANALYZE (8.0+), slow log |
This is an ecosystem tie for many teams: pick what your SREs already run at night. The ecosystem advantage often goes to the engine your hosting provider and framework defaults optimize for.
Use?�case?�based selection (without tribalism)
| You are building?? | Sensible default | Why it is a reasonable bias |
|---|---|---|
| B2B SaaS with complex reporting | PostgreSQL | Rich SQL, JSONB, analytics?�friendly |
| High?�QPS, simple queries, existing MySQL SREs | MySQL 8 (InnoDB) | Mature playbooks, huge community corpus |
| Geospatial | PostgreSQL + PostGIS | Industry standard for GIS in SQL |
| WordPress / PHP CMS | MySQL / MariaDB | Ecosystem and hosting defaults |
| Serverless/edge | Whichever the vendor nails (e.g. Neon, PlanetScale) | Vendor product can matter more than engine theology |
If two cells both seem true, use the one your team can operate safely. A slightly ?�weaker??database with strong backups and on?�call runbooks beats a ?�stronger??engine with nobody watching replication lag.
Production experience (patterns that survive audits)
What actually breaks in production is rarely a missing SQL function?�it is connection storms, long transactions holding locks, N+1 queries, and unbounded background jobs hammering the primary. I have seen PostgreSQL instances shine in organizations that invested in pgbouncer pool sizing, regular VACUUM health checks, and pg_stat_statements to kill regressions. I have seen MySQL fleets carry enormous traffic with disciplined schema discipline (proper InnoDB, no accidental MyISAM), careful replication lag dashboards, and gh-ost for low?�drama migrations. Both engines can surprise you after major version upgrades?�always run replay tests and check planner changes, not just ?�it boots.??
A fair lesson: standardize on migrations in CI, synthetic canaries for your hottest queries, and automated restore drills. Those practices pay for either database?�and they pay for your sleep.
Performance Tips
-- EXPLAIN ANALYZE (PostgreSQL)
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > NOW() - INTERVAL '30 days'
GROUP BY u.id;
-- Key things to look for:
-- Seq Scan ??missing index
-- Hash Join vs Nested Loop vs Merge Join
-- Actual rows vs Estimated rows (large diff = stale statistics)
-- Buffers: hit vs read (cache hit ratio)
-- Update statistics
ANALYZE users; -- Update table statistics
VACUUM ANALYZE; -- Reclaim space + update statistics
-- EXPLAIN in MySQL
EXPLAIN FORMAT=JSON
SELECT u.name, COUNT(o.id)
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id;
-- Check index usage
SHOW INDEX FROM users;
SELECT * FROM information_schema.STATISTICS WHERE table_name = 'users';
-- Connection pooling (critical for both)
-- PostgreSQL: PgBouncer
-- MySQL: ProxySQL or MySQL Router
-- PostgreSQL: table bloat (MVCC creates dead tuples)
-- Regular VACUUM prevents bloat
ALTER TABLE orders SET (autovacuum_vacuum_scale_factor = 0.05); -- More frequent vacuum for large tables
-- PostgreSQL: check table size
SELECT
relname AS table_name,
pg_size_pretty(pg_total_relation_size(relid)) AS total_size,
pg_size_pretty(pg_relation_size(relid)) AS table_size,
pg_size_pretty(pg_indexes_size(relid)) AS index_size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;
Extensions (PostgreSQL Only)
PostgreSQL’s extension system is a significant differentiator.
-- List available extensions
SELECT name, default_version, comment FROM pg_available_extensions ORDER BY name;
-- PostGIS ??geospatial data
CREATE EXTENSION postgis;
SELECT ST_Distance(
ST_GeographyFromText('POINT(-122.4194 37.7749)'), -- San Francisco
ST_GeographyFromText('POINT(-118.2437 34.0522)') -- Los Angeles
) / 1000 AS distance_km;
-- pg_trgm ??trigram similarity (fuzzy search)
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_name_trgm ON users USING GIN (name gin_trgm_ops);
SELECT name FROM users WHERE name % 'Alce'; -- typo-tolerant search
SELECT name, similarity(name, 'Alice') AS sim FROM users ORDER BY sim DESC LIMIT 10;
-- uuid-ossp ??UUID generation
CREATE EXTENSION "uuid-ossp";
INSERT INTO items (id, name) VALUES (uuid_generate_v4(), 'Widget');
-- pg_stat_statements ??query performance tracking
CREATE EXTENSION pg_stat_statements;
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC LIMIT 10;
-- timescaledb ??time-series data
CREATE EXTENSION timescaledb;
SELECT create_hypertable('metrics', 'time');
Node.js Integration
// PostgreSQL with node-postgres (pg)
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
});
const result = await pool.query(
'SELECT * FROM users WHERE email = $1', // Parameterized query (prevents SQL injection)
['[email protected]']
);
// MySQL with mysql2
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
uri: process.env.DATABASE_URL,
waitForConnections: true,
connectionLimit: 20,
});
const [rows] = await pool.execute(
'SELECT * FROM users WHERE email = ?', // ? for parameters
['[email protected]']
);
// Both work with ORMs ??Prisma supports both
// prisma/schema.prisma
datasource db {
provider = "postgresql" // or "mysql"
url = env("DATABASE_URL")
}
Related posts:
- [Prisma ORM Complete Guide](/en/blog/prisma-complete-guide/
- [Redis Complete Guide](/en/blog/redis-advanced-guide/
- [Docker Compose Complete Guide](/en/blog/docker-compose-complete-guide/
On?�page FAQ (quick links to details above)
When should I apply this in production?
A. When you are choosing an engine, designing replication, or planning JSON and search. Use the decision matrix and use?�case table to align the database with your access patterns, not a generic blog ranking.
What should I read next?
A. Follow the Related posts at the end of this article for Prisma, Redis, and Docker Compose, which are the usual companions to a production SQL deployment.
Where can I go deeper on internals?
A. The official PostgreSQL documentation and MySQL Reference Manual are authoritative. For performance, prioritize running EXPLAIN (ANALYZE) and measuring p95/p99 on your own data.
Related posts (internal links)
- Prisma ORM Complete Guide | Schema, migrations, queries, type safety
- Redis Advanced Guide | Caching, Pub/Sub, streams, cluster tuning
- Docker Compose Complete Guide | Multi-container apps, networks, volumes
Search keywords: PostgreSQL, MySQL, SQL, database, backend, performance, replication, OLTP, OLAP, JSONB, InnoDB.