SQL Performance and Optimization Interview Questions

Execution plans, indexing, normalization, locking, partitioning and caching.

10 Questions
Detailed Answers

Filter by Difficulty

16
Intermediate

What is query execution plan and how do you read it?

Execution Plan 5 min read

An execution plan shows how the database will execute a query. It helps identify performance bottlenecks and optimization opportunities.

Getting Execution Plan:
-- MySQL
EXPLAIN SELECT * FROM employees WHERE department = 'Sales';

-- PostgreSQL
EXPLAIN ANALYZE SELECT * FROM employees WHERE department = 'Sales';

-- SQL Server
SET SHOWPLAN_TEXT ON;
GO
SELECT * FROM employees WHERE department = 'Sales';
GO
SET SHOWPLAN_TEXT OFF;
Sample EXPLAIN Output (MySQL):
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | employees | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 1000 |    10.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+

Key Things to Look For:

  • Type: ALL = full table scan (bad), INDEX = using index (good)
  • Possible Keys: Indexes that could be used
  • Key: Index actually being used
  • Rows: Estimated rows examined
  • Extra: Additional info (Using filesort, Using temporary = bad)
  • Look for full table scans (type=ALL) - may need indexes
  • Check if appropriate indexes are being used
  • Watch for filesort or temporary table usage

Optimization Tips from EXPLAIN:

  • If "Using filesort" appears, consider adding ORDER BY index
  • If "Using temporary" appears, query may need rewriting
  • If "type=ALL", add appropriate WHERE clause indexes
  • If rows examined is high but filtered is low, improve WHERE conditions
20
Advanced

What is database normalization and denormalization? When to use each?

Database Design 5 min read

Normalization organizes data to reduce redundancy, while denormalization intentionally adds redundancy to improve performance.

Normalized Design (3NF):
-- Separate tables for related data
CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

CREATE TABLE order_items (
    order_item_id INT PRIMARY KEY,
    order_id INT,
    product_id INT,
    quantity INT,
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);
Denormalized Design:
-- Combined table for faster reads
CREATE TABLE order_details (
    order_id INT,
    customer_name VARCHAR(100),
    customer_email VARCHAR(100),
    order_date DATE,
    product_name VARCHAR(100),
    quantity INT,
    unit_price DECIMAL(10,2),
    total_price DECIMAL(10,2)  -- Redundant but pre-calculated
);

When to Normalize:

  • OLTP systems (Online Transaction Processing)
  • When data integrity is critical
  • When write performance is important
  • When storage space is limited
  • When data relationships are complex

When to Denormalize:

  • OLAP systems (Online Analytical Processing)
  • Data warehouses and reporting databases
  • When read performance is critical
  • For frequently joined tables
  • For summary/reporting tables
  • When joins are too expensive

Common Denormalization Techniques:

  • Adding calculated columns (total_price = quantity * unit_price)
  • Flattening hierarchies into single table
  • Creating summary tables with pre-aggregated data
  • Adding redundant foreign key data
  • Creating materialized views
  • Using NoSQL databases for specific use cases
24
Advanced

What is database locking and what are different lock types?

Concurrency 5 min read

Database locking prevents multiple transactions from modifying the same data simultaneously, ensuring data consistency.

Types of Locks:

  • Shared Lock (Read Lock): Multiple transactions can read, but none can write
  • Exclusive Lock (Write Lock): Only one transaction can read/write
  • Intent Lock: Indicates intention to lock at a finer granularity
  • Schema Lock: Lock on table structure (DDL operations)
  • Update Lock: Intermediate lock before upgrading to exclusive
Locking Examples:
-- SQL Server explicit locking
BEGIN TRANSACTION;
-- Get exclusive lock on table
SELECT * FROM employees WITH (TABLOCKX);
-- Or row-level lock
SELECT * FROM employees WITH (ROWLOCK, XLOCK) WHERE id = 1;
COMMIT;

-- MySQL locking
START TRANSACTION;
-- Lock tables explicitly
LOCK TABLES employees WRITE, departments READ;
-- Perform operations
UPDATE employees SET salary = salary * 1.1 WHERE department = 'Engineering';
-- Release locks
UNLOCK TABLES;
COMMIT;

-- PostgreSQL advisory locks
BEGIN;
SELECT pg_advisory_lock(123);  -- Application-level lock
-- Critical section
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
SELECT pg_advisory_unlock(123);
COMMIT;

Lock Granularity:

  • Row-level locks: Most granular, best concurrency
  • Page-level locks: Locks database pages (8KB chunks)
  • Table-level locks: Locks entire table, poor concurrency
  • Database-level locks: Locks entire database

Common Locking Issues:

  • Deadlock: Two transactions waiting for each other
  • Blocking: One transaction blocking others
  • Lock escalation: Many row locks converted to table lock
  • Lock timeout: Transaction waiting too long for lock
  • Solution: Keep transactions short, access tables in same order, use appropriate isolation levels
28
Advanced

What is query caching and how does it work?

Performance 4 min read

Query caching stores the results of SQL queries so subsequent identical queries can be served faster without re-execution.

How Query Caching Works:

  • Database parses and optimizes query
  • Checks cache for identical query (exact text match)
  • If cached, returns result immediately
  • If not cached, executes query and stores result in cache
  • Cache invalidated when underlying data changes
MySQL Query Cache Configuration:
-- Check query cache status
SHOW VARIABLES LIKE 'query_cache%';

-- Typical configuration in my.cnf
[mysqld]
query_cache_type = 1              -- Enable cache
query_cache_size = 64M           -- Cache size
query_cache_limit = 2M           -- Max result size to cache
query_cache_min_res_unit = 4K    -- Minimum allocation unit

-- Check cache performance
SHOW STATUS LIKE 'Qcache%';

-- Force query to use cache (MySQL 8+ removed query cache)
SELECT SQL_CACHE * FROM employees WHERE department = 'Sales';

-- Force query to NOT use cache
SELECT SQL_NO_CACHE * FROM employees WHERE department = 'Sales';
PostgreSQL (no built-in query cache):
-- PostgreSQL relies on shared buffers and OS cache
-- Check buffer cache hit ratio
SELECT 
    sum(blks_hit) * 100.0 / (sum(blks_hit) + sum(blks_read)) as cache_hit_ratio
FROM pg_stat_database;

-- Use pg_prewarm to load data into cache
CREATE EXTENSION pg_prewarm;
SELECT pg_prewarm('employees');

-- Materialized views as manual cache
CREATE MATERIALIZED VIEW mv_employee_stats AS
SELECT department, COUNT(*), AVG(salary)
FROM employees
GROUP BY department;

-- Refresh cache periodically
REFRESH MATERIALIZED VIEW mv_employee_stats;

When Query Caching is Effective:

  • Read-heavy applications with repetitive queries
  • Queries with complex joins or aggregations
  • Data that changes infrequently
  • Small to medium result sets
  • Applications with many identical queries

When Query Caching is Ineffective:

  • Write-heavy applications (frequent cache invalidation)
  • Queries with NOW(), RANDOM(), CURRENT_TIMESTAMP
  • Large result sets that consume cache memory
  • Highly dynamic data
  • Queries with user-specific parameters
31
Advanced

What is database partitioning and what are its benefits?

Partitioning 5 min read

Partitioning divides a large table into smaller, more manageable pieces while maintaining logical unity. Each partition can be stored and accessed separately.

Range Partitioning Example (PostgreSQL):
-- Create partitioned table
CREATE TABLE sales (
    sale_id SERIAL,
    sale_date DATE NOT NULL,
    amount DECIMAL(10,2),
    region VARCHAR(50)
) PARTITION BY RANGE (sale_date);

-- Create partitions for each year
CREATE TABLE sales_2023 PARTITION OF sales
    FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

CREATE TABLE sales_2024 PARTITION OF sales
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE sales_2025 PARTITION OF sales
    FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

-- Create default partition
CREATE TABLE sales_default PARTITION OF sales DEFAULT;
List Partitioning Example (MySQL):
-- Partition by region
CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    name VARCHAR(100),
    region VARCHAR(50),
    email VARCHAR(100)
)
PARTITION BY LIST COLUMNS(region) (
    PARTITION p_north VALUES IN ('NY', 'NJ', 'CT'),
    PARTITION p_south VALUES IN ('TX', 'FL', 'GA'),
    PARTITION p_west VALUES IN ('CA', 'OR', 'WA'),
    PARTITION p_other VALUES IN (DEFAULT)
);

Partitioning Strategies:

  • Range Partitioning: Based on value ranges (dates, numbers)
  • List Partitioning: Based on discrete values (regions, status)
  • Hash Partitioning: Based on hash function (even distribution)
  • Composite Partitioning: Combination of methods

Benefits:

  • Improved Performance: Query only relevant partitions
  • Easier Maintenance: Manage partitions independently
  • Better Availability: Failure affects only one partition
  • Efficient Archiving: Drop/archive old partitions easily
  • Parallel Processing: Partitions can be processed in parallel
35
Advanced

What are covering indexes and how do they improve performance?

Indexing 4 min read

A covering index includes all columns needed for a query, allowing the database to satisfy the query entirely from the index without accessing the table.

Creating Covering Indexes:
-- Query needing multiple columns
SELECT employee_id, name, department, salary
FROM employees
WHERE department = 'Engineering'
AND hire_date > '2023-01-01';

-- Non-covering index (requires table lookup)
CREATE INDEX idx_dept_hire ON employees(department, hire_date);

-- Covering index (includes all needed columns)
CREATE INDEX idx_covering ON employees(department, hire_date)
INCLUDE (employee_id, name, salary);

-- Or include columns in index key
CREATE INDEX idx_covering_complete ON employees(
    department, 
    hire_date, 
    employee_id, 
    name, 
    salary
);
How It Works:
-- Without covering index:
1. Use idx_dept_hire to find matching rows
2. For each match, read table (heap/clustered index) to get employee_id, name, salary
3. Return results

-- With covering index:
1. Use idx_covering to find matching rows
2. All needed columns are in index
3. Return results directly from index
4. No table access needed!

Benefits:

  • Eliminates table lookups: Biggest performance gain
  • Reduces I/O: Index only vs index + table
  • Improves cache efficiency: More rows fit in memory
  • Faster queries: Especially for range scans

When to Use Covering Indexes:

  • Frequently executed queries with specific column sets
  • Queries that return many rows (reduces many table lookups)
  • Reporting queries with aggregations
  • When table is large and scattered (high fragmentation)
  • For read-heavy tables where writes are infrequent

Trade-offs:

  • Larger index size (more columns = more storage)
  • Slower INSERT/UPDATE/DELETE (more indexes to update)
  • Increased maintenance overhead
  • May not help if query patterns change
38
Advanced

What is database connection pooling and its benefits?

Connection Pooling 4 min read

Connection pooling maintains a cache of database connections that can be reused by multiple applications, reducing overhead of establishing new connections.

Connection Pool Configuration (Java - HikariCP):
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(20);       // Max connections in pool
config.setMinimumIdle(10);           // Minimum idle connections
config.setConnectionTimeout(30000);  // 30 seconds timeout
config.setIdleTimeout(600000);       // 10 minutes idle timeout
config.setMaxLifetime(1800000);      // 30 minutes max lifetime
config.setConnectionTestQuery("SELECT 1");

HikariDataSource dataSource = new HikariDataSource(config);
Python (psycopg2 with pooling):
import psycopg2
from psycopg2 import pool

# Create connection pool
connection_pool = psycopg2.pool.SimpleConnectionPool(
    5, 20,  # minconn, maxconn
    host="localhost",
    database="mydb",
    user="user",
    password="password",
    port=5432
)

# Get connection from pool
conn = connection_pool.getconn()
try:
    cur = conn.cursor()
    cur.execute("SELECT * FROM employees")
    results = cur.fetchall()
finally:
    # Return connection to pool (don't close!)
    connection_pool.putconn(conn)

Benefits:

  • Reduced Latency: Reuse existing connections (no TCP handshake)
  • Resource Efficiency: Limits concurrent connections to database
  • Improved Performance: Avoids connection establishment overhead
  • Better Scalability: Handles connection spikes gracefully
  • Connection Management: Automatic validation and cleanup

Common Pool Parameters:

  • maxPoolSize: Maximum connections in pool
  • minPoolSize: Minimum connections kept ready
  • connectionTimeout: Max wait time for connection
  • idleTimeout: How long idle connections stay in pool
  • maxLifetime: Total lifetime of a connection
  • validationQuery: Test query to validate connection

When to Use Connection Pooling:

  • Web applications with many concurrent users
  • Microservices architecture
  • Applications with frequent database calls
  • When database connection limits are low
  • To prevent connection leaks and timeouts
42
Advanced

What is database connection multiplexing and its benefits?

Connection Management 4 min read

Connection multiplexing allows multiple logical client connections to share a single physical database connection, reducing connection overhead.

Traditional vs Multiplexed Connections:
-- Traditional: Each client has dedicated connection
Client1 → Connection1 → Database
Client2 → Connection2 → Database
Client3 → Connection3 → Database
-- 3 physical connections, each with memory/CPU overhead

-- Multiplexed: Multiple clients share connections
Client1 → \
Client2 → → Multiplexer → Single Connection → Database
Client3 → /
-- 3 logical connections, 1 physical connection
How Multiplexing Works:
// Pseudo-code for connection multiplexer
class ConnectionMultiplexer {
    private ConnectionPool physicalPool;
    private Map virtualConnections;
    
    executeQuery(clientId, sql) {
        // Get or create virtual connection for client
        VirtualConnection vconn = virtualConnections.get(clientId);
        if (vconn == null) {
            vconn = new VirtualConnection(clientId);
            virtualConnections.put(clientId, vconn);
        }
        
        // Borrow physical connection from pool
        PhysicalConnection pconn = physicalPool.borrow();
        try {
            // Execute on physical connection
            ResultSet rs = pconn.execute(sql);
            
            // Map results back to virtual connection
            return vconn.processResults(rs);
        } finally {
            // Return physical connection to pool
            physicalPool.return(pconn);
        }
    }
}

Benefits:

  • Reduced Connection Count: Fewer physical connections to database
  • Lower Memory Usage: Each connection consumes 1-10MB
  • Better Scalability: Support more clients with same resources
  • Connection Pool Efficiency: Better utilization of pooled connections
  • Reduced Latency: Reuse established connections

Challenges:

  • Session State: Temporary tables, session variables may conflict
  • Transactions: Need to manage transaction isolation
  • Error Handling: Errors from one client shouldn't affect others
  • Load Balancing: Need to distribute queries evenly
  • Complexity: More complex than simple connection pooling

When to Use:

  • Microservices with many instances
  • Serverless functions (Lambda) with database access
  • Applications hitting database connection limits
  • Read-heavy workloads with short queries
  • When database memory is constrained
45
Advanced

What is query plan caching and parameter sniffing problem?

Query Optimization 5 min read

Parameter sniffing occurs when SQL Server creates an execution plan optimized for the first parameter values, which may be suboptimal for subsequent calls with different values.

Parameter Sniffing Example:
-- Stored procedure with parameter
CREATE PROCEDURE GetOrdersByStatus
    @status VARCHAR(20)
AS
BEGIN
    SELECT * FROM orders
    WHERE order_status = @status
    ORDER BY order_date DESC;
END;

-- First execution (with 'Completed' - few rows)
EXEC GetOrdersByStatus @status = 'Completed';
-- Creates plan optimized for few rows (uses index seek)

-- Second execution (with 'Pending' - many rows)
EXEC GetOrdersByStatus @status = 'Pending';
-- Uses same plan (seek), but table scan would be better
-- Result: Poor performance for 'Pending' status
Solutions to Parameter Sniffing:
-- 1. Use OPTION (RECOMPILE) - recompile each time
CREATE PROCEDURE GetOrdersByStatus
    @status VARCHAR(20)
AS
BEGIN
    SELECT * FROM orders
    WHERE order_status = @status
    ORDER BY order_date DESC
    OPTION (RECOMPILE);  -- New plan each execution
END;

-- 2. Use OPTIMIZE FOR UNKNOWN
CREATE PROCEDURE GetOrdersByStatus
    @status VARCHAR(20)
AS
BEGIN
    SELECT * FROM orders
    WHERE order_status = @status
    ORDER BY order_date DESC
    OPTION (OPTIMIZE FOR UNKNOWN);  -- Use average statistics
END;

-- 3. Use OPTIMIZE FOR specific value
CREATE PROCEDURE GetOrdersByStatus
    @status VARCHAR(20)
AS
BEGIN
    SELECT * FROM orders
    WHERE order_status = @status
    ORDER BY order_date DESC
    OPTION (OPTIMIZE FOR (@status = 'Pending'));  -- Optimize for common case
END;

-- 4. Use local variables (disables sniffing)
CREATE PROCEDURE GetOrdersByStatus
    @status VARCHAR(20)
AS
BEGIN
    DECLARE @local_status VARCHAR(20) = @status;
    
    SELECT * FROM orders
    WHERE order_status = @local_status
    ORDER BY order_date DESC;
END;

How Query Plan Caching Works:

  • SQL Server compiles query and creates execution plan
  • Plan cached in memory (plan cache)
  • Subsequent identical queries reuse cached plan
  • Parameter values sniffed during first compilation
  • Plan optimized for those specific parameter values

When Parameter Sniffing Helps vs Hurts:

  • Helps: When parameter distribution is uniform
  • Hurts: When data distribution varies widely
  • Example: Status='Deleted' (few rows) vs Status='Active' (many rows)
  • Solution: Monitor performance, use appropriate mitigation
49
Advanced

What is database connection failover and high availability?

High Availability 5 min read

Failover automatically switches to a standby database when the primary fails, ensuring high availability and minimal downtime.

Database Mirroring (SQL Server):
-- Configure database mirroring
ALTER DATABASE MyDB SET PARTNER = 'TCP://primary-server:5022';
ALTER DATABASE MyDB SET WITNESS = 'TCP://witness-server:5023';

-- On secondary server
ALTER DATABASE MyDB SET PARTNER = 'TCP://secondary-server:5022';

-- Check mirroring status
SELECT 
    database_name,
    mirroring_state_desc,
    mirroring_role_desc,
    mirroring_safety_level_desc
FROM sys.database_mirroring
WHERE mirroring_state IS NOT NULL;

-- Manual failover
ALTER DATABASE MyDB SET PARTNER FAILOVER;
PostgreSQL Streaming Replication:
-- Primary server configuration (postgresql.conf)
wal_level = replica
max_wal_senders = 10
wal_keep_size = 1GB

-- Primary pg_hba.conf
host replication repuser secondary-ip/32 md5

-- Secondary setup
# Create base backup
pg_basebackup -h primary-host -D /var/lib/postgresql/data -U repuser -v -P

-- Secondary recovery.conf (PostgreSQL 12+ uses postgresql.conf)
primary_conninfo = 'host=primary-host user=repuser password=secret'
promote_trigger_file = '/tmp/promote_to_primary'

-- Check replication status on primary
SELECT * FROM pg_stat_replication;

-- Promote secondary to primary
# On secondary server
touch /tmp/promote_to_primary
# Or: pg_ctl promote -D /var/lib/postgresql/data

High Availability Patterns:

  • Active-Passive: Primary handles traffic, standby ready
  • Active-Active: Both handle traffic, load balanced
  • Multi-Master: All nodes accept writes, complex sync
  • Read Replicas: Scale reads, async replication

Failover Strategies:

  • Automatic Failover: Detects failure, switches automatically
  • Manual Failover: Admin initiates switch
  • Planned Failover: For maintenance, zero data loss
  • Forced Failover: When primary unreachable, possible data loss

Connection String with Failover:

-- JDBC connection string with failover
jdbc:postgresql://primary:5432,secondary:5432/mydb
?targetServerType=primary
&loadBalanceHosts=true
&hostRecheckSeconds=10

-- Application logic
try {
    // Connect to primary
    executeQuery();
} catch (SQLException e) {
    // Connection failed, retry with secondary
    updateConnectionString();
    executeQuery();
}

Browse other topics