Performance Optimization
Introduction
Optimizing MySQL performance ensures fast query execution, efficient resource usage, and better user experience. This guide covers essential optimization techniques for WAMP/XAMPP environments.
Query Optimization
1. Use EXPLAIN
-- Analyze query execution
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- Extended information
EXPLAIN EXTENDED SELECT * FROM users WHERE email = 'test@example.com';
SHOW WARNINGS;
-- JSON format (MySQL 5.7+)
EXPLAIN FORMAT=JSON SELECT * FROM users WHERE email = 'test@example.com';
Key EXPLAIN Columns:
- type: Join type (best to worst: system, const, eq_ref, ref, range, index, ALL)
- possible_keys: Indexes that could be used
- key: Index actually used
- rows: Estimated rows to examine
- Extra: Additional information
2. Avoid SELECT *
-- Bad: Retrieves unnecessary data
SELECT * FROM users WHERE user_id = 1;
-- Good: Select only needed columns
SELECT user_id, username, email FROM users WHERE user_id = 1;
3. Use Indexes Effectively
-- Create index on frequently queried columns
CREATE INDEX idx_email ON users(email);
-- Composite index for multi-column queries
CREATE INDEX idx_name ON users(last_name, first_name);
-- Use covering indexes
CREATE INDEX idx_user_info ON users(user_id, username, email);
SELECT user_id, username, email FROM users WHERE user_id = 1;
-- Query uses index only, no table access needed
4. Optimize WHERE Clauses
-- Bad: Function on indexed column prevents index use
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- Good: Sargable query uses index
SELECT * FROM users
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
-- Bad: Leading wildcard prevents index use
SELECT * FROM users WHERE email LIKE '%@gmail.com';
-- Good: Can use index
SELECT * FROM users WHERE email LIKE 'john%@gmail.com';
5. Limit Result Sets
-- Always use LIMIT for large results
SELECT * FROM posts ORDER BY created_at DESC LIMIT 100;
-- Pagination
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 40;
6. Use Proper JOIN Syntax
-- Explicit JOIN (clear and optimizable)
SELECT u.username, p.title
FROM users u
INNER JOIN posts p ON u.user_id = p.author_id
WHERE u.is_active = TRUE;
-- Avoid implicit JOIN
SELECT u.username, p.title
FROM users u, posts p
WHERE u.user_id = p.author_id AND u.is_active = TRUE;
7. Optimize Subqueries
-- Slow: Subquery in SELECT
SELECT
u.username,
(SELECT COUNT(*) FROM posts WHERE author_id = u.user_id) AS post_count
FROM users u;
-- Faster: JOIN with GROUP BY
SELECT u.username, COUNT(p.post_id) AS post_count
FROM users u
LEFT JOIN posts p ON u.user_id = p.author_id
GROUP BY u.user_id;
-- Use EXISTS instead of IN for large subqueries
-- Slow
SELECT * FROM users
WHERE user_id IN (SELECT user_id FROM orders);
-- Faster
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.user_id);
Index Optimization
When to Create Indexes
Always index:
- Primary keys (automatic)
- Foreign keys
- Columns in WHERE clauses
- Columns in JOIN conditions
- Columns in ORDER BY
Consider indexing:
- Columns in GROUP BY
- Columns with high selectivity
Don’t index:
- Small tables
- Columns with low selectivity
- Columns rarely used in queries
- Columns with frequent updates
Index Types
-- B-Tree (default, best for most cases)
CREATE INDEX idx_name ON users(last_name);
-- Unique index
CREATE UNIQUE INDEX idx_email ON users(email);
-- Composite index (order matters!)
CREATE INDEX idx_location ON users(country, city, zip_code);
-- Good for: WHERE country = 'USA' AND city = 'NYC'
-- Good for: WHERE country = 'USA'
-- Bad for: WHERE city = 'NYC' (doesn't use index)
-- Full-text index
CREATE FULLTEXT INDEX idx_content ON articles(title, content);
-- Prefix index (for long strings)
CREATE INDEX idx_url ON pages(url(50)); -- First 50 characters
Analyzing Indexes
-- Show indexes on table
SHOW INDEX FROM users;
-- Analyze index usage
SELECT * FROM information_schema.STATISTICS
WHERE table_schema = 'mydb' AND table_name = 'users';
-- Find unused indexes (MySQL 8.0+)
SELECT * FROM sys.schema_unused_indexes;
-- Drop unused indexes
DROP INDEX idx_unused ON users;
Configuration Optimization
Key MySQL Configuration Variables
Edit my.ini (Windows) or my.cnf (Linux/Mac):
[mysqld]
# Buffer Pool (most important for InnoDB)
# Set to 70-80% of available RAM for dedicated server
# Or 256MB-1GB for development
innodb_buffer_pool_size = 1G
# Log file size
innodb_log_file_size = 256M
# Query cache (deprecated in MySQL 8.0)
# MySQL 5.7 and earlier
query_cache_type = 1
query_cache_size = 64M
# Thread cache
thread_cache_size = 8
# Table cache
table_open_cache = 2000
# Max connections
max_connections = 150
# Sort and join buffers
sort_buffer_size = 2M
join_buffer_size = 2M
read_buffer_size = 1M
read_rnd_buffer_size = 1M
# Temporary tables
tmp_table_size = 64M
max_heap_table_size = 64M
# Connection timeout
wait_timeout = 600
interactive_timeout = 600
View Current Configuration
-- Show all variables
SHOW VARIABLES;
-- Show specific variable
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- Show status
SHOW STATUS;
-- Show processlist
SHOW FULL PROCESSLIST;
Calculate Optimal Settings
-- Check current usage
SELECT
FORMAT(SUM(data_length + index_length), 0) AS size_bytes,
FORMAT(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb
FROM information_schema.TABLES
WHERE table_schema = 'mydb';
-- Monitor buffer pool usage
SHOW STATUS LIKE 'Innodb_buffer_pool%';
Table Optimization
1. Choose Right Storage Engine
-- InnoDB (default, recommended)
-- - ACID compliant
-- - Foreign keys
-- - Row-level locking
CREATE TABLE users (...) ENGINE=InnoDB;
-- MyISAM (legacy)
-- - No foreign keys
-- - Table-level locking
-- - Full-text search (before MySQL 5.6)
CREATE TABLE logs (...) ENGINE=MyISAM;
2. Optimize Table Structure
-- Analyze table
ANALYZE TABLE users;
-- Optimize table (defragment)
OPTIMIZE TABLE users;
-- Check table
CHECK TABLE users;
-- Repair table (if corrupted)
REPAIR TABLE users;
3. Use Appropriate Data Types
-- Good: Right-sized types
CREATE TABLE products (
product_id INT UNSIGNED,
price DECIMAL(10,2),
stock SMALLINT UNSIGNED,
is_active BOOLEAN
);
-- Bad: Oversized types
CREATE TABLE products (
product_id BIGINT,
price DOUBLE,
stock INT,
is_active VARCHAR(10)
);
4. Normalize Appropriately
-- Balance normalization with performance
-- Sometimes denormalization improves read performance
-- Instead of JOIN every time:
SELECT u.username, COUNT(p.post_id)
FROM users u
LEFT JOIN posts p ON u.user_id = p.author_id
GROUP BY u.user_id;
-- Can denormalize:
ALTER TABLE users ADD COLUMN post_count INT DEFAULT 0;
-- Update via trigger or application
UPDATE users SET post_count = post_count + 1
WHERE user_id = ?;
Caching Strategies
1. Application-Level Caching
// Use Redis or Memcached
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// Check cache first
$cacheKey = 'user:' . $userId;
$user = $redis->get($cacheKey);
if (!$user) {
// Query database
$user = $db->query("SELECT * FROM users WHERE user_id = ?", [$userId]);
// Store in cache (expire in 1 hour)
$redis->setex($cacheKey, 3600, json_encode($user));
}
2. Query Cache (MySQL 5.7 and earlier)
-- Enable query cache
SET GLOBAL query_cache_type = 1;
SET GLOBAL query_cache_size = 67108864; -- 64MB
-- Check query cache stats
SHOW STATUS LIKE 'Qcache%';
-- Force cache
SELECT SQL_CACHE * FROM users WHERE user_id = 1;
-- Bypass cache
SELECT SQL_NO_CACHE * FROM users WHERE user_id = 1;
Note: Query cache removed in MySQL 8.0
3. Result Set Caching
// Cache expensive query results
$cacheKey = 'dashboard:stats';
if (!$cache->has($cacheKey)) {
$stats = $db->query("/* expensive query */");
$cache->set($cacheKey, $stats, 300); // 5 minutes
}
Connection Pooling
PHP PDO Connection
// Reuse connections
class Database {
private static $instance = null;
public static function getInstance() {
if (self::$instance == null) {
self::$instance = new PDO(
'mysql:host=localhost;dbname=mydb',
'user',
'pass',
[
PDO::ATTR_PERSISTENT => true, // Persistent connection
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]
);
}
return self::$instance;
}
}
Connection Limits
-- Set max connections
SET GLOBAL max_connections = 200;
-- View current connections
SHOW STATUS LIKE 'Threads_connected';
SHOW PROCESSLIST;
-- Kill idle connections
KILL CONNECTION_ID;
Monitoring and Analysis
1. Slow Query Log
# Enable slow query log in my.ini/my.cnf
[mysqld]
slow_query_log = 1
slow_query_log_file = /path/to/slow-query.log
long_query_time = 2 # Queries longer than 2 seconds
log_queries_not_using_indexes = 1
-- Enable dynamically
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
2. Analyze Slow Queries
# Use mysqldumpslow (included with MySQL)
mysqldumpslow -t 10 /path/to/slow-query.log
# Shows top 10 slow queries
# Options:
# -s: Sort (at=average time, t=total time, c=count)
# -t: Top N queries
# -g: Filter pattern
3. Performance Schema (MySQL 5.5+)
-- Enable performance schema
SET GLOBAL performance_schema = ON;
-- Find slow statements
SELECT
DIGEST_TEXT,
COUNT_STAR AS executions,
ROUND(AVG_TIMER_WAIT / 1000000000000, 2) AS avg_seconds,
ROUND(MAX_TIMER_WAIT / 1000000000000, 2) AS max_seconds
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
-- Find queries using temporary tables
SELECT * FROM sys.statements_with_temp_tables;
-- Find full table scans
SELECT * FROM sys.statements_with_full_table_scans;
4. Information Schema
-- Table sizes
SELECT
table_name,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb
FROM information_schema.TABLES
WHERE table_schema = 'mydb'
ORDER BY (data_length + index_length) DESC;
-- Column information
SELECT * FROM information_schema.COLUMNS
WHERE table_schema = 'mydb' AND table_name = 'users';
Partitioning
Range Partitioning
-- Partition by date range
CREATE TABLE logs (
log_id INT,
log_date DATE,
message TEXT
) PARTITION BY RANGE (YEAR(log_date)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- Query specific partition
SELECT * FROM logs PARTITION (p2024);
List Partitioning
-- Partition by list of values
CREATE TABLE users (
user_id INT,
country VARCHAR(2)
) PARTITION BY LIST COLUMNS(country) (
PARTITION p_us VALUES IN ('US'),
PARTITION p_eu VALUES IN ('UK', 'FR', 'DE'),
PARTITION p_asia VALUES IN ('JP', 'CN', 'IN'),
PARTITION p_other VALUES IN (DEFAULT)
);
Best Practices Checklist
Query Optimization
- Use EXPLAIN for slow queries
- Avoid SELECT *
- Use appropriate indexes
- Optimize WHERE clauses
- Use LIMIT for large result sets
- Optimize JOINs and subqueries
Index Management
- Index foreign keys
- Index WHERE/JOIN columns
- Use composite indexes appropriately
- Remove unused indexes
- Monitor index usage
Configuration
- Optimize buffer pool size
- Configure appropriate timeouts
- Set reasonable connection limits
- Enable slow query log
- Monitor resource usage
Application Level
- Use connection pooling
- Implement caching
- Batch operations
- Prepare statements
- Handle errors properly
Monitoring
- Regular performance reviews
- Analyze slow queries
- Monitor disk usage
- Check connection counts
- Review error logs
Common Performance Issues
1. N+1 Query Problem
// Bad: N+1 queries
$users = $db->query("SELECT * FROM users");
foreach ($users as $user) {
$posts = $db->query(
"SELECT * FROM posts WHERE author_id = ?",
[$user['user_id']]
);
}
// Good: Single query with JOIN
$result = $db->query("
SELECT u.*, p.*
FROM users u
LEFT JOIN posts p ON u.user_id = p.author_id
");
2. Missing Indexes
-- Add missing indexes
CREATE INDEX idx_author ON posts(author_id);
CREATE INDEX idx_created ON posts(created_at);
3. Large Result Sets
-- Use pagination
SELECT * FROM logs
ORDER BY created_at DESC
LIMIT 100 OFFSET 0;
-- Or keyset pagination (more efficient)
SELECT * FROM logs
WHERE log_id < ?
ORDER BY log_id DESC
LIMIT 100;
4. Inefficient Queries
-- Rewrite inefficient queries
-- Use EXPLAIN to identify issues
-- Add appropriate indexes
-- Simplify complex JOINs
Tools and Resources
MySQL Tools
- MySQL Workbench (GUI)
- mysqldumpslow (analyze slow queries)
- mysqltuner (configuration recommendations)
- pt-query-digest (Percona Toolkit)
Monitoring Tools
- phpMyAdmin (Status, Variables)
- MySQL Performance Schema
- Information Schema
- sys Schema (MySQL 5.7+)
External Tools
- NewRelic
- Datadog
- PMM (Percona Monitoring)
- Grafana + Prometheus