Creating and Managing Indexes
Now that you understand how indexes work, it's time to learn the practical skills of creating, modifying, and managing indexes in MySQL. In this lesson, we'll cover various syntax options, best practices, and advanced techniques.
CREATE INDEX Syntax
There are multiple ways to create indexes in MySQL. Let's explore each method:
Method 1: CREATE INDEX Statement
-- Single column index
CREATE INDEX idx_email ON users(email);
-- Multi-column (composite) index
CREATE INDEX idx_name ON users(first_name, last_name);
-- Unique index
CREATE UNIQUE INDEX idx_username ON users(username);
Method 2: ALTER TABLE Statement
-- Add single index
ALTER TABLE users ADD INDEX idx_country (country);
-- Add composite index
ALTER TABLE users ADD INDEX idx_location (country, city, zip_code);
-- Add unique index
ALTER TABLE users ADD UNIQUE KEY idx_email (email);
-- Add full-text index
ALTER TABLE articles ADD FULLTEXT idx_content (title, content);
Method 3: CREATE TABLE with Index Definition
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE,
email VARCHAR(255),
country VARCHAR(50),
city VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Define indexes during table creation
INDEX idx_email (email),
INDEX idx_location (country, city),
UNIQUE KEY idx_username (username)
);
Best Practice: Use CREATE INDEX for existing tables and include index definitions in CREATE TABLE for new tables. This makes your table structure self-documenting.
Index Naming Conventions
Following consistent naming conventions makes index management easier:
Recommended Naming Patterns:
-- Single column indexes
idx_{column_name}
Example: idx_email, idx_country
-- Composite indexes
idx_{column1}_{column2}
Example: idx_country_city, idx_category_published
-- Unique indexes
uk_{column_name} or uniq_{column_name}
Example: uk_email, uniq_username
-- Full-text indexes
ft_{column_name} or fulltext_{column_name}
Example: ft_content, fulltext_title_body
-- Foreign key indexes
fk_{table}_{column}
Example: fk_orders_user_id
Why Naming Matters: Good names help you understand what each index does without looking at the table structure. This is especially important in large databases with hundreds of indexes.
Composite Index Column Order
The order of columns in a composite index is critical for performance. Consider these guidelines:
-- Scenario: You have these common queries
SELECT * FROM orders WHERE customer_id = 123;
SELECT * FROM orders WHERE customer_id = 123 AND status = 'pending';
SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC;
-- GOOD: customer_id first (most selective and used in all queries)
CREATE INDEX idx_customer_status_date
ON orders(customer_id, status, created_at);
-- This index can be used for:
✓ WHERE customer_id = 123
✓ WHERE customer_id = 123 AND status = 'pending'
✓ WHERE customer_id = 123 AND status = 'pending' ORDER BY created_at
✓ WHERE customer_id = 123 ORDER BY created_at (partial use)
-- BAD: Wrong column order
CREATE INDEX idx_bad ON orders(status, created_at, customer_id);
-- Can't efficiently use this for WHERE customer_id = 123
Column Order Rules:
1. Equality First: Columns used with = or IN
2. Range Second: Columns used with <, >, BETWEEN
3. Sorting Last: Columns used in ORDER BY
Example:
WHERE country = 'USA' -- Equality (first)
AND age > 18 -- Range (second)
ORDER BY created_at DESC -- Sorting (last)
Optimal index:
CREATE INDEX idx_optimal ON users(country, age, created_at);
Covering Indexes
A covering index contains all columns needed by a query, eliminating the need to access the table data:
-- Query that needs id, email, and country
SELECT id, email, country FROM users WHERE email = 'john@example.com';
-- Regular index on email
CREATE INDEX idx_email ON users(email);
-- MySQL: Find row in index → Access table to get id and country
-- Covering index includes all needed columns
CREATE INDEX idx_email_covering ON users(email, id, country);
-- MySQL: Find row in index → All data in index (no table access needed!)
Result: Faster query (no table lookup required)
Check if your query uses a covering index with EXPLAIN:
EXPLAIN SELECT id, email, country
FROM users
WHERE email = 'john@example.com';
+----+------+-------------------+-------+-------------+
| id | type | key | Extra |
+----+------+-------------------+-------+-------------+
| 1 | ref | idx_email_covering| Using index |
+----+------+-------------------+-------+-------------+
"Using index" = Covering index is being used! ✓
Tip: Covering indexes are extremely powerful for read-heavy applications, but be careful not to make them too large. Balance coverage with index size.
Invisible Indexes (MySQL 8.0+)
MySQL 8.0 introduced invisible indexes - indexes that exist but are not used by the query optimizer:
-- Create invisible index
CREATE INDEX idx_email ON users(email) INVISIBLE;
-- Make existing index invisible
ALTER TABLE users ALTER INDEX idx_email INVISIBLE;
-- Make invisible index visible again
ALTER TABLE users ALTER INDEX idx_email VISIBLE;
Why Use Invisible Indexes?
Use Case 1: Testing Index Removal
-- Before dropping an index, make it invisible to test impact
ALTER TABLE users ALTER INDEX idx_country INVISIBLE;
-- Monitor application performance for 24 hours
-- If no issues, drop the index safely
DROP INDEX idx_country ON users;
Use Case 2: Conditional Index Usage
-- Create index for future use but don't use it yet
CREATE INDEX idx_future ON users(new_column) INVISIBLE;
-- Enable when needed
ALTER TABLE users ALTER INDEX idx_future VISIBLE;
Dropping Indexes
Remove indexes that are no longer needed to reduce overhead:
-- Method 1: DROP INDEX
DROP INDEX idx_country ON users;
-- Method 2: ALTER TABLE
ALTER TABLE users DROP INDEX idx_country;
-- Drop multiple indexes at once
ALTER TABLE users
DROP INDEX idx_country,
DROP INDEX idx_city,
DROP INDEX idx_zip;
Warning: You cannot drop a PRIMARY KEY using DROP INDEX. Use ALTER TABLE to drop and recreate primary keys:
ALTER TABLE users DROP PRIMARY KEY;
ALTER TABLE users ADD PRIMARY KEY (id);
Modifying Existing Indexes
MySQL doesn't support direct index modification. You must drop and recreate:
-- Want to change idx_name from (first_name) to (first_name, last_name)
-- Step 1: Drop old index
DROP INDEX idx_name ON users;
-- Step 2: Create new index
CREATE INDEX idx_name ON users(first_name, last_name);
-- Or do both in one statement
ALTER TABLE users
DROP INDEX idx_name,
ADD INDEX idx_name (first_name, last_name);
Index Length Prefix
For VARCHAR and TEXT columns, you can index only the first N characters to save space:
-- Index first 10 characters of email
CREATE INDEX idx_email ON users(email(10));
-- Useful for long TEXT columns
CREATE INDEX idx_description ON products(description(50));
-- Composite index with prefix
CREATE INDEX idx_name ON users(first_name(5), last_name(10));
When to use prefix indexes:
✓ Use prefix when:
- Column is very long (TEXT, BLOB)
- First N characters are unique enough
- Index size is a concern
✗ Avoid prefix when:
- You need exact matching
- You use ORDER BY on the column (prefix indexes can't be used for sorting)
- Column is already short (VARCHAR(50) or less)
Real-World Index Management Scenario
Let's optimize a real e-commerce orders table:
-- Original table (no indexes except PRIMARY KEY)
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
customer_id INT NOT NULL,
status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled'),
total_amount DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Common queries:
-- Q1: Find customer's orders
SELECT * FROM orders WHERE customer_id = 123;
-- Q2: Find pending orders
SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at;
-- Q3: Find customer's recent orders
SELECT * FROM orders WHERE customer_id = 123 ORDER BY created_at DESC LIMIT 10;
-- Q4: Admin dashboard - today's revenue
SELECT SUM(total_amount) FROM orders
WHERE created_at >= CURDATE() AND status != 'cancelled';
Optimal indexes for these queries:
-- Index 1: For Q1 and Q3 (customer queries)
CREATE INDEX idx_customer_created
ON orders(customer_id, created_at);
-- Helps with customer filter + date sorting
-- Index 2: For Q2 (status queries)
CREATE INDEX idx_status_created
ON orders(status, created_at);
-- Helps with status filter + date sorting
-- Index 3: For Q4 (reporting queries with covering index)
CREATE INDEX idx_reporting
ON orders(created_at, status, total_amount);
-- Covering index: has all columns needed (no table access)
-- Verify our indexes
SHOW INDEXES FROM orders;
Practice Exercise:
Scenario: You have a social media posts table with poor performance.
CREATE TABLE posts (
id INT PRIMARY KEY,
user_id INT,
content TEXT,
category VARCHAR(50),
likes_count INT DEFAULT 0,
created_at TIMESTAMP,
is_published BOOLEAN DEFAULT 1
);
-- These queries are slow:
-- Q1: User's published posts
SELECT * FROM posts
WHERE user_id = 456 AND is_published = 1
ORDER BY created_at DESC;
-- Q2: Top posts by category
SELECT id, category, likes_count FROM posts
WHERE category = 'Technology' AND is_published = 1
ORDER BY likes_count DESC
LIMIT 10;
-- Q3: Recent posts with content preview
SELECT id, LEFT(content, 100) as preview, created_at
FROM posts
WHERE is_published = 1
ORDER BY created_at DESC
LIMIT 20;
Task: Create optimal indexes for these queries.
Solution:
-- Index for Q1 (user's posts)
CREATE INDEX idx_user_published_created
ON posts(user_id, is_published, created_at);
-- Covers user filter, published filter, and date sorting
-- Index for Q2 (top posts - covering index)
CREATE INDEX idx_category_published_likes
ON posts(category, is_published, likes_count, id);
-- Covering index includes all needed columns (no table access)
-- Index for Q3 (recent posts)
CREATE INDEX idx_published_created
ON posts(is_published, created_at);
-- Can't include content (too large) but optimizes the filter+sort
-- Note: Still needs table access for content, but much faster
-- Verify performance improvement
EXPLAIN SELECT * FROM posts
WHERE user_id = 456 AND is_published = 1
ORDER BY created_at DESC;
-- Should show: type=ref, key=idx_user_published_created
Best Practices Summary
✓ Use meaningful, consistent index names
✓ Create composite indexes for multi-column queries
✓ Put most selective columns first in composite indexes
✓ Use covering indexes for frequently-run queries
✓ Test index changes on a copy of production data
✓ Monitor index usage with EXPLAIN and slow query log
✓ Remove unused indexes (check with sys.schema_unused_indexes)
✗ Don't create redundant indexes (idx_a, idx_a_b when idx_a_b exists)
✗ Don't over-index (each index adds overhead to writes)
✗ Don't index every column "just in case"
✗ Don't forget to test performance before and after
Summary
In this lesson, you learned:
- Multiple ways to create indexes: CREATE INDEX, ALTER TABLE, and inline in CREATE TABLE
- Index naming conventions for better organization
- Composite index column ordering rules (equality → range → sorting)
- Covering indexes eliminate table access for better performance
- Invisible indexes (MySQL 8.0+) for safe index testing
- How to drop and modify indexes
- Index prefix technique for long columns
- Real-world index optimization strategies
Next Up: In the next lesson, we'll master query optimization using EXPLAIN and learn how to identify and fix slow queries!