Performance & Monitoring
Key Performance Metrics
| Metric | Target / Watch For |
|---|---|
| Query latency (p99) | < 100ms for OLTP; alert if rising |
| Queries per second (QPS) | Baseline + watch for spikes |
| Buffer pool hit rate | > 99% for InnoDB |
| Replication lag | < 1 second; alert if > 5 seconds |
| Active connections | Should be below max_connections |
| Lock wait time | Spikes indicate lock contention |
| Temp table to disk | Should be near 0; index/query issue |
| Disk I/O | High read I/O = cache miss; high write = heavy load |
| CPU usage | Sustained > 70% = investigate queries |
| Deadlocks/sec | Should be near 0 |
Slow Query Logging
MySQL
# my.cnf
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # log queries > 1 second
log_queries_not_using_indexes = ON
-- Runtime (no restart needed)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;
-- Analyze slow query log
-- Use: pt-query-digest (Percona Toolkit)
-- pt-query-digest /var/log/mysql/slow.log
PostgreSQL
# postgresql.conf
log_min_duration_statement = 500 # log queries > 500ms
log_statement = 'none' # don't log all (too noisy)
log_checkpoints = on
log_lock_waits = on
auto_explain.log_min_duration = 1000 # log explain plans for slow queries
-- pg_stat_statements: aggregated query stats
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT
LEFT(query, 80) AS query,
calls,
round(mean_exec_time::numeric, 2) AS avg_ms,
round(total_exec_time::numeric, 2) AS total_ms,
rows / calls AS avg_rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
EXPLAIN and EXPLAIN ANALYZE
MySQL EXPLAIN
EXPLAIN SELECT u.name, COUNT(o.id)
FROM users u LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id;
| Column | Meaning |
|---|---|
type | Join type: const > eq_ref > ref > range > index > ALL |
key | Index used (NULL = no index) |
rows | Estimated rows examined |
filtered | % of rows after WHERE filter |
Extra | Using index, Using filesort, Using temporary, Using where |
Red flags: type = ALL, Extra = Using filesort, Extra = Using temporary.
PostgreSQL EXPLAIN ANALYZE
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = 5;
Index Scan using idx_orders_user on orders (cost=0.43..12.46 rows=5 width=88)
(actual time=0.056..0.062 rows=3 loops=1)
Index Cond: (user_id = 5)
Buffers: shared hit=4
Planning Time: 0.2 ms
Execution Time: 0.1 ms
Key things to check:
Seq Scanon large table → missing index- Large difference between
cost rowsandactual rows→ stale statistics (ANALYZE) Buffers: shared read→ disk I/O (not in buffer cache)- Nested Loop with large inner rows → consider Hash Join
Connection Pooling
Opening a DB connection is expensive. Applications must use a connection pool to reuse connections.
For detailed guidelines on pool sizing formulas, parameter details, failure modes, and PgBouncer/RDS Proxy configuration, see the centralized Database Connection Pooling guide.
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
PgBouncer (PostgreSQL Connection Pooler)
Sits between app and PostgreSQL; reuses backend connections:
# pgbouncer.ini
[pgbouncer]
pool_mode = transaction # transaction-level pooling (most efficient)
max_client_conn = 10000 # clients connecting to PgBouncer
default_pool_size = 20 # connections to PostgreSQL per DB/user
| Pool Mode | Connection reused when |
|---|---|
session | Client disconnects |
transaction | Transaction ends (most efficient for stateless apps) |
statement | Each statement (cannot use multi-statement transactions) |
SET, prepared statements (without server_reset_query), and advisory locks. Spring apps must disable prepared statement caching.The 7 Rules for Writing High-Performance SQL & Slow Query Tuning
Rule 1: Find the Slow Query (Telemetry Over Guesswork)
Engineers frequently optimize the wrong queries based on subjective developer complaints. A 3-second query run once per month costs far fewer database resources than a 40ms unindexed query run 250,000 times per minute.
Database engines continuously track execution telemetry in shared memory buffers. Cumulative wall-clock impact = (Call Frequency) × (Mean Execution Latency). High cumulative time starves the Buffer Pool and hogs CPU execution threads.
-- ❌ Blindly guessing query bottlenecks without telemetry:
-- "Hey, I think the checkout page is slow, let me randomly add indexes on orders.created_at!"
-- Result: Added write amplification, bloated storage, zero p99 latency reduction.-- ✅ Querying DB-native telemetry for top cumulative latency consumers:
SELECT
round(total_exec_time::numeric, 2) AS total_ms,
calls,
round(mean_exec_time::numeric, 2) AS avg_ms,
round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 1) AS pct_total_load,
substr(query, 1, 90) AS query_snippet
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5;Optimizing slow queries is not about guessing which columns to index. High-performance database engineering follows a structured, telemetry-driven workflow designed to minimize random physical page I/O, eliminate redundant CPU sorts, and prevent execution plan regressions.
Rule 1: Find the Slow Query (Telemetry Over Guesswork)
Never rely on anecdotal developer complaints or isolated local benchmark runs. High-load production systems experience latency through cumulative system load, calculated as:
A single complex analytical query taking 3 seconds that runs once per night costs vastly less CPU and buffer pool capacity than a 35ms unindexed query executed 250,000 times per minute by an active API gateway.
Diagnostic Tooling Matrix by Database Engine
| Database Engine | Primary Telemetry Store | Key Metric / Query | Actionable Diagnosis |
|---|---|---|---|
| PostgreSQL | pg_stat_statements | SELECT query, calls, total_exec_time, mean_exec_time, shared_blks_read, shared_blks_hit FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10; | High shared_blks_read indicates buffer cache misses and heavy physical disk reads. |
| MySQL | sys.statement_analysis / Slow Query Log | SELECT query, exec_count, total_latency, avg_latency, rows_examined, rows_sent FROM sys.statement_analysis ORDER BY total_latency DESC LIMIT 10; | If rows_examined is orders of magnitude greater than rows_sent, queries are missing critical indexes. |
| Oracle | V$SQL, V$SQL_MONITOR, AWR & ASH | SELECT sql_id, executions, elapsed_time/1000000 AS total_sec, cpu_time, buffer_gets, disk_reads FROM v$sql ORDER BY elapsed_time DESC FETCH FIRST 10 ROWS ONLY; | Highlights queries responsible for the highest CPU consumption and physical disk reads. |
| SQL Server | Query Store / sys.dm_exec_query_stats | SELECT TOP 10 total_worker_time, execution_count, total_logical_reads, text FROM sys.dm_exec_query_stats CROSS APPLY sys.dm_exec_sql_text(sql_handle) ORDER BY total_worker_time DESC; | Pinpoints queries suffering plan regressions or excessive logical reads. |
Rule 2: Read the Execution Plan (EXPLAIN Before Changing Code)
Before writing any code or introducing an index, inspect the physical execution plan chosen by the Cost-Based Optimizer (CBO).
-- PostgreSQL: Always include BUFFERS to observe memory vs disk I/O
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT o.id, c.name, o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'COMPLETED' AND o.created_at >= '2025-01-01';
What to Look for in the Execution Plan:
- Access Paths (Scans):
Seq Scan/ALL(Table Scan): Scans every physical page on disk. Catastrophic on large tables.Index Scan: Traverses B-Tree index and performs random heap lookups for data pages.Index Only Scan/Using index: All projected columns reside in the index leaf pages. Zero heap page reads required!Bitmap Index/Heap Scan: Collects matching page offsets in a bitmap to convert random physical I/O into sequential page reads.
- Join Operators:
Nested Loop: Ideal when the outer table is very small and the inner table has an index lookup ().Hash Join: Builds an in-memory hash table of the smaller relation, then streams the larger relation through it (). Best for large, unsorted datasets.Merge Join: Reads two already-sorted input streams in lockstep (). Best when indexes provide natural ordering.
- Cardinality Estimation Errors:
- Compare
rows=X(Estimated) vsactual rows=Y. If the optimizer estimates 1 row but the actual execution returns 500,000 rows, optimizer statistics are stale! The planner mistakenly chooses aNested Loopinstead of aHash Join, resulting in severe CPU lockups. - Resolution: Run
ANALYZE tablename;(Postgres/SQLite) orOPTIMIZE TABLE tablename;(MySQL) to rebuild histogram statistics.
- Compare
- Buffer Metrics:
Buffers: shared hit=420(Data fetched from fast RAM Buffer Pool).Buffers: shared read=18500(Data read from physical NVMe/HDD storage — primary latency driver).
Rule 3: Use Appropriate Indexes (Selectivity, Order & Data Type Traps)
An index is only effective if the query predicate is SARGable (Search Argument Able) and respects the B+ Tree ordering.
Anti-Pattern A: Implicit Data Type Conversions
If the table column is VARCHAR(32) but you query using an integer literal, the database engine implicitly casts the column, invalidating index traversal:
-- ❌ Anti-pattern: account_number is VARCHAR, but 12345678 is INTEGER
SELECT balance FROM bank_accounts WHERE account_number = 12345678;
-- Under the hood, the engine rewrites this to:
-- WHERE CAST(account_number AS BIGINT) = 12345678
-- ❌ Result: Forces a 100% Sequential Table Scan across millions of rows!
-- ✅ Optimized: Explicitly match data types using string literals
SELECT balance FROM bank_accounts WHERE account_number = '12345678';
-- ✅ Result: Single B-Tree Index Scan (< 0.1ms)
Anti-Pattern B: Functions Wrapping Indexed Columns
-- ❌ Disables B-Tree index because the function must evaluate on every row:
WHERE DATE(created_at) = '2025-03-01'
-- ✅ SARGable: Retains raw column reference for logarithmic range search:
WHERE created_at >= '2025-03-01 00:00:00' AND created_at < '2025-03-02 00:00:00'
Anti-Pattern C: Composite Index Column Order & The Left-Prefix Rule
For a composite index on (tenant_id, status, created_at):
WHERE tenant_id = 5 AND status = 'ACTIVE'➔ ✅ Uses index on both columns.WHERE tenant_id = 5 AND created_at > '2025-01-01'➔ ⚠️ Uses index ontenant_id, then performs filtered index scan.WHERE status = 'ACTIVE'➔ ❌ Violates left-prefix rule; index cannot be used as an entry point.
Rule 4: Prefer UNION ALL Over UNION
A standard UNION operator executes an implicit DISTINCT across every single column in both combined result sets:
-- ❌ Anti-pattern: UNION forces complete result deduplication
SELECT user_id, email, 'CURRENT' AS status FROM active_subscribers
UNION
SELECT user_id, email, 'CHURNED' AS status FROM cancelled_subscribers;
Under the Hood:
- The database appends rows from both tables.
- It allocates a temporary memory buffer (
sort_buffer_sizein MySQL,work_memin PostgreSQL). - It performs a Hash Aggregate or Sort Unique (
Using filesortorSort Method: external merge Disk). - If the combined dataset exceeds available working memory, the database spills intermediate rows to temporary disk tablespace, causing massive disk I/O bottlenecks.
The Optimized Solution:
-- ✅ High Performance: Streams rows with zero sorting and O(1) memory overhead
SELECT user_id, email, 'CURRENT' AS status FROM active_subscribers
UNION ALL
SELECT user_id, email, 'CHURNED' AS status FROM cancelled_subscribers;
Senior Rule of Thumb: Unless you have confirmed duplicates across result streams and business logic strictly demands their removal, always default to
UNION ALL.
Rule 5: Avoid DISTINCT (Stop Masking Broken JOINs with Semi-Joins)
In production codebases, DISTINCT is frequently abused as a quick band-aid when a developer observes duplicate rows caused by an inadvertent one-to-many (1:N) join.
-- ❌ Anti-pattern: Using DISTINCT to collapse rows inflated by a 1:N join
SELECT DISTINCT c.id, c.name, c.email
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total_amount > 1000;
Why This Crushes Performance:
- If 10,000 customers have an average of 40 orders over $1,000, the
JOINexpands the working memory dataset to 400,000 intermediate rows! - The database engine then wastes significant CPU sorting or hashing all 400,000 rows just to discard 390,000 duplicates.
The Optimized Solution: Semi-Join with EXISTS
-- ✅ Semi-Join: Stops scanning child rows immediately upon first match
SELECT c.id, c.name, c.email
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id AND o.total_amount > 1000
);
- Under the Hood: The optimizer executes a
Hash Semi Joinor an index-driven semi-join. For each customer, as soon as the first qualifying order is found in the index, evaluation halts and the customer row is emitted immediately. Intermediate row count remains strictly at 10,000.
Rule 6: Structure with CTEs & Avoid Correlated Scalar Subqueries
Correlated scalar subqueries placed inside the SELECT clause execute in a nested loop — executing once for every single row returned by the outer query ( execution penalty):
-- ❌ Anti-pattern: Two correlated subqueries executed 50,000 times!
SELECT
e.id,
e.full_name,
(SELECT COUNT(*) FROM tasks t WHERE t.assigned_to = e.id AND t.completed = true) AS completed_tasks,
(SELECT AVG(rating) FROM reviews r WHERE r.employee_id = e.id) AS avg_rating
FROM employees e
WHERE e.department_id = 4;
-- 50,000 outer rows = 100,001 total index scans!
The Optimized Solution: Common Table Expressions (CTEs)
CTEs modularize complex calculations, permitting the database to aggregate child tables in a single pass:
-- ✅ Optimized: Pre-aggregated CTEs joined via hash joins in a single scan
WITH dept_tasks AS (
SELECT assigned_to, COUNT(*) AS completed_tasks
FROM tasks
WHERE completed = true
GROUP BY assigned_to
),
dept_reviews AS (
SELECT employee_id, AVG(rating) AS avg_rating
FROM reviews
GROUP BY employee_id
)
SELECT
e.id,
e.full_name,
COALESCE(dt.completed_tasks, 0) AS completed_tasks,
round(dr.avg_rating::numeric, 2) AS avg_rating
FROM employees e
LEFT JOIN dept_tasks dt ON e.id = dt.assigned_to
LEFT JOIN dept_reviews dr ON e.id = dr.employee_id
WHERE e.department_id = 4;
Engine Materialization Nuance:
- PostgreSQL 11 and older: Treated CTEs as strict optimization fences. Every CTE was evaluated once and materialized into a temporary memory/disk table, preventing predicate pushdown.
- PostgreSQL 12+: Inlines non-recursive, read-only CTEs by default (
AS NOT MATERIALIZED). If you want to force PG to calculate an expensive subquery once, useWITH cte AS MATERIALIZED (...). - MySQL 8.0+ & SQL Server: Treat CTEs as inline derived tables, allowing the optimizer to push down outer
WHEREclauses into the CTE.
Rule 7: Deploy Summary Tables & Materialized Views (Pre-Aggregation for OLAP)
When queries aggregate tens of millions of records (e.g. calculating financial metrics, user engagement stats, or analytics dashboards), even optimal indexing cannot bypass the physical hardware constraint of reading millions of pages from disk into memory.
Architectural Strategy:
Pre-compute and maintain aggregated data asynchronously, trading a controlled degree of real-time freshness for sub-millisecond query execution.
-- ✅ PostgreSQL Materialized View with concurrent refresh capability:
CREATE MATERIALIZED VIEW mv_daily_financial_summary AS
SELECT
merchant_id,
DATE_TRUNC('day', transaction_time) AS tx_date,
COUNT(*) AS total_tx,
SUM(amount) AS gross_revenue,
SUM(fee) AS net_fees
FROM transactions
GROUP BY merchant_id, DATE_TRUNC('day', transaction_time);
-- Unique index is mandatory for concurrent refresh:
CREATE UNIQUE INDEX idx_mv_daily_fin ON mv_daily_financial_summary(merchant_id, tx_date);
-- Dashboard queries now scan only ~365 rows per merchant instead of 50,000,000 rows:
SELECT * FROM mv_daily_financial_summary
WHERE merchant_id = 1042 AND tx_date >= '2025-01-01';
-- Refresh asynchronously via background worker or cron:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_financial_summary;
Implementation Strategies Across Systems:
- PostgreSQL:
MATERIALIZED VIEWwithREFRESH CONCURRENTLY(non-blocking for read queries). - Oracle: Materialized Views with
FAST REFRESH ON COMMITusing Materialized View Logs (MLOG$). - SQL Server: Indexed Views (
WITH SCHEMABINDING), where the clustered index is updated synchronously by the engine during DML. - MySQL / Aurora: Dedicated summary rollup tables updated via scheduled events, triggers, or CDC pipelines (e.g., Debezium ➔ Kafka ➔ Flink ➔ Summary Table).
Common Performance Anti-Patterns
N+1 Query Problem
// ❌ N+1: 1 query for users + N queries for orders
List<User> users = userRepo.findAll();
for (User u : users) {
List<Order> orders = orderRepo.findByUserId(u.getId()); // N queries!
}
// ✅ Single JOIN query
List<User> users = userRepo.findAllWithOrders();
// Repository: @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
SELECT *
-- ❌ Fetches all columns, prevents covering index
SELECT * FROM users WHERE active = true;
-- ✅ Fetch only needed columns
SELECT id, email, name FROM users WHERE active = true;
OFFSET Pagination on Large Tables
-- ❌ Scans and discards all rows before offset
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 50000;
-- ✅ Keyset pagination
SELECT * FROM orders WHERE created_at < :lastSeenDate
ORDER BY created_at DESC LIMIT 20;
Implicit Conversions
-- ❌ user_id is INT, comparison to VARCHAR triggers full scan
WHERE user_id = '100'
-- ❌ Function on indexed column prevents index use
WHERE DATE(created_at) = '2024-01-15'
-- ✅
WHERE created_at >= '2024-01-15' AND created_at < '2024-01-16'
Monitoring Tools
| Tool | Use Case |
|---|---|
| pg_stat_statements | PostgreSQL query stats aggregation |
| Percona Monitoring & Management (PMM) | MySQL/PostgreSQL dashboards |
| pt-query-digest | Analyze MySQL slow query logs |
| pgBadger | PostgreSQL log analyzer |
| Prometheus + Grafana | Custom metrics + visualization |
| Datadog / New Relic | APM with DB query tracing |
| p6spy | Java: log all JDBC queries with timing |
| datasource-proxy | Spring: intercept & log DataSource queries |
// p6spy configuration (Maven)
// Add p6spy dependency
// Create spy.properties:
// driverlist=com.mysql.cj.jdbc.Driver
// logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat
// appender=com.p6spy.engine.spy.appender.Slf4JLogger
// Change JDBC URL to: jdbc:p6spy:mysql://...
Interview Questions
Q1. How do you identify slow queries in production?
Enable slow query logs (MySQL:
slow_query_log; PostgreSQL:log_min_duration_statement). For aggregated stats, use PostgreSQL'spg_stat_statementsor MySQL's performance schema. APM tools like Datadog or New Relic trace queries at the application level. In Java/Spring, use p6spy or datasource-proxy.
Q2. What is the N+1 problem and how do you fix it?
N+1 occurs when loading N parent entities triggers N additional queries to load their children (one per parent). Fix: use JOIN FETCH in JPQL/HQL,
@EntityGraphin Spring Data, or@BatchSizefor batch loading. Always inspect Hibernate SQL logs during development.
Q3. Why is OFFSET pagination bad for large tables and what's the alternative?
OFFSET forces the DB to scan and discard all rows before the offset — O(offset + limit) work. At page 5000, you scan 100,000 rows. Keyset (cursor) pagination uses a WHERE clause on the last-seen value (
WHERE id > ?orWHERE created_at < ?) — always O(limit) regardless of page depth.
Q4. How do you size a connection pool?
A common formula:
pool_size = (core_count * 2) + spindle_count. For OLTP workloads, 10-20 connections per app instance is typical. Bigger pools don't always help — more concurrent queries mean more lock contention and context switching. Scale horizontally (more app nodes) rather than making one huge pool.
Q5. What does Using filesort mean in MySQL EXPLAIN and how do you fix it?
The DB cannot satisfy the ORDER BY using an index and must sort the result set in memory (or on disk for large sets). Fix: create an index that matches the ORDER BY column(s) and their direction. If combined with a WHERE clause, the composite index should cover both the filter and sort columns.
Q6. What is the difference between EXPLAIN and EXPLAIN ANALYZE?
EXPLAINshows the query plan with estimated costs and row counts based on statistics — no actual execution.EXPLAIN ANALYZEactually executes the query and shows both estimated and actual rows, time, and buffer usage. Use EXPLAIN ANALYZE on production only with care (it runs the query); use EXPLAIN in development first.
Q7. What metrics do you monitor for database health?
Key metrics: query latency (p50/p99), QPS, buffer pool hit rate (>99%), connections used vs max, replication lag, lock wait events, deadlock rate, disk IOPS, temp tables written to disk, and slow query count. Set alerts on p99 latency spikes, replication lag > threshold, and connections near max.
Q8. What is PgBouncer and why is it used?
PgBouncer is a connection pooler that sits between the application and PostgreSQL. PostgreSQL creates a backend process per connection (~5MB RAM each), so thousands of app connections are expensive. PgBouncer maintains a small pool of real DB connections and multiplexes thousands of client connections onto them. Transaction-mode pooling is the most efficient.
Q9. Why is UNION ALL significantly faster than UNION under the hood?
UNIONperforms an implicitDISTINCTacross every projected column in the combined dataset. Under the hood, this forces the database engine to construct an in-memory hash aggregate table or execute an external sort (Using filesortin MySQL,Sort Method: external merge Diskin PostgreSQL). If the row count exceedswork_mem/sort_buffer_size, data spills to temporary disk tablespace.UNION ALLsimply streams rows directly from each subquery without allocating sort buffers, deduplicating, or spilling to disk, achieving auxiliary memory and linear streaming latency.
Q10. Why is appending DISTINCT to a query following a JOIN considered an architectural anti-pattern, and what is the optimal fix?
Developers often use
DISTINCTas a cosmetic band-aid when joining a parent table to a child table in a one-to-many (1:N) relationship, causing Cartesian row explosion (e.g. 10,000 users joined with 500,000 orders produces 500,000 intermediate rows). The database then expends enormous CPU sorting or hashing all 500,000 rows just to collapse them back to 10,000. The correct fix is replacing theJOINwith a semi-join usingEXISTS:SELECT u.id, u.email FROM users uWHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'PAID');The semi-join short-circuits evaluation the moment the first matching child record is found, keeping intermediate row count strictly at 10,000 without sorting.
Q11. How does implicit data type casting cause an indexed query to degrade into a full table scan?
A B+ Tree index is ordered based on the exact binary representation of the indexed column's declared data type. When comparing an indexed
VARCHARcolumn against an integer literal (WHERE account_num = 123456), SQL type precedence rules dictate that strings be cast to numbers. The engine rewrites the predicate internally toWHERE CAST(account_num AS NUMERIC) = 123456. Because the function wraps the indexed column rather than the constant literal, the B-Tree cannot seek directly to leaf nodes and must evaluate the cast function against every single row via a Full Table Scan (non-SARGable predicate).
Q12. How do Common Table Expressions (CTEs) interact with query optimization fences across PostgreSQL versions and MySQL?
In PostgreSQL 11 and older, all CTEs acted as strict optimization fences: each CTE was evaluated independently and materialized into temporary storage, preventing the optimizer from pushing outer
WHEREclauses into the CTE. In PostgreSQL 12+, non-recursive, side-effect-free CTEs are automatically inlined (AS NOT MATERIALIZED) by default, enabling full predicate pushdown, though engineers can explicitly specifyAS MATERIALIZEDwhen they desire deliberate isolation. In MySQL 8.0+ and SQL Server, CTEs are always treated as derived inline views by the cost-based optimizer.
Q13. When should you choose summary tables or materialized views over query and index tuning?
Indexing accelerates point queries and selective range scans (). However, when analytical queries must aggregate over millions of records (e.g.
SUM(amount),COUNT(*)across 50,000,000 ledger rows for a dashboard), hardware constraints dictate that reading gigabytes of page blocks into RAM takes seconds regardless of indexes. Pre-aggregating into a summary table or a Materialized View (REFRESH MATERIALIZED VIEW CONCURRENTLY) trades write-time CPU and slight data latency for sub-millisecond read response times, reducing 50M rows scanned down to a few hundred pre-aggregated rows.
Advanced Editorial Pass: Database Observability and Performance Governance
Senior Engineering Focus
- Measure at plan, lock, and resource levels to explain latency movement.
- Correlate DB metrics with upstream traffic and deployment changes.
- Use SLO-driven tuning instead of ad-hoc query tweaks.
Failure Modes to Anticipate
- Alert fatigue from noisy, non-actionable metrics.
- Blind spots in lock/contention visibility.
- Tuning changes made without baseline and rollback plan.
Practical Heuristics
- Define golden signals for each datastore role.
- Keep plan regressions and slow-query trends under continuous review.
- Run periodic capacity and failure-injection exercises.
