← Back to Index

Chapter 7: Advanced Features

Indexes, views, transactions & stored procedures

1. Indexes

An index is a data structure that improves the speed of data retrieval on a table, similar to the index at the back of a book. Without an index, MySQL must scan every row (full table scan) to find matching records.

Key Point: Read-Write Tradeoff

Indexes speed up SELECT queries but slow down INSERT, UPDATE, and DELETE operations because the index must be maintained alongside the data. Only index columns that are frequently searched, filtered, or joined on.

Creating Indexes

-- Create an index on a single column
CREATE INDEX idx_customer_id ON orders (customer_id);

-- Create a unique index (enforces uniqueness)
CREATE UNIQUE INDEX idx_email ON users (email);

-- Create a composite (multi-column) index
CREATE INDEX idx_date_customer ON orders (order_date, customer_id);

-- Add index during table creation
CREATE TABLE employees (
    id         INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name  VARCHAR(50),
    dept_id    INT,
    hire_date  DATE,
    salary     DECIMAL(10,2),
    INDEX idx_dept (dept_id),
    INDEX idx_name (last_name, first_name),
    INDEX idx_hire (hire_date)
);

-- Drop an index
DROP INDEX idx_customer_id ON orders;

-- Show all indexes on a table
SHOW INDEX FROM orders;

Index Types

Type Engine Best For Notes
B-Tree InnoDB (default) Range queries, sorting, equality Most common; supports <, >, BETWEEN, LIKE 'abc%'
Hash Memory Exact equality lookups O(1) lookup; no range queries; InnoDB uses adaptive hash internally
Full-Text InnoDB, MyISAM Text search Supports natural language and boolean mode search
Spatial InnoDB, MyISAM Geographic data R-Tree based; for POINT, POLYGON, etc.

Composite Index and the Leftmost Prefix Rule

-- Given this composite index:
CREATE INDEX idx_abc ON orders (customer_id, order_date, product);

-- These queries CAN use the index:
SELECT * FROM orders WHERE customer_id = 1;                              -- uses (a)
SELECT * FROM orders WHERE customer_id = 1 AND order_date = '2024-01-15'; -- uses (a, b)
SELECT * FROM orders WHERE customer_id = 1 AND order_date > '2024-01-01'
                       AND product = 'Laptop';                            -- uses (a, b)

-- These queries CANNOT use the index:
SELECT * FROM orders WHERE order_date = '2024-01-15';    -- skips leftmost column
SELECT * FROM orders WHERE product = 'Laptop';            -- skips leftmost columns

Full-Text Indexes

-- Create a full-text index
CREATE TABLE articles (
    id      INT PRIMARY KEY AUTO_INCREMENT,
    title   VARCHAR(200),
    body    TEXT,
    FULLTEXT INDEX ft_content (title, body)
);

INSERT INTO articles (title, body) VALUES
('MySQL Performance Tuning', 'Learn how to optimize your MySQL queries with indexes and query analysis.'),
('Introduction to SQL', 'SQL is a standard language for managing relational databases.'),
('Database Design Best Practices', 'Proper normalization and index design lead to better database performance.');

-- Natural language mode search
SELECT *, MATCH(title, body) AGAINST('MySQL performance') AS relevance
FROM articles
WHERE MATCH(title, body) AGAINST('MySQL performance');

-- Boolean mode: + means must include, - means must exclude
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+MySQL -introduction' IN BOOLEAN MODE);

EXPLAIN: Analyzing Query Execution

-- EXPLAIN shows how MySQL executes a query
EXPLAIN SELECT * FROM orders WHERE customer_id = 1;

-- Key columns to check:
-- type:          ALL (full scan) → index → range → ref → eq_ref → const
-- possible_keys: indexes that could be used
-- key:           index actually chosen
-- rows:          estimated rows to examine
-- Extra:         Using index, Using filesort, Using temporary, etc.

-- EXPLAIN ANALYZE (MySQL 8.0.18+) runs the query and shows actual execution stats
EXPLAIN ANALYZE
SELECT customer_id, SUM(amount)
FROM orders
GROUP BY customer_id;

-- Use EXPLAIN to verify index usage
EXPLAIN SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31';

-- Common anti-patterns that prevent index usage:
-- 1. Function on indexed column
EXPLAIN SELECT * FROM orders WHERE YEAR(order_date) = 2024;     -- cannot use index
EXPLAIN SELECT * FROM orders WHERE order_date >= '2024-01-01'
                               AND order_date < '2025-01-01';    -- uses index

-- 2. Implicit type conversion
EXPLAIN SELECT * FROM orders WHERE customer_id = '1';  -- string vs int, may skip index

-- 3. Leading wildcard
EXPLAIN SELECT * FROM articles WHERE title LIKE '%MySQL%';  -- cannot use B-Tree index
EXPLAIN SELECT * FROM articles WHERE title LIKE 'MySQL%';   -- can use B-Tree index

2. Views

A view is a virtual table based on a SELECT query. It simplifies complex queries, enforces security, and provides a consistent interface even when underlying tables change.

Creating and Using Views

-- Create a simple view
CREATE VIEW v_order_summary AS
SELECT
    o.id AS order_id,
    o.customer_id,
    o.product,
    o.amount,
    o.quantity,
    o.order_date,
    o.amount * o.quantity AS total_value
FROM orders o;

-- Query the view just like a table
SELECT * FROM v_order_summary WHERE customer_id = 1;

-- Views can join multiple tables
CREATE VIEW v_sales_report AS
SELECT
    p.category,
    p.name AS product_name,
    p.price,
    s.quantity,
    s.sale_date,
    s.quantity * p.price AS revenue
FROM sales s
JOIN products p ON s.product_id = p.id;

-- Query the complex view easily
SELECT
    category,
    SUM(revenue) AS total_revenue
FROM v_sales_report
GROUP BY category;

Advantages of Views

Simplification

Hide complex joins and calculations behind a simple name.

Security

Expose only specific columns or filtered rows to users.

Abstraction

Change the underlying table structure without breaking application queries.

Reusability

Define common queries once, use them everywhere.

Updatable vs Read-Only Views

-- Updatable view: based on a single table, no aggregation
CREATE VIEW v_active_products AS
SELECT id, name, category, price
FROM products
WHERE price > 0;

-- You can INSERT, UPDATE, DELETE through an updatable view
UPDATE v_active_products SET price = 34.99 WHERE name = 'Wireless Mouse';

-- WITH CHECK OPTION prevents inserting/updating rows that fall outside the view's WHERE condition
CREATE VIEW v_expensive_products AS
SELECT id, name, category, price
FROM products
WHERE price >= 100
WITH CHECK OPTION;

-- This succeeds:
UPDATE v_expensive_products SET price = 150.00 WHERE id = 5;

-- This FAILS because $50 < $100 violates the view condition:
-- UPDATE v_expensive_products SET price = 50.00 WHERE id = 5;

-- Read-only views: contain JOIN, GROUP BY, DISTINCT, subqueries, or aggregate functions
-- Attempting INSERT/UPDATE/DELETE on these will produce an error

-- Replace an existing view
CREATE OR REPLACE VIEW v_order_summary AS
SELECT id, customer_id, product, amount, order_date
FROM orders
WHERE amount IS NOT NULL;

-- Drop a view
DROP VIEW IF EXISTS v_order_summary;

3. Transactions

A transaction is a sequence of SQL statements that execute as a single logical unit. Either all succeed, or all are rolled back. Transactions ensure data integrity in multi-step operations.

ACID Properties

Atomicity

All operations in the transaction succeed, or none do. No partial completion.

Consistency

The database moves from one valid state to another. Constraints are always satisfied.

Isolation

Concurrent transactions do not interfere with each other.

Durability

Once committed, changes survive system crashes (persisted to disk).

Basic Transaction Control

-- Start a transaction
START TRANSACTION;

-- Perform operations
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;

-- If everything is OK, commit
COMMIT;

-- If something goes wrong, rollback
-- ROLLBACK;

Example: Bank Transfer

-- Setup
CREATE TABLE accounts (
    id      INT PRIMARY KEY,
    name    VARCHAR(50) NOT NULL,
    balance DECIMAL(12,2) NOT NULL DEFAULT 0,
    CHECK (balance >= 0)
);

INSERT INTO accounts VALUES (1, 'Alice', 1000.00), (2, 'Bob', 500.00);

-- Safe transfer with error handling
DELIMITER //
CREATE PROCEDURE transfer_funds(
    IN from_id INT,
    IN to_id INT,
    IN amount DECIMAL(12,2)
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SELECT 'Transfer failed, rolled back' AS result;
    END;

    START TRANSACTION;

    -- Check sufficient funds
    IF (SELECT balance FROM accounts WHERE id = from_id) < amount THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient funds';
    END IF;

    -- Debit sender
    UPDATE accounts SET balance = balance - amount WHERE id = from_id;

    -- Credit receiver
    UPDATE accounts SET balance = balance + amount WHERE id = to_id;

    COMMIT;
    SELECT 'Transfer successful' AS result;
END //
DELIMITER ;

-- Execute the transfer
CALL transfer_funds(1, 2, 200.00);

-- Verify
SELECT * FROM accounts;

SAVEPOINT

START TRANSACTION;

INSERT INTO orders (customer_id, product, amount, quantity, order_date)
VALUES (1, 'Tablet', 399.99, 1, CURDATE());

SAVEPOINT before_bonus;

UPDATE accounts SET balance = balance + 50 WHERE id = 1;

-- Oops, roll back only the bonus, not the order
ROLLBACK TO SAVEPOINT before_bonus;

-- The order INSERT is still pending
COMMIT;

-- You can release a savepoint when no longer needed
-- RELEASE SAVEPOINT before_bonus;

Isolation Levels

Level Dirty Read Non-Repeatable Read Phantom Read
READ UNCOMMITTED Possible Possible Possible
READ COMMITTED Prevented Possible Possible
REPEATABLE READ Prevented Prevented Mostly prevented*
SERIALIZABLE Prevented Prevented Prevented

* MySQL InnoDB uses gap locks under REPEATABLE READ to also prevent most phantom reads.

-- Check current isolation level
SELECT @@transaction_isolation;

-- Set for current session
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Set for next transaction only
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- Set globally (requires SUPER privilege)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

MySQL vs PostgreSQL: Default Isolation Level

MySQL (InnoDB): Default is REPEATABLE READ. Uses MVCC with gap locking to prevent most phantom reads even at this level. This means you get near-SERIALIZABLE behavior without the performance penalty.

PostgreSQL: Default is READ COMMITTED. Uses MVCC with Serializable Snapshot Isolation (SSI) at the SERIALIZABLE level. PostgreSQL's REPEATABLE READ detects serialization conflicts and may abort transactions.

Practical tip: When migrating between MySQL and PostgreSQL, review your isolation assumptions. Code relying on MySQL's gap locks may behave differently under PostgreSQL's READ COMMITTED default.

4. Stored Procedures & Functions

Stored procedures and functions are precompiled SQL programs stored in the database. They encapsulate business logic, reduce network round-trips, and improve security.

Creating a Stored Procedure

-- Change delimiter to avoid conflicts with semicolons inside the procedure
DELIMITER //

CREATE PROCEDURE get_customer_orders(IN cust_id INT)
BEGIN
    SELECT
        o.id,
        o.product,
        o.amount,
        o.quantity,
        o.order_date
    FROM orders o
    WHERE o.customer_id = cust_id
    ORDER BY o.order_date DESC;
END //

DELIMITER ;

-- Call the procedure
CALL get_customer_orders(1);

Parameters: IN, OUT, INOUT

DELIMITER //

CREATE PROCEDURE get_order_stats(
    IN  cust_id      INT,
    OUT total_orders  INT,
    OUT total_amount  DECIMAL(12,2),
    OUT avg_amount    DECIMAL(12,2)
)
BEGIN
    SELECT
        COUNT(*),
        COALESCE(SUM(amount), 0),
        COALESCE(AVG(amount), 0)
    INTO total_orders, total_amount, avg_amount
    FROM orders
    WHERE customer_id = cust_id;
END //

DELIMITER ;

-- Call with OUT parameters
CALL get_order_stats(1, @orders, @total, @avg);
SELECT @orders AS total_orders, @total AS total_spent, @avg AS avg_order;

Variables and Control Flow

DELIMITER //

CREATE PROCEDURE classify_customers()
BEGIN
    DECLARE done INT DEFAULT FALSE;
    DECLARE cid INT;
    DECLARE spent DECIMAL(12,2);
    DECLARE tier VARCHAR(20);

    -- Cursor to iterate over customers
    DECLARE cur CURSOR FOR
        SELECT customer_id, SUM(amount)
        FROM orders
        GROUP BY customer_id;

    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

    -- Temp table for results
    CREATE TEMPORARY TABLE IF NOT EXISTS customer_tiers (
        customer_id INT,
        total_spent DECIMAL(12,2),
        tier        VARCHAR(20)
    );

    TRUNCATE TABLE customer_tiers;

    OPEN cur;

    read_loop: LOOP
        FETCH cur INTO cid, spent;
        IF done THEN
            LEAVE read_loop;
        END IF;

        -- IF/ELSEIF control flow
        IF spent >= 1000 THEN
            SET tier = 'Gold';
        ELSEIF spent >= 200 THEN
            SET tier = 'Silver';
        ELSE
            SET tier = 'Bronze';
        END IF;

        INSERT INTO customer_tiers VALUES (cid, spent, tier);
    END LOOP;

    CLOSE cur;

    SELECT * FROM customer_tiers ORDER BY total_spent DESC;
END //

DELIMITER ;

CALL classify_customers();

Stored Functions

DELIMITER //

CREATE FUNCTION calc_discount(
    price DECIMAL(10,2),
    quantity INT
) RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    DECLARE discount_rate DECIMAL(4,2);

    -- CASE expression for discount tiers
    CASE
        WHEN quantity >= 100 THEN SET discount_rate = 0.20;
        WHEN quantity >= 50  THEN SET discount_rate = 0.15;
        WHEN quantity >= 10  THEN SET discount_rate = 0.10;
        WHEN quantity >= 5   THEN SET discount_rate = 0.05;
        ELSE SET discount_rate = 0.00;
    END CASE;

    RETURN ROUND(price * quantity * (1 - discount_rate), 2);
END //

DELIMITER ;

-- Use the function in a SELECT
SELECT
    product,
    amount AS unit_price,
    quantity,
    calc_discount(amount, quantity) AS discounted_total
FROM orders
WHERE amount IS NOT NULL;

-- Drop procedure or function
DROP PROCEDURE IF EXISTS get_customer_orders;
DROP FUNCTION IF EXISTS calc_discount;

5. Triggers

Triggers automatically execute SQL statements in response to INSERT, UPDATE, or DELETE events on a table. They run BEFORE or AFTER the triggering event.

Example: Audit Logging

-- Create an audit log table
CREATE TABLE order_audit (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    order_id    INT NOT NULL,
    action      ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL,
    old_amount  DECIMAL(10,2),
    new_amount  DECIMAL(10,2),
    changed_by  VARCHAR(100),
    changed_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- AFTER INSERT trigger
DELIMITER //
CREATE TRIGGER trg_order_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
    INSERT INTO order_audit (order_id, action, new_amount, changed_by)
    VALUES (NEW.id, 'INSERT', NEW.amount, CURRENT_USER());
END //
DELIMITER ;

-- AFTER UPDATE trigger
DELIMITER //
CREATE TRIGGER trg_order_update
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
    IF OLD.amount != NEW.amount OR (OLD.amount IS NULL) != (NEW.amount IS NULL) THEN
        INSERT INTO order_audit (order_id, action, old_amount, new_amount, changed_by)
        VALUES (NEW.id, 'UPDATE', OLD.amount, NEW.amount, CURRENT_USER());
    END IF;
END //
DELIMITER ;

-- BEFORE DELETE trigger
DELIMITER //
CREATE TRIGGER trg_order_delete
BEFORE DELETE ON orders
FOR EACH ROW
BEGIN
    INSERT INTO order_audit (order_id, action, old_amount, changed_by)
    VALUES (OLD.id, 'DELETE', OLD.amount, CURRENT_USER());
END //
DELIMITER ;

Testing the Triggers

-- Insert a new order (fires trg_order_insert)
INSERT INTO orders (customer_id, product, amount, quantity, order_date)
VALUES (1, 'Tablet', 399.99, 1, '2024-04-20');

-- Update the order amount (fires trg_order_update)
UPDATE orders SET amount = 349.99 WHERE product = 'Tablet' AND customer_id = 1;

-- Delete the order (fires trg_order_delete)
DELETE FROM orders WHERE product = 'Tablet' AND customer_id = 1;

-- Check the audit log
SELECT * FROM order_audit ORDER BY changed_at DESC;

BEFORE Trigger for Data Validation

DELIMITER //

CREATE TRIGGER trg_validate_order
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
    -- Ensure quantity is positive
    IF NEW.quantity <= 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Quantity must be greater than zero';
    END IF;

    -- Auto-set order_date if not provided
    IF NEW.order_date IS NULL THEN
        SET NEW.order_date = CURDATE();
    END IF;
END //

DELIMITER ;

-- Managing triggers
SHOW TRIGGERS;
DROP TRIGGER IF EXISTS trg_validate_order;

6. Users & Permissions

MySQL's privilege system controls who can connect and what they can do. Follow the principle of least privilege: grant only the minimum permissions needed.

Creating Users

-- Create a user with password
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'SecurePass123!';

-- Allow connections from any host
CREATE USER 'remote_user'@'%' IDENTIFIED BY 'RemotePass456!';

-- Allow connections from a specific IP range
CREATE USER 'office_user'@'192.168.1.%' IDENTIFIED BY 'OfficePass789!';

-- Create user with password expiration
CREATE USER 'temp_user'@'localhost'
    IDENTIFIED BY 'TempPass!'
    PASSWORD EXPIRE INTERVAL 30 DAY;

-- View existing users
SELECT user, host FROM mysql.user;

Granting Privileges

-- Grant specific privileges on a database
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'app_user'@'localhost';

-- Grant all privileges on a specific table
GRANT ALL PRIVILEGES ON mydb.orders TO 'app_user'@'localhost';

-- Grant read-only access
GRANT SELECT ON mydb.* TO 'report_user'@'localhost';

-- Grant with column-level precision
GRANT SELECT (id, name, email), UPDATE (email) ON mydb.users TO 'app_user'@'localhost';

-- Grant privilege to create views
GRANT CREATE VIEW, SHOW VIEW ON mydb.* TO 'app_user'@'localhost';

-- Grant EXECUTE for stored procedures
GRANT EXECUTE ON PROCEDURE mydb.transfer_funds TO 'app_user'@'localhost';

-- Apply changes
FLUSH PRIVILEGES;

Revoking Privileges

-- Revoke specific privileges
REVOKE INSERT, UPDATE ON mydb.* FROM 'app_user'@'localhost';

-- Revoke all privileges
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app_user'@'localhost';

-- Check current grants for a user
SHOW GRANTS FOR 'app_user'@'localhost';

-- Check your own grants
SHOW GRANTS;

Roles (MySQL 8.0+)

-- Create roles
CREATE ROLE 'app_read', 'app_write', 'app_admin';

-- Grant privileges to roles
GRANT SELECT ON mydb.* TO 'app_read';
GRANT INSERT, UPDATE, DELETE ON mydb.* TO 'app_write';
GRANT ALL PRIVILEGES ON mydb.* TO 'app_admin';

-- Assign roles to users
GRANT 'app_read', 'app_write' TO 'app_user'@'localhost';
GRANT 'app_admin' TO 'admin_user'@'localhost';

-- Activate roles for the session
SET DEFAULT ROLE ALL TO 'app_user'@'localhost';

-- Revoke a role
REVOKE 'app_write' FROM 'app_user'@'localhost';

User Management

-- Change password
ALTER USER 'app_user'@'localhost' IDENTIFIED BY 'NewSecurePass!';

-- Lock/unlock an account
ALTER USER 'app_user'@'localhost' ACCOUNT LOCK;
ALTER USER 'app_user'@'localhost' ACCOUNT UNLOCK;

-- Set resource limits
ALTER USER 'app_user'@'localhost'
    WITH MAX_QUERIES_PER_HOUR 1000
         MAX_UPDATES_PER_HOUR 500
         MAX_CONNECTIONS_PER_HOUR 100;

-- Drop a user
DROP USER IF EXISTS 'temp_user'@'localhost';

7. Chapter Summary

Indexes

  • B-Tree (default), Hash, Full-Text, and Spatial types
  • Composite indexes follow the leftmost prefix rule
  • Use EXPLAIN to verify index usage
  • Avoid functions on indexed columns in WHERE

Views

  • Virtual tables built from SELECT queries
  • Simplify, secure, and abstract data access
  • Simple views are updatable; complex ones are read-only
  • WITH CHECK OPTION enforces view conditions on writes

Transactions

  • ACID guarantees data integrity
  • START TRANSACTION, COMMIT, ROLLBACK
  • SAVEPOINT for partial rollback
  • MySQL defaults to REPEATABLE READ isolation

Procedures, Triggers & Users

  • Stored procedures: reusable server-side logic
  • Functions: return a value, usable in SELECT
  • Triggers: auto-fire on INSERT/UPDATE/DELETE
  • Roles (MySQL 8.0+) simplify permission management