mysql-best-practices

MySQL 8.x best practices for high-performance, secure SaaS applications. Use when designing database schemas, writing queries, optimizing performance, implementing multi-tenant isolation, configuring servers, setting up replication, hardening security, or troubleshooting slow queries. Covers indexing, EXPLAIN analysis, transactions, locking, advanced SQL (CTEs, window functions), backup, and server tuning.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "mysql-best-practices" with this command: npx skills add peterbamuhigire/skills-web-dev/peterbamuhigire-skills-web-dev-mysql-best-practices

Required Plugins

Superpowers plugin: MUST be active for all work using this skill. Use throughout the entire build pipeline — design decisions, code generation, debugging, quality checks, and any task where it offers enhanced capabilities. If superpowers provides a better way to accomplish something, prefer it over the default approach.

MySQL Best Practices for SaaS

Production-grade MySQL patterns for high-performance, secure, scalable SaaS applications.

Core Principle: Performance is query response time. Optimize queries and indexes first, tune server second, scale hardware last.

Access Policy (Required): Frontend clients must never access the database directly. All data access must flow through backend services exposed via APIs.

Deep References: references/query-performance.md, references/indexing-deep-dive.md, references/server-tuning-mycnf.md, references/security-hardening.md, references/high-availability.md, references/advanced-sql-patterns.md, references/backup-recovery.md, references/transaction-locking.md

SQL References: references/stored-procedures.sql, references/triggers.sql, references/partitioning.sql

Deployment Environments

EnvironmentOSDatabaseNotes
DevelopmentWindows 11 (WAMP)MySQL 8.4.7User: root, no password
StagingUbuntu VPSMySQL 8.xUser: peter, password required
ProductionDebian VPSMySQL 8.xUser: peter, password required

Cross-platform rules:

  • Always use utf8mb4_unicode_ci collation (never utf8mb4_0900_ai_ci or utf8mb4_general_ci)
  • Never use platform-specific SQL features; test on MySQL 8.x
  • Production migrations go in database/migrations-production/ with -production suffix

When to Use

✅ Designing MySQL schemas ✅ Optimizing queries ✅ Multi-tenant isolation ✅ Transactional systems ✅ Security hardening ✅ Server tuning ✅ Replication setup ✅ Advanced SQL

❌ NoSQL databases ❌ OLAP/data warehouses ❌ Non-MySQL databases


Schema Design

Database & Table Defaults

CREATE DATABASE saas_platform
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE TABLE users (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  tenant_id INT UNSIGNED NOT NULL,
  email VARCHAR(255) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  KEY idx_tenant (tenant_id),
  UNIQUE KEY uk_tenant_email (tenant_id, email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;

Always: ENGINE=InnoDB (ACID, row-level locking, crash recovery), utf8mb4, ROW_FORMAT=DYNAMIC.

Data Types — Choose Smallest Sufficient

-- Integers (use smallest that fits)
TINYINT UNSIGNED     -- 0-255 (status codes, booleans)
SMALLINT UNSIGNED    -- 0-65,535 (categories, small counts)
INT UNSIGNED         -- 0-4.2B (most FKs, tenant_id)
BIGINT UNSIGNED      -- >4.2B (PKs, high-volume tables)

-- Financial — NEVER use FLOAT/DOUBLE
amount DECIMAL(13, 2) NOT NULL  -- Exact precision

-- Fixed-length codes
currency CHAR(3) NOT NULL DEFAULT 'UGX'
country_code CHAR(2) NOT NULL

-- Enums for fixed sets
status ENUM('pending', 'active', 'suspended') NOT NULL

-- Timestamps — store UTC, convert at app layer
TIMESTAMP            -- 4 bytes, auto UTC conversion
DATETIME             -- 8 bytes, no timezone handling

Less data = more performance. Smaller types mean more rows per InnoDB page (16KB), better buffer pool utilization, faster I/O.

Normalization (3NF Default)

-- 1NF: Atomic values (no CSV in columns)
-- 2NF: Depend on entire key (not partial)
-- 3NF: Depend only on PK (no transitive dependencies)

-- Strategic denormalization ONLY for proven performance needs
-- Keep denormalized data in sync via triggers

Foreign Keys

FOREIGN KEY fk_tenant (tenant_id) REFERENCES tenants(id)
  ON DELETE RESTRICT ON UPDATE CASCADE

Strategies: RESTRICT (prevent orphans), CASCADE (delete children), SET NULL (optional relationships).


Indexing

The #1 performance lever. Every slow query starts here.

📖 See references/indexing-deep-dive.md for B-tree internals, ICP, covering indexes

Leftmost Prefix Rule (CRITICAL)

MySQL can only use an index starting from the leftmost column:

-- Index: KEY idx_abc (a, b, c)
-- ✓ Uses index: WHERE a = ?
-- ✓ Uses index: WHERE a = ? AND b = ?
-- ✓ Uses index: WHERE a = ? AND b = ? AND c = ?
-- ✗ Cannot use: WHERE b = ?        (skips leftmost)
-- ✗ Cannot use: WHERE b = ? AND c = ?  (skips leftmost)

ESR Rule (Equality, Sort, Range)

Order composite index columns: equality first, sort next, range last.

-- Query: WHERE tenant_id = ? AND status = ? ORDER BY created_at DESC WHERE amount > ?
KEY idx_esr (tenant_id, status, created_at DESC, amount)
--           ^equality   ^equality  ^sort              ^range (last!)

Range conditions (<, >, BETWEEN, LIKE 'prefix%') stop further index column use.

Index Types

-- Primary key (clustered — table data stored IN this index)
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY

-- Unique (uniqueness enforcement + fast lookup)
UNIQUE KEY uk_email (tenant_id, email)

-- Regular (WHERE, JOIN, ORDER BY)
KEY idx_tenant_status (tenant_id, status)

-- Fulltext (text search)
FULLTEXT INDEX ft_search (title, content)

-- Descending (MySQL 8.0+ — for ORDER BY col DESC)
KEY idx_created_desc (tenant_id, created_at DESC)

EXPLAIN — Always Verify

EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE tenant_id = 1 AND status = 'active';

-- EXPLAIN ANALYZE (MySQL 8.0.18+) — measures actual execution time
EXPLAIN ANALYZE SELECT * FROM orders WHERE tenant_id = 1 ORDER BY created_at DESC LIMIT 25;

Access types (best to worst):

typeMeaningAction
constUnique index, 1 rowIdeal
eq_refUnique index in JOINIdeal
refNon-unique indexGood
rangeIndex range scanGood
indexFull index scanReview
ALLFull TABLE scanFIX IMMEDIATELY

Red flags: type: ALL with key: NULL = no usable index. select_full_join > 0 = unindexed join. Stop and fix.

Index Anti-Patterns

-- ✗ Redundant indexes (KEY(a) is redundant if KEY(a,b) exists)
-- ✗ Low-cardinality solo indexes (KEY(is_deleted) — only 2 values)
-- ✗ Over-indexing (each index = write amplification)
-- ✗ Missing join indexes (every JOIN column needs an index)

-- Monitor unused indexes
SELECT object_name, index_name FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE count_read = 0 AND index_name != 'PRIMARY' AND object_schema = 'your_db';

Query Performance

Performance = query response time. The only metric users experience.

📖 See references/query-performance.md for profiling workflow, 9 essential metrics, query load

Optimization Workflow

  1. Profile — Enable slow query log or use Performance Schema to find slowest queries
  2. EXPLAIN — Analyze execution plan; look for type: ALL, missing indexes
  3. Index — Create/modify indexes following ESR rule
  4. Verify — Re-EXPLAIN, confirm improvement
  5. Monitor — Track response time continuously

Query Rules

-- ✓ DO: Select only needed columns
SELECT id, name, total FROM orders WHERE tenant_id = ? LIMIT 25;

-- ✗ DON'T: SELECT * (wastes I/O, prevents covering indexes)
SELECT * FROM orders;

-- ✓ DO: Parameterized queries (security + plan reuse)
PREPARE stmt FROM 'SELECT * FROM users WHERE tenant_id = ? AND id = ?';
EXECUTE stmt USING @tenant_id, @user_id;

-- ✓ DO: Keyset pagination (fast at any offset)
SELECT id, name FROM items WHERE tenant_id = ? AND id < ? ORDER BY id DESC LIMIT 25;

-- ✗ DON'T: Large OFFSET (scans and discards rows)
SELECT * FROM items LIMIT 25 OFFSET 100000;

-- ✓ DO: JOINs with indexed columns
SELECT o.id, c.name FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.tenant_id = ?;

-- ✗ DON'T: Subqueries where JOINs work
SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE name LIKE '%smith%');

Slow Query Log

# my.cnf
slow_query_log = 1
long_query_time = 1              # Log queries > 1 second
log_queries_not_using_indexes = 1 # Also log queries without indexes
log_slow_extra = ON              # MySQL 8.0.14+ — extra metrics

Security

Never use root for application connections. Principle of least privilege.

📖 See references/security-hardening.md for TDE, SSL/TLS, audit, firewall, RBAC

Application User

-- Create dedicated application user with minimal privileges
CREATE USER 'saas_app'@'%' IDENTIFIED BY 'strong_random_password_here';
GRANT SELECT, INSERT, UPDATE, DELETE ON saas_platform.* TO 'saas_app'@'%';
-- Never grant: FILE, PROCESS, SHUTDOWN, SUPER, CREATE USER, GRANT OPTION

-- Read-only user for reporting/replicas
CREATE USER 'saas_readonly'@'%' IDENTIFIED BY 'another_strong_password';
GRANT SELECT ON saas_platform.* TO 'saas_readonly'@'%';

Encryption

# my.cnf — TDE (Transparent Data Encryption)
early-plugin-load=keyring_file.so
default-table-encryption = ON

# SSL/TLS (encrypt in transit)
require_secure_transport = ON
ssl_ca=/path/to/ca-cert.pem
ssl_cert=/path/to/server-cert.pem
ssl_key=/path/to/server-key.pem
-- Application-level encryption for sensitive columns
phone_encrypted VARBINARY(255)  -- AES-256 at app layer via PHP openssl_encrypt()

Multi-Tenant Isolation (CRITICAL)

-- EVERY query MUST include tenant filter
SELECT * FROM orders WHERE tenant_id = ? AND id = ?;

-- Never allow cross-tenant access
-- Always verify tenant_id at application layer before query
-- Use UUIDs for public-facing IDs (prevent enumeration)

-- Stored procedures must enforce tenant_id
CREATE PROCEDURE sp_get_order(IN p_tenant_id INT, IN p_order_id BIGINT)
BEGIN
  SELECT * FROM orders WHERE tenant_id = p_tenant_id AND id = p_order_id;
END;

SQL Injection Prevention

-- ✓ CORRECT: Parameterized queries
PREPARE stmt FROM 'SELECT * FROM users WHERE email = ? AND tenant_id = ?';
EXECUTE stmt USING @email, @tenant_id;

-- ✗ WRONG: String concatenation (VULNERABLE!)
SET @q = CONCAT('SELECT * FROM users WHERE email = "', @email, '"');

Transactions & Locking

📖 See references/transaction-locking.md for MVCC, gap locks, deadlock patterns

Isolation Levels

LevelGap LocksPhantom RowsUse Case
READ COMMITTEDNoYesRecommended for most SaaS apps — reduces locking contention
REPEATABLE READYesNo (InnoDB)Default — more locking, stronger consistency
-- Set per-session (recommended for SaaS)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

Deadlock Prevention

-- ✓ DO: Lock rows in consistent order
START TRANSACTION;
SELECT * FROM accounts WHERE id = LEAST(100, 200) FOR UPDATE;
SELECT * FROM accounts WHERE id = GREATEST(100, 200) FOR UPDATE;
-- ... update both ...
COMMIT;

-- ✓ DO: Keep transactions short
-- ✓ DO: Use IN() instead of BETWEEN (fewer gap locks)
-- ✗ DON'T: Long-running transactions (block purging, grow undo log)
-- ✗ DON'T: BEGIN without COMMIT/ROLLBACK (stalled transactions)

Transaction Best Practices

-- Always use explicit transactions for multi-statement operations
START TRANSACTION;
  INSERT INTO orders (...) VALUES (...);
  INSERT INTO order_items (...) VALUES (...);
  UPDATE inventory SET quantity = quantity - ? WHERE id = ? AND quantity >= ?;
  -- Check affected rows — if 0, ROLLBACK (insufficient inventory)
COMMIT;

Advanced SQL

📖 See references/advanced-sql-patterns.md for full patterns, recursive CTEs, pivots

Key Patterns

-- CTE with window function
WITH monthly AS (
  SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, SUM(total) AS revenue
  FROM orders WHERE tenant_id = ? GROUP BY month
)
SELECT month, revenue, revenue - LAG(revenue) OVER (ORDER BY month) AS change
FROM monthly;

-- Ranking
SELECT id, name, RANK() OVER (PARTITION BY category ORDER BY total DESC) AS rank
FROM products WHERE tenant_id = ?;

-- ROLLUP subtotals
SELECT COALESCE(category, 'TOTAL') AS category, SUM(amount)
FROM sales WHERE tenant_id = ? GROUP BY category WITH ROLLUP;

-- Recursive CTE (hierarchy)
WITH RECURSIVE tree AS (
  SELECT id, name, 1 AS depth FROM employees WHERE supervisor_id IS NULL AND tenant_id = ?
  UNION ALL
  SELECT e.id, e.name, t.depth + 1 FROM employees e JOIN tree t ON e.supervisor_id = t.id
)
SELECT * FROM tree ORDER BY depth, name;

Server Tuning

📖 See references/server-tuning-mycnf.md for 70+ metrics, complete my.cnf template

Critical Variables

VariableDefaultRecommendation
innodb_buffer_pool_size128MB70-80% of server RAM (most important variable)
innodb_buffer_pool_instances88-24 for high concurrency
innodb_redo_log_capacity100MB1-4GB for write-heavy workloads (8.0.30+)
innodb_log_buffer_size16MB48MB for heavy transactions
innodb_flush_log_at_trx_commit1Keep at 1 (ACID durability)
max_connections151500+ for production
innodb_dedicated_serverOFFON for dedicated MySQL servers (8.0.14+)

Do NOT randomly tune variables. The buffer pool is the overwhelmingly most impactful setting. Optimize queries and indexes first.

Monitoring

-- Buffer pool hit rate (should be > 99%)
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
-- Hit rate = read_requests / (read_requests + reads) * 100

-- Active threads (should be low single digits normally)
SHOW GLOBAL STATUS LIKE 'Threads_running';

-- Row lock contention (should be near zero)
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';

-- Table maintenance
ANALYZE TABLE customers, orders;  -- Update index statistics
OPTIMIZE TABLE large_table;       -- Reclaim space (locks table — off-peak only)

Operations

📖 See references/backup-recovery.md (mysqldump, XtraBackup, point-in-time recovery) | references/high-availability.md (GTID replication, InnoDB Cluster, failover)

# Essential backup command
mysqldump --single-transaction --routines --triggers --all-databases > backup.sql

Migrations

# Pre-migration checklist: 1) grep references, 2) check stored procs, 3) backup
grep -r "table_name" --include="*.php" --include="*.sql" .
mysqldump --single-transaction database_name > backup_pre_migration.sql
ALTER TABLE orders ADD COLUMN tracking VARCHAR(50) DEFAULT NULL;  -- Non-breaking

Checklist

Schema: ✅ UTF8MB4 + InnoDB + ROW_FORMAT=DYNAMIC ✅ Smallest sufficient data types ✅ DECIMAL for money ✅ 3NF normalized ✅ Foreign keys with RESTRICT/CASCADE

Indexing: ✅ ESR composite indexes ✅ Leftmost prefix satisfied ✅ No redundant indexes ✅ Join columns indexed ✅ EXPLAIN verified on critical queries ✅ No type: ALL in EXPLAIN

Performance: ✅ No SELECT * ✅ Keyset pagination for large offsets ✅ Covering indexes for hot queries ✅ Slow query log enabled ✅ Buffer pool sized to 70-80% RAM ✅ ANALYZE TABLE regular

Security: ✅ Application user (not root) ✅ Minimal privileges ✅ Parameterized queries only ✅ TDE + SSL encryption ✅ tenant_id in EVERY query ✅ UUIDs for public IDs

Transactions: ✅ READ COMMITTED isolation for SaaS ✅ Consistent lock ordering ✅ Short transactions ✅ IN() over BETWEEN for less locking ✅ No stalled transactions

Operations: ✅ Binary logging enabled ✅ Regular backups tested ✅ Monitoring active ✅ Migration checklist followed


Sources: Efficient MySQL Performance (Nichter 2022), Mastering MySQL Administration (Kumar et al. 2024), Leveling Up with SQL (Simon 2023) Last Updated: 2026-03-04 Maintained by: Peter Bamuhigire

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

vibe security skill

No summary provided by upstream source.

Repository SourceNeeds Review
Security

implementation-status-auditor

No summary provided by upstream source.

Repository SourceNeeds Review
Security

skill-safety-audit

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

google-play-store-review

No summary provided by upstream source.

Repository SourceNeeds Review