MySQL Schema Design and Indexing Essentials
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/database/references/mysql.md |
| Description | Not specified |
Source Content
MySQL Schema Design and Indexing Essentials
This is a lighter reference for MySQL basics; Postgres is the primary engine in this skill. Expand this reference as MySQL workloads appear.
Schema Design Basics
Naming Conventions
- Tables: plural, lowercase, snake_case (
orders,customer_payments) - Columns: lowercase, snake_case (
customer_id,created_at) - Foreign keys:
<table>_idpattern (orders.customer_id,payments.order_id) - Indexes:
idx_<table>_<column(s)>(idx_orders_customer_date)
Character Sets and Collation
-- Use utf8mb4 for full Unicode support (emoji, etc.)CREATE TABLE users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;Always Use InnoDB
- InnoDB: ACID, transactions, crash recovery — use this for everything.
- MyISAM: deprecated; use InnoDB only.
Indexing Strategy
Single-Column B-Tree Index
-- Equality and range queriesCREATE INDEX idx_users_email ON users (email);CREATE INDEX idx_orders_customer_id ON orders (customer_id);Composite Indexes
Rule: Most selective columns first, then range/sort columns.
-- Query: SELECT * FROM orders WHERE customer_id = ? AND status = 'shipped' ORDER BY created_at DESCCREATE INDEX idx_orders_customer_status_dateON orders (customer_id, status, created_at DESC);Covering Indexes
Include non-key columns to avoid table lookups (MySQL 5.7+):
-- SELECT order_total, status FROM orders WHERE customer_id = ?CREATE INDEX idx_orders_customer_coveringON orders (customer_id, order_total, status);Partial Indexes (MySQL 5.7+, via WHERE clause)
-- Index only active rowsCREATE INDEX idx_active_ordersON orders (customer_id, created_at)WHERE status != 'cancelled';Full-Text Indexes
-- Full-text search on documentsCREATE TABLE articles ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255), content LONGTEXT, FULLTEXT INDEX ft_title_content (title, content));
-- QuerySELECT * FROM articlesWHERE MATCH (title, content) AGAINST ('database performance' IN BOOLEAN MODE);Foreign Keys and Referential Integrity
CREATE TABLE customers ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, customer_id BIGINT NOT NULL, total DECIMAL(10, 2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE CASCADE) ENGINE=InnoDB;ON DELETE policies:
CASCADE— delete child rows when parent is deletedRESTRICT— block delete if children existSET NULL— set FK to NULL on parent delete (column must allow NULL)
Normalization (3NF)
Design for normalization first, denormalize only with measured justification.
Bad (denormalized, data redundancy):
CREATE TABLE orders ( id INT PRIMARY KEY, customer_id INT, customer_name VARCHAR(100), -- Redundant; depends on customer_id customer_email VARCHAR(100), -- Redundant order_date DATE, total DECIMAL(10, 2));Good (normalized):
CREATE TABLE customers ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), email VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, customer_id BIGINT NOT NULL, order_date DATE, total DECIMAL(10, 2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES customers (id));
-- Composite index on common queriesCREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);Query Optimization Basics
EXPLAIN FORMAT=JSON
EXPLAIN FORMAT=JSONSELECT * FROM ordersWHERE customer_id = 123ORDER BY created_at DESCLIMIT 10\GLook for:
"type": “ref” (good, index) vs “ALL” (bad, full table scan)"rows": estimated rows — if wildly off, runANALYZE TABLE orders;"key": which index was used (ornullif none)
Common Query Patterns
Equality + range:
-- Use index for both filter and sortSELECT * FROM ordersWHERE customer_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)ORDER BY created_at DESC;
CREATE INDEX idx_orders_customer_date ON orders (customer_id, created_at);Top-N per group (LIMIT … OFFSET):
-- Slow: materializes all rows then sortsSELECT * FROM productsORDER BY category, price DESCLIMIT 10 OFFSET 100;
-- Better: cursor paginationSELECT * FROM productsWHERE (category, price) < (?, ?) -- KeysetORDER BY category DESC, price DESCLIMIT 11;Pagination: Keyset Over OFFSET
-- First page: use NULL or 0 for cursorSELECT * FROM ordersWHERE customer_id = ? AND created_at < NOW() -- Cursor positionORDER BY created_at DESCLIMIT 21; -- Fetch one extra to detect "more"
-- Next page: use last row's created_at as cursorSELECT * FROM ordersWHERE customer_id = ? AND created_at < (last row's created_at)ORDER BY created_at DESCLIMIT 21;Performance Tuning Parameters
-- Check current settingsSHOW VARIABLES LIKE 'innodb_buffer_pool%';SHOW VARIABLES LIKE 'query_cache%';
-- Typical production recommendations:-- innodb_buffer_pool_size: 50–80% of available RAM-- innodb_log_file_size: 1–2 GB for large tables-- max_connections: adjust per workload (default 151)-- query_cache_size: 0 (disabled in modern MySQL; use app-level caching)Monitoring and Maintenance
Table Statistics
-- Refresh statistics for query plannerANALYZE TABLE orders;
-- Check table sizeSELECT table_name, ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mbFROM information_schema.TABLESWHERE table_schema = 'your_database'ORDER BY size_mb DESC;Slow Query Log
-- Enable slow query logSET GLOBAL slow_query_log = 'ON';SET GLOBAL long_query_time = 2; -- Log queries > 2 seconds
-- View slow queriesSHOW GLOBAL STATUS LIKE 'Slow_queries';Key Differences from PostgreSQL
| Feature | MySQL | Postgres |
|---|---|---|
| Indexes | B-tree, Full-text, SPATIAL | B-tree, GIN, GIST, BRIN, Hash |
| Transactions | InnoDB only; implicit commit in other engines | Default; all operations are transactional |
| JSON | Limited; use JSON_EXTRACT, JSON_SET | Rich JSONB with operators, GIN indexing |
| Auto-increment | AUTO_INCREMENT column | SERIAL / GENERATED AS IDENTITY |
| Sequences | N/A | Native support |
| Full-text | FULLTEXT INDEX | to_tsvector, GIN indexes |
| Partitioning | RANGE, LIST, HASH | RANGE, LIST, HASH, plus native time partitioning |
References
- MySQL 8.0 Documentation
- MySQL Indexing Best Practices
- Baron Schwartz — High Performance MySQL (reference, though dated)