Relational databases remain the backbone of most applications. While basic SQL is table stakes, interviews for senior backend roles probe deeper—indexing strategies, query optimization, transaction isolation, and operational knowledge separate strong candidates from the rest.
This 2026 update uses PostgreSQL 18 and MySQL 8.4 LTS as stable reference lines. MySQL also publishes faster-moving Innovation releases; confirm the target production line before relying on version-specific behavior.
Table of Contents
- PostgreSQL vs MySQL Questions
- Indexing Strategy Questions
- Query Optimization Questions
- Transaction and Locking Questions
- Advanced Data Types Questions
- Replication and High Availability Questions
- Performance Tuning Questions
- Quick Reference
PostgreSQL vs MySQL Questions
These questions test your understanding of when to choose each database and their architectural differences.
What are the key architectural differences between PostgreSQL and MySQL?
| Aspect | PostgreSQL | MySQL (InnoDB) |
|---|---|---|
| Process model | Process per connection | Thread per connection |
| MVCC implementation | Stores versions in main table | Stores versions in undo logs |
| Replication | Logical + streaming | Binary log replication |
| Default isolation | READ COMMITTED | REPEATABLE READ |
| JSON support | JSON/JSONB; GIN and expression indexes | Binary JSON; generated-column and multi-valued indexes |
| Full-text search | Built-in, configurable | Built-in, simpler |
| Extensions | Rich ecosystem (PostGIS, etc.) | Limited |
When would you choose PostgreSQL over MySQL?
Choose PostgreSQL when you need its specific strengths:
- PostGIS or another PostgreSQL extension
- JSONB with indexing for semi-structured data
- Custom types, functions, and extensions
- Advanced indexing (partial, expression, GIN/GiST)
-- PostgreSQL strengths: Window functions with complex logic
SELECT
customer_id,
order_date,
amount,
SUM(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) as running_total,
amount - LAG(amount) OVER (
PARTITION BY customer_id
ORDER BY order_date
) as change_from_previous
FROM orders;
-- JSONB querying with indexing
CREATE INDEX idx_orders_metadata ON orders USING GIN (metadata jsonb_path_ops);
SELECT * FROM orders WHERE metadata @> '{"priority": "high"}';When would you choose MySQL over PostgreSQL?
Choose MySQL when its specific strengths match your constraints:
- Existing InnoDB/MySQL operational expertise and tooling
- Compatibility with a product or platform standardized on MySQL
- MySQL Group Replication or InnoDB Cluster as the chosen HA model
- MySQL's replication, Performance Schema, or HeatWave/Oracle ecosystem
- Lower migration risk for an established MySQL application
Neither database is universally simpler or faster. Benchmark the real schema and queries, and rehearse backups, restore, failover, upgrades, and observability before choosing.
-- MySQL strengths: Simple read replica setup
-- Primary
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='primary.example.com',
SOURCE_USER='repl',
SOURCE_PASSWORD='password',
SOURCE_AUTO_POSITION=1;
START REPLICA;Indexing Strategy Questions
These questions test whether you can design indexes for real query shapes and explain their costs.
How does a B-tree index work?
flowchart TB
Root["[M]<br/>Root"]
Root --> I1["[D, H]<br/>Internal"]
Root --> I2["[P, T]<br/>Internal"]
Root --> I3["[X]<br/>Internal"]
I1 --> L1["A,B,C<br/>Leaf"]
I1 --> L2["E,F,G<br/>Leaf"]
I1 --> L3["I,J,K<br/>Leaf"]
I1 --> L4["L<br/>Leaf"]Key properties:
- Tree height grows slowly, making equality and range navigation efficient; actual cost also depends on cache, selectivity, row fetches, and storage
- Sorted order enables range queries and ORDER BY
- Leaf nodes are linked for efficient range scans
- Works for:
=,<,>,<=,>=,BETWEEN,LIKE 'prefix%'
What is a composite index and how does column order matter?
-- Composite index on (country, city, created_at)
CREATE INDEX idx_location_date ON users(country, city, created_at);
-- Uses full index (all 3 columns)
SELECT * FROM users
WHERE country = 'US' AND city = 'NYC' AND created_at > '2024-01-01';
-- Uses first 2 columns
SELECT * FROM users WHERE country = 'US' AND city = 'NYC';
-- Uses first column only
SELECT * FROM users WHERE country = 'US';
-- No leading-column predicate; the planner may scan, use skip scan, or choose another index
SELECT * FROM users WHERE city = 'NYC';
-- Can use the country prefix, but created_at cannot normally narrow navigation
SELECT * FROM users WHERE country = 'US' AND created_at > '2024-01-01';
-- A dedicated (country, created_at) index may be better for this query shapeIndex column order rule: Place columns in order of:
- Equality conditions first (
=) - Then the first range or ordering requirement that should limit the scan
- Add included/output columns only when index-only access is valuable
This is a heuristic, not a syntax rule. PostgreSQL 18 can use skip scan in suitable cases, and both optimizers may use only a prefix or choose a different plan. Base the order on actual predicates, ordering, selectivity, write cost, and EXPLAIN evidence—not “highest cardinality first.”
What index types are available beyond B-tree?
-- PostgreSQL index types
-- Hash index: Equality only; benchmark against B-tree rather than assuming faster
CREATE INDEX idx_email_hash ON users USING HASH (email);
-- GIN (Generalized Inverted Index): Arrays, JSONB, full-text
CREATE INDEX idx_tags ON articles USING GIN (tags);
CREATE INDEX idx_metadata ON products USING GIN (metadata jsonb_path_ops);
CREATE INDEX idx_search ON documents USING GIN (to_tsvector('english', content));
-- GiST (Generalized Search Tree): Geometric, range types, full-text
CREATE INDEX idx_location ON places USING GIST (coordinates);
CREATE INDEX idx_schedule ON events USING GIST (time_range);
-- BRIN (Block Range Index): Large sequential data, minimal storage
CREATE INDEX idx_created ON logs USING BRIN (created_at);| Index Type | Use Case | Size | Query Types |
|---|---|---|---|
| B-tree | General purpose | Medium | =, <, >, range, ORDER BY |
| Hash | Equality only | Small | = only |
| GIN | Arrays, JSONB, text | Large | Contains, overlap |
| GiST | Geometric, ranges | Medium | Spatial, range overlap |
| BRIN | Time-series, sequential | Tiny | Range on sorted data |
What is a covering index and how does it enable index-only scans?
-- Regular index: Must fetch row from table
CREATE INDEX idx_email ON users(email);
SELECT name FROM users WHERE email = 'test@example.com';
-- Index lookup → Row fetch → Return name
-- Covering index: All data in index
CREATE INDEX idx_email_name ON users(email) INCLUDE (name);
SELECT name FROM users WHERE email = 'test@example.com';
-- Index lookup → Return name (no table access!)
-- PostgreSQL INCLUDE syntax (non-key columns)
CREATE INDEX idx_order_lookup ON orders(customer_id)
INCLUDE (order_date, total);What is a partial index and when would you use it?
-- Index only active users (much smaller than full index)
CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';
-- Index a stable subset such as unprocessed jobs
CREATE INDEX idx_pending_jobs ON jobs(queue, created_at)
WHERE processed_at IS NULL;
-- Index only non-null values
CREATE INDEX idx_phone ON users(phone)
WHERE phone IS NOT NULL;
-- Query must match WHERE clause to use partial index
SELECT * FROM users WHERE email = 'test@example.com' AND status = 'active';Query Optimization Questions
These questions test your ability to read execution plans and diagnose performance issues.
How do you read and interpret an execution plan?
EXPLAIN (ANALYZE, BUFFERS)
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.country = 'US'
GROUP BY u.id, u.name
ORDER BY order_count DESC
LIMIT 10;Limit (cost=1250.23..1250.26 rows=10 width=48) (actual time=45.2..45.3 rows=10 loops=1)
-> Sort (cost=1250.23..1256.73 rows=2600 width=48) (actual time=45.2..45.2 rows=10 loops=1)
Sort Key: (count(o.id)) DESC
Sort Method: top-N heapsort Memory: 25kB
-> HashAggregate (cost=1150.00..1189.00 rows=2600 width=48) (actual time=42.1..44.8 rows=2600 loops=1)
Group Key: u.id
-> Hash Right Join (cost=85.50..1020.00 rows=26000 width=44) (actual time=1.2..28.5 rows=26000 loops=1)
Hash Cond: (o.user_id = u.id)
-> Seq Scan on orders o (cost=0.00..620.00 rows=26000 width=8) (actual time=0.01..8.2 rows=26000 loops=1)
-> Hash (cost=73.00..73.00 rows=1000 width=40) (actual time=1.1..1.1 rows=1000 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 72kB
-> Seq Scan on users u (cost=0.00..73.00 rows=1000 width=40) (actual time=0.02..0.8 rows=1000 loops=1)
Filter: (country = 'US'::text)
Rows Removed by Filter: 9000
Planning Time: 0.5 ms
Execution Time: 45.5 ms
Key things to look for:
- Seq Scan → Check selectivity and pages read; it is often correct when much of a table is needed
- Nested Loop → Multiply actual rows by loops before deciding whether repeated inner work is excessive
- actual rows far from rows → Investigate stale statistics, skew, correlations, expressions, and parameter values
- Temp read/write or external sort → A spill occurred; tune the query/index or carefully size per-operation memory
EXPLAIN ANALYZE executes the statement. Use a rollback-safe test environment for writes and avoid casually running it against expensive or destructive production statements.
What query patterns prevent index usage?
-- Pattern 1: An expression needs a matching expression index
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
-- Fix: Expression index
CREATE INDEX idx_email_lower ON users(LOWER(email));
-- Anti-pattern 2: Implicit type conversion
SELECT * FROM orders WHERE order_id = '12345'; -- order_id is INT
-- Fix: Use correct type
SELECT * FROM orders WHERE order_id = 12345;
-- Pattern 3: A normal B-tree cannot navigate a leading wildcard efficiently
SELECT * FROM users WHERE name LIKE '%smith';
-- Fix: Full-text search or trigram index
CREATE INDEX idx_name_trgm ON users USING GIN (name gin_trgm_ops);
-- Pattern 4: OR may use bitmap index combination; verify the plan first
SELECT * FROM users WHERE email = 'a@b.com' OR phone = '555-1234';
-- UNION is an alternative only when its duplicate semantics are intended
SELECT * FROM users WHERE email = 'a@b.com'
UNION
SELECT * FROM users WHERE phone = '555-1234';
-- Pattern 5: NOT IN has surprising three-valued logic when the subquery returns NULL
SELECT * FROM orders WHERE customer_id NOT IN (SELECT id FROM inactive_customers);
-- Fix: NOT EXISTS or LEFT JOIN IS NULL
SELECT o.* FROM orders o
LEFT JOIN inactive_customers ic ON o.customer_id = ic.id
WHERE ic.id IS NULL;What is the N+1 query problem and how do you solve it?
-- N+1 pattern in application code (pseudocode):
users = query("SELECT * FROM users WHERE country = 'US'") -- 1 query
for user in users:
orders = query("SELECT * FROM orders WHERE user_id = ?", user.id) -- N queries
-- Total: 1 + N queries (if 1000 users, that's 1001 queries!)
-- Solution: JOIN or subquery
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.country = 'US'; -- 1 query
-- Or with aggregation
SELECT
u.*,
COALESCE(order_stats.count, 0) as order_count,
COALESCE(order_stats.total, 0) as order_total
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) as count, SUM(amount) as total
FROM orders
GROUP BY user_id
) order_stats ON order_stats.user_id = u.id
WHERE u.country = 'US';Transaction and Locking Questions
These questions test your understanding of transactions, data integrity, and concurrency.
What are ACID properties and why do they matter?
- Atomicity: All or nothing
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- If either fails, both are rolled back
COMMIT;- Consistency: Database moves from one valid state to another
-- Constraints ensure consistency
ALTER TABLE accounts ADD CONSTRAINT positive_balance CHECK (balance >= 0);
-- Transfer that would make balance negative will fail- Isolation: Concurrent transactions don't interfere
-- Transaction A reads balance, Transaction B updates it
-- Isolation level determines which anomalies A may observe- Durability: Under the documented durability configuration, committed changes survive the failures covered by that configuration
COMMIT;Durability is not independent of configuration and infrastructure. PostgreSQL fsync, synchronous_commit, replication mode, storage write caches, and MySQL innodb_flush_log_at_trx_commit/sync_binlog change which failure scenarios are covered.
What are the four isolation levels and their trade-offs?
The SQL standard names four levels, but the guarantees and locking behavior are implementation-specific:
| Level | PostgreSQL 18 | MySQL 8.4 InnoDB |
|---|---|---|
| READ UNCOMMITTED | Behaves as READ COMMITTED; no dirty reads | Dirty reads are possible |
| READ COMMITTED | Default; new snapshot for each statement | New consistent-read snapshot per read |
| REPEATABLE READ | Transaction snapshot; no phantom reads, but serialization anomalies remain possible | Default; consistent snapshot plus next-key locking for locking reads |
| SERIALIZABLE | Serializable Snapshot Isolation; conflicting transactions may abort | Stricter locking behavior; deadlocks/timeouts remain possible |
-- Set isolation level
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- All reads in this transaction see consistent snapshot
SELECT balance FROM accounts WHERE id = 1; -- Returns 1000
-- Even if another transaction commits a change...
SELECT balance FROM accounts WHERE id = 1; -- Still returns 1000
COMMIT;
-- PostgreSQL serializable: retry the whole transaction on SQLSTATE 40001
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT SUM(balance) FROM accounts;
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
COMMIT; -- May fail: "could not serialize access"There is no universal “fastest to slowest” ordering and no isolation level chosen merely because data is financial. Define the business invariant, understand the engine's anomalies, keep transactions short, and implement bounded retries for serialization failures and deadlocks.
How does PostgreSQL implement MVCC?
flowchart TB
subgraph INITIAL["Row Version Structure"]
R1["xmin: 100 | xmax: 0<br/>id=1, name='Alice'<br/><i>Created by txn 100</i>"]
end
subgraph AFTER["After UPDATE by transaction 150"]
R2["xmin: 100 | xmax: 150<br/>id=1, name='Alice'<br/><i>Old version (dead)</i>"]
R3["xmin: 150 | xmax: 0<br/>id=1, name='Alice Smith'<br/><i>New version</i>"]
end
INITIAL -->|"UPDATE"| AFTERVisibility rules:
- Tuple headers store creating/deleting transaction IDs and flags; snapshot visibility also depends on transaction status and the active/in-progress transaction set, so simple numeric comparison is insufficient
- READ COMMITTED takes a fresh snapshot per command; REPEATABLE READ and SERIALIZABLE keep a transaction-level snapshot
- Ordinary readers generally do not block writers, but writers can block writers and explicit locks still apply
- VACUUM marks dead-tuple space reusable, maintains the visibility map, and prevents transaction-ID wraparound; ordinary VACUUM usually does not return space to the operating system
-- Check the current transaction ID (assigns one if needed)
SELECT pg_current_xact_id();
-- See dead rows (before vacuum)
SELECT * FROM pg_stat_user_tables WHERE relname = 'accounts';
-- n_dead_tup is an estimate, not an exact live countWhat is the difference between optimistic and pessimistic locking?
-- Pessimistic locking: Lock row immediately
BEGIN;
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE;
-- Row is locked, other transactions wait
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 1;
COMMIT;
-- FOR UPDATE variants:
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE NOWAIT; -- Fail immediately if locked
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE SKIP LOCKED; -- Skip locked rows
-- Optimistic locking: Check version at update time
-- Application maintains version column
UPDATE inventory
SET quantity = quantity - 1, version = version + 1
WHERE product_id = 1 AND version = 5;
-- If 0 rows affected, someone else modified it → retry
-- With timestamp
UPDATE inventory
SET quantity = quantity - 1, updated_at = NOW()
WHERE product_id = 1 AND updated_at = '2024-01-15 10:30:00';| Approach | Pros | Cons | Use When |
|---|---|---|---|
| Pessimistic | Makes conflicting access wait or fail explicitly | Blocking, deadlocks, lock-order discipline | Short critical sections with known contention |
| Optimistic | Detects conflicting updates without holding a read lock | Updates still lock; conflicts require retry/merge | Conflicts are uncommon and retries are safe |
Neither approach guarantees a business invariant by itself. The predicate, transaction boundary, constraints, lock scope, and retry/idempotency behavior must all be correct.
Advanced Data Types Questions
These questions test your knowledge of modern database types beyond basic scalars.
When should you use JSON vs JSONB in PostgreSQL?
-- JSONB: Binary storage, most common choice
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
attributes JSONB -- Flexible schema for product attributes
);
-- Insert JSONB
INSERT INTO products (name, attributes) VALUES
('Laptop', '{"brand": "Dell", "specs": {"ram": 16, "storage": "512GB"}}');
-- Query JSONB
SELECT * FROM products WHERE attributes->>'brand' = 'Dell';
SELECT * FROM products WHERE attributes @> '{"brand": "Dell"}';
SELECT * FROM products WHERE attributes->'specs'->>'ram' = '16';
-- JSONB operators
-- -> : Get JSON element (returns JSON)
-- ->> : Get JSON element as text
-- @> : Contains
-- ? : Key exists
-- ?| : Any key exists
-- ?& : All keys exist
-- Index JSONB for fast queries
CREATE INDEX idx_attributes ON products USING GIN (attributes);
CREATE INDEX idx_brand ON products ((attributes->>'brand'));
-- JSONB vs JSON
-- JSON: Preserves whitespace, key order, duplicates. Parsed on each access.
-- JSONB: Decomposed binary form, keeps the last duplicate key, indexable.How do you query nested JSONB efficiently?
-- Path queries with jsonb_path_query
SELECT * FROM products
WHERE jsonb_path_exists(attributes, '$.specs.ram ? (@ > 8)');
-- Aggregate JSON
SELECT
attributes->>'brand' as brand,
COUNT(*),
AVG((attributes->'specs'->>'ram')::int) as avg_ram
FROM products
GROUP BY attributes->>'brand';
-- Update nested JSONB
UPDATE products
SET attributes = jsonb_set(attributes, '{specs,ram}', '32')
WHERE id = 1;
-- Add key
UPDATE products
SET attributes = attributes || '{"warranty": "2 years"}'
WHERE id = 1;
-- Remove key
UPDATE products
SET attributes = attributes - 'warranty'
WHERE id = 1;How do you work with array columns in PostgreSQL?
-- Array column
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT,
tags TEXT[]
);
INSERT INTO articles (title, tags) VALUES
('PostgreSQL Tips', ARRAY['database', 'postgresql', 'performance']);
-- Query arrays
SELECT * FROM articles WHERE 'postgresql' = ANY(tags);
SELECT * FROM articles WHERE tags @> ARRAY['postgresql', 'database'];
SELECT * FROM articles WHERE tags && ARRAY['mysql', 'postgresql']; -- Overlap
-- Array functions
SELECT array_length(tags, 1) FROM articles;
SELECT unnest(tags) FROM articles; -- Expand to rows
-- Index for array queries
CREATE INDEX idx_tags ON articles USING GIN (tags);How do you implement full-text search in PostgreSQL?
-- Create text search column
ALTER TABLE articles ADD COLUMN search_vector tsvector;
UPDATE articles SET search_vector =
to_tsvector('english', title || ' ' || COALESCE(content, ''));
CREATE INDEX idx_search ON articles USING GIN (search_vector);
-- Search with ranking
SELECT title, ts_rank(search_vector, query) as rank
FROM articles, to_tsquery('english', 'postgresql & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC;
-- Phrase search
SELECT * FROM articles
WHERE search_vector @@ phraseto_tsquery('english', 'database performance');
-- Auto-update with trigger
CREATE TRIGGER update_search_vector
BEFORE INSERT OR UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'pg_catalog.english', title, content);Replication and High Availability Questions
These questions test your knowledge of database redundancy and failover capabilities.
How does PostgreSQL streaming replication work?
flowchart TB
subgraph REPLICATION["Primary-Replica Architecture"]
direction LR
PRIMARY["Primary<br/>Read/Write"]
REPLICA["Replica<br/>Read Only"]
PRIMARY -->|"WAL Stream"| REPLICA
end
APP_W["App<br/>(Write)"]
APP_R["App<br/>(Read)"]
APP_W --> PRIMARY
APP_R --> REPLICA-- Primary configuration (postgresql.conf)
wal_level = replica
max_wal_senders = 10
# Synchronous replication additionally requires synchronous_standby_names.
# synchronous_commit controls what acknowledgement a commit waits for.
-- Replica setup
-- pg_basebackup -h primary -D /var/lib/postgresql/data -U replicator -P
-- standby.signal file indicates replica mode
-- primary_conninfo in postgresql.auto.conf
-- Check replication status (on primary)
SELECT client_addr, state, sent_lsn, write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;
-- On a standby: time since the last replayed commit (NULL when unavailable)
SELECT NOW() - pg_last_xact_replay_timestamp() AS replication_lag;Physical streaming replication is asynchronous by default. A lag interval alone can mislead when the primary is idle; also monitor LSN byte lag, receive/replay state, WAL retention, replication slots, conflicts, and the freshness requirement seen by the application. Replication is not a backup, and automatic failover requires an external control plane or managed service.
How do you set up MySQL replication?
-- Source (primary) configuration
[mysqld]
server-id = 1
log_bin = mysql-bin
binlog_format = ROW
gtid_mode = ON
enforce_gtid_consistency = ON
-- Replica configuration
[mysqld]
server-id = 2
relay_log = relay-bin
read_only = ON
gtid_mode = ON
enforce_gtid_consistency = ON
-- Set up replication on replica
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = 'primary.example.com',
SOURCE_USER = 'repl',
SOURCE_PASSWORD = '<provision securely>',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
-- Check status
SHOW REPLICA STATUS\GThis is an outline, not a complete production runbook. Use a least-privilege replication account, TLS and certificate verification, backups plus restore tests, monitoring, and a rehearsed promotion/failback procedure. GTID-based asynchronous replication does not by itself provide automatic failover or zero data loss; evaluate semisynchronous or Group Replication/InnoDB Cluster against the required RPO/RTO.
Why is connection pooling important?
flowchart LR
subgraph WITHOUT["Without Pooling"]
direction LR
APP1["App Server"]
DB1["Database<br/>many backend sessions<br/><i>higher memory/scheduling cost</i>"]
APP1 -->|"100 connections"| DB1
end
subgraph WITH["With Pooling (PgBouncer)"]
direction LR
APP2["App Server"]
POOL["PgBouncer<br/>pool"]
DB2["Database<br/>20 conns"]
APP2 -->|"100"| POOL
POOL -->|"20"| DB2
end# PgBouncer configuration
[databases]
mydb = host=localhost dbname=mydb
[pgbouncer]
pool_mode = transaction # or session, statement
max_client_conn = 1000
default_pool_size = 20
min_pool_size = 5
# Pool modes:
# session: Connection held until client disconnects
# transaction: Connection returned after each transaction (most common)
# statement: strongest restrictions; unsuitable for multi-statement transactionsPool size should follow database capacity and measured concurrency, not application instance count alone. Transaction pooling can break session-scoped assumptions such as prepared statements, temporary tables, advisory locks, and session settings unless the client/pooler combination explicitly supports them.
Performance Tuning Questions
These questions test your knowledge of database configuration and performance optimization.
What are the key configuration parameters for PostgreSQL tuning?
Do not memorize percentages as universal settings. Start with the workload, memory limit, storage latency/IOPS, connection concurrency, checkpoint behavior, replica/backup requirements, and observed plans.
shared_buffers: PostgreSQL's buffer cache; size together with the OS cache and container limit.work_mem: budget per sort/hash operation, potentially multiplied many times per query and session.maintenance_work_memandautovacuum_work_mem: maintenance budgets; consider concurrent workers.effective_cache_size: planner estimate, not allocated memory.max_connections: every backend has cost; use pooling where connection churn or concurrency is high.max_wal_size, checkpoint settings, WAL compression, andsynchronous_commit: balance recovery, write latency, durability, and I/O.- Planner costs and I/O concurrency: change only after measuring the actual storage and checking plan quality.
Use pg_stat_statements, EXPLAIN (ANALYZE, BUFFERS), wait events, WAL/checkpoint statistics, OS metrics, and restore/failover tests to validate changes.
What are the key configuration parameters for MySQL/InnoDB tuning?
The same rule applies: size from evidence, not “70–80% of RAM” copied from a checklist. Leave memory for connections, per-query buffers, Performance Schema, replication, the OS, and colocated services.
innodb_buffer_pool_size: main InnoDB cache; validate hit patterns and working-set size.- Redo capacity and
innodb_log_buffer_size: affect checkpoint pressure and large transactions. innodb_flush_log_at_trx_commitwithsync_binlog: define durability behavior for redo and binary logs.innodb_io_capacity/innodb_io_capacity_max: should reflect storage capability and flushing behavior.- Connection limits and thread handling: coordinate with application pools/proxies.
- Replica parallelism, binary-log format, and GTID settings: tune as part of a tested replication topology.
Use Performance Schema, the sys schema, slow query log, EXPLAIN ANALYZE, InnoDB metrics, and end-to-end load/recovery tests before and after each change.
When and how should you partition tables?
-- PostgreSQL declarative partitioning
CREATE TABLE orders (
id BIGSERIAL,
customer_id INT,
order_date DATE,
amount DECIMAL(10,2)
) PARTITION BY RANGE (order_date);
-- Create partitions
CREATE TABLE orders_2024_q1 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_2024_q2 PARTITION OF orders
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
-- Queries automatically route to correct partition
SELECT * FROM orders WHERE order_date = '2024-02-15';
-- Only scans orders_2024_q1
-- Partition pruning
EXPLAIN SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31';
-- Shows only orders_2024_q1 scannedPartition strategies:
- Range: Date, numeric ranges (most common)
- List: Discrete values (country, status)
- Hash: Even distribution (no natural range)
Partition only when pruning, retention, bulk loading, or maintenance isolation justifies the extra objects and operational complexity. It is not a generic substitute for indexes. Ensure every value has a target partition, automate future partition creation, and understand uniqueness/foreign-key constraints and planning overhead.
Why is VACUUM important in PostgreSQL?
-- VACUUM: Reclaims dead row space
VACUUM orders; -- Standard vacuum
VACUUM FULL orders; -- Rewrites table, locks it
VACUUM ANALYZE orders; -- Vacuum + update statistics
-- Check vacuum status
SELECT relname, n_dead_tup, last_vacuum, last_autovacuum
FROM pg_stat_user_tables;
-- ANALYZE: Updates query planner statistics
ANALYZE orders;
-- Auto-vacuum settings
-- autovacuum_vacuum_threshold = 50
-- autovacuum_vacuum_scale_factor = 0.2
-- Vacuum when: dead_tuples > threshold + scale_factor * table_size
-- For high-write tables, tune per-table
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_scale_factor = 0.02
);Treat the shown autovacuum values as an example, not a recommendation. Tune from table size, update rate, wraparound risk, vacuum duration, WAL/I/O impact, and observed dead tuples. VACUUM FULL takes an ACCESS EXCLUSIVE lock and rewrites the table; use it only with an explicit maintenance plan.
Quick Reference
| Topic | Key Points |
|---|---|
| PostgreSQL vs MySQL | Features, MVCC implementation, use cases |
| B-tree index | Ordered keys; composite leading columns and query shape matter |
| Index types | B-tree, Hash, GIN, GiST, BRIN - know when to use each |
| EXPLAIN ANALYZE | Executes the statement; compare estimates, loops, buffers, I/O and spills |
| Isolation levels | Engine-specific anomalies, locks and retry requirements |
| MVCC | Snapshot timing, tuple visibility, writer conflicts, VACUUM |
| Locking | Optimistic vs pessimistic, FOR UPDATE |
| JSONB | Operators (@>, ?), GIN indexing |
| Replication | Streaming, sync vs async, lag monitoring |
| Tuning | shared_buffers, work_mem, connection pooling |
Official Sources
- PostgreSQL versioning policy
- PostgreSQL 18 transaction isolation
- PostgreSQL 18 multicolumn indexes
- PostgreSQL 18 index-only scans
- PostgreSQL 18 JSON types
- PostgreSQL 18 routine vacuuming
- PostgreSQL 18
EXPLAIN - PostgreSQL 18 warm standby and streaming replication
- MySQL LTS and Innovation releases
- MySQL 8.4 JSON
- MySQL 8.4 InnoDB transaction model
- MySQL 8.4 replication implementation
Related Articles
- SQL Joins Interview Guide - JOIN fundamentals
- System Design Interview Guide - Database architecture patterns
- Hibernate & JPA Interview Guide - ORM integration
- Complete Java Backend Developer Interview Guide - Full Java backend preparation
Frequently Asked Questions
When should you choose PostgreSQL over MySQL?
Choose from measured workload needs, team expertise, managed-service constraints, extensions, migration cost, and failure operations. PostgreSQL is compelling for PostGIS, rich extension/type/index support, and its specific concurrency semantics. MySQL 8.4 LTS is a strong choice when its ecosystem, InnoDB behavior, Group Replication/InnoDB Cluster, or operational experience fits. Both support CTEs, window functions, JSON, full-text search, replication, and complex production workloads.
How do database indexes work and when should you create them?
B-tree indexes keep ordered keys and support equality, range, and compatible ordering. Create an index for important query shapes only after inspecting plans and workload frequency. Composite column order follows equality/range predicates, ordering, selectivity, and engine behavior—not a universal high-cardinality-first rule. Indexes cost storage, cache, write amplification, and maintenance; low-cardinality columns can still be useful in composite or partial indexes.
What are database isolation levels and which should you use?
SQL names four levels, but behavior is engine-specific. PostgreSQL maps READ UNCOMMITTED to READ COMMITTED, its default, and its REPEATABLE READ prevents phantoms but can still allow serialization anomalies. InnoDB defaults to REPEATABLE READ and combines consistent reads with locking reads and next-key locks. Choose from the invariants each transaction must preserve and implement retries for deadlocks or serialization failures; financial data does not automatically imply one level.
How does MVCC prevent locking in PostgreSQL?
PostgreSQL stores row versions with transaction visibility metadata so ordinary reads and writes usually do not block each other, though writers can block writers and explicit locks still matter. Snapshot timing depends on isolation: READ COMMITTED takes a new snapshot per statement, while REPEATABLE READ and SERIALIZABLE keep a transaction snapshot. VACUUM reclaims reusable space and prevents transaction-ID wraparound.
What is the difference between JSON and JSONB in PostgreSQL?
PostgreSQL json stores the original text, preserving whitespace, key order, and duplicate keys. jsonb stores a decomposed binary representation, does not preserve formatting/order, and retains only the last value for duplicate keys. jsonb supports rich operators and GIN indexing and is the usual choice for querying; json is appropriate only when preserving the exact input representation matters. Neither replaces relational constraints for stable, frequently joined attributes.
How do you identify and fix slow database queries?
Start with representative workload evidence, then use EXPLAIN and carefully use EXPLAIN ANALYZE because it executes the statement. Compare estimated and actual rows, loops, buffers, I/O, temp spills, and total time. A sequential scan or nested loop can be optimal. Large estimate errors may come from stale statistics, skew, or correlated columns. Fix the cause with schema/query changes, appropriate indexes, extended statistics, or configuration validated by measurement.
