SQL Practice Logo

SQLPractice Online

Database System Fundamentals: Mistakes

Module: Database-Specific Features

Using MySQL for high read concurrency workload (analytics dashboard with 1000 concurrent users)

Use PostgreSQL with MVCC for high read concurrency (writers don't block readers)

MySQL InnoDB uses locking: writers acquire exclusive locks, blocking readers. With 1000 concurrent readers and frequent writes, readers wait for locks (100-1000ms). PostgreSQL MVCC allows writers and readers to work simultaneously without blocking. Writers create new row versions, readers see old versions. Uber migrated from MySQL to PostgreSQL for ride matching: 50K writes/sec, zero blocking reads. Check lock waits: MySQL (SHOW ENGINE INNODB STATUS, look for "LOCK WAIT"), PostgreSQL (SELECT * FROM pg_stat_activity WHERE wait_event_type = 'Lock').

Analyze workload before choosing database. High read concurrency (>100 concurrent readers with frequent writes) = use PostgreSQL MVCC. Write-heavy with few readers = MySQL InnoDB is fine.

High

Slow queries, lock waits, timeout errors

Setting buffer pool to 90% of RAM on shared server (database + application on same server)

Set buffer pool to 50-60% of RAM on shared server, 70-80% on dedicated database server

Buffer pool caches database pages in RAM. Setting too high causes OS to swap (1000x slower than RAM). On shared server, application needs RAM too. Example: 64GB RAM server, buffer pool 90% (58GB), application needs 10GB, OS needs 2GB = 70GB total > 64GB RAM = swapping. Correct: buffer pool 50% (32GB), application 10GB, OS 2GB, free 20GB = no swapping. Monitor: Linux (free -h, check "Swap" usage), MySQL (SHOW VARIABLES LIKE 'innodb_buffer_pool_size'), PostgreSQL (SHOW shared_buffers). If swapping occurs, reduce buffer pool by 10-20%.

Start conservative (50% RAM), monitor buffer pool hit ratio (should be >95%), increase gradually if hit ratio is low and no swapping occurs. Dedicated database server: 70-80% RAM. Shared server: 50-60% RAM.

Critical

Out of memory, swapping, system crash

Using SERIALIZABLE isolation level for all queries ("maximum safety")

Use READ COMMITTED for most queries, REPEATABLE READ for financial transactions, SERIALIZABLE only for critical data with known conflicts

SERIALIZABLE provides full isolation but is 10x slower than READ COMMITTED. It detects conflicts and aborts transactions with "serialization failure" errors. Example: Two transactions read account balance ($1000), both try to withdraw $600, one succeeds, other aborts. Application must retry. For most queries, READ COMMITTED is sufficient: see committed data, no dirty reads. Use REPEATABLE READ for financial transactions: same query returns same results within transaction. Use SERIALIZABLE only for critical data with known conflicts (inventory updates, seat reservations). Check isolation level: PostgreSQL (SHOW transaction_isolation), MySQL (SELECT @@transaction_isolation).

Default isolation levels are good: PostgreSQL (READ COMMITTED), MySQL (REPEATABLE READ). Only change if you have specific requirements. Monitor serialization failures: PostgreSQL (SELECT * FROM pg_stat_database WHERE datname = 'mydb', check "serialization_failures").

Medium

Slow queries (10x slower), frequent serialization failures, deadlocks

Creating new database connection for every HTTP request (no connection pooling)

Use connection pooling: PgBouncer (PostgreSQL), ProxySQL (MySQL), or application-level pooling

Creating database connection is expensive: TCP handshake, authentication, session initialization = 100ms overhead. With 1000 req/sec, that's 100 seconds of wasted time per second (impossible). Connection pooling reuses connections: 1ms overhead. Example: Without pooling: 1000 req/sec × 100ms = 100 connections/sec created, max_connections=100 exhausted in 1 second. With pooling: 10 pooled connections handle 1000 req/sec, each connection serves 100 req/sec. Setup: PgBouncer (PostgreSQL): pool_mode=transaction, default_pool_size=25, max_client_conn=1000. ProxySQL (MySQL): max_connections=1000, mysql-max_connections=100.

Always use connection pooling in production. Application-level pooling (HikariCP, pgbouncer) is easier to setup. Database-level pooling (PgBouncer, ProxySQL) enables connection sharing across multiple applications. Monitor: active connections should be <50% of max_connections.

Critical

Slow response times (100ms overhead per request), connection limit exhausted, database crash

Ignoring PostgreSQL MVCC bloat (never running VACUUM, n_dead_tup growing)

Run VACUUM regularly: autovacuum enabled (default), manual VACUUM on high-update tables, monitor n_dead_tup

PostgreSQL MVCC keeps old row versions for concurrent readers. After transactions commit, old versions become "dead tuples". VACUUM removes dead tuples and reclaims space. Without VACUUM, tables grow 10x larger: 1GB table with 90% dead tuples = 10GB on disk. Queries scan all pages (including dead tuples) = 10x slower. Check bloat: SELECT schemaname, tablename, n_live_tup, n_dead_tup, ROUND(100.0 * n_dead_tup / (n_live_tup + n_dead_tup), 2) AS dead_pct FROM pg_stat_user_tables WHERE n_dead_tup > 10000 ORDER BY n_dead_tup DESC. If dead_pct > 20%, run VACUUM. Autovacuum runs automatically but may be too slow for high-update tables. Manual VACUUM: VACUUM ANALYZE tablename (runs in background, doesn't block queries).

Enable autovacuum (default): autovacuum = on. Monitor dead tuples weekly: alert if dead_pct > 20%. For high-update tables (>10K updates/min), run manual VACUUM daily. VACUUM FULL reclaims space but locks table (avoid in production).

High

Table bloat (10x larger), slow queries (10x slower), disk space exhausted