MySQL & MariaDB Reference
MySQL & MariaDB Reference
MySQL 8.x commands and patterns you use every day: the mysql client, user management, indexes, EXPLAIN, performance schema queries, backup, and replication troubleshooting.
mysql client — connection and meta commands
# Connect
mysql -u root -p # prompt for password
mysql -u app_user -p mydb # connect to specific database
mysql -h 127.0.0.1 -P 3306 -u root -p # explicit host/port
mysql -u root -p --ssl-mode=REQUIRED # enforce TLS
# One-liners (no interactive shell)
mysql -u root -p -e "SHOW DATABASES;"
mysql -u root -pMySuperSecret mydb < dump.sql # restore
mysql -u root -p mydb -e "SELECT COUNT(*) FROM users;"
# MySQL shell meta commands (no semicolon needed)
\h -- help
\s -- server status, version, connection info
\u mydb -- use database
\! -- shell command
\q -- quit
status -- alias for \s
-- SQL meta queries
SHOW DATABASES;
SHOW TABLES;
SHOW TABLES FROM mydb;
DESCRIBE users; -- column info + types
SHOW CREATE TABLE users\G -- full CREATE TABLE (with indexes)
SHOW FULL PROCESSLIST; -- running queries (like pg_stat_activity)
SHOW GLOBAL VARIABLES LIKE '%timeout%';
SHOW GLOBAL STATUS LIKE 'Threads%';
SHOW ENGINE INNODB STATUS\G -- InnoDB internals (locks, buffer pool)
-- Switch output format (wide rows)
SELECT * FROM users LIMIT 5\G -- \G = vertical format, one column per line
User and privilege management
-- Create user (MySQL 8+)
CREATE USER 'appuser'@'%' IDENTIFIED BY 'StrongPassword123!';
CREATE USER 'appuser'@'10.0.0.%' IDENTIFIED BY 'pwd'; -- restrict subnet
CREATE USER 'readonly'@'%' IDENTIFIED WITH caching_sha2_password BY 'pwd';
-- Grant privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'appuser'@'%';
GRANT ALL PRIVILEGES ON mydb.* TO 'appuser'@'%';
GRANT SELECT ON mydb.users TO 'readonly'@'%'; -- table-level
GRANT SELECT (id, email) ON mydb.users TO 'ro'@'%'; -- column-level
-- Apply changes (not needed in MySQL 8+ but harmless)
FLUSH PRIVILEGES;
-- View grants
SHOW GRANTS FOR 'appuser'@'%';
SELECT user, host FROM mysql.user;
-- Revoke
REVOKE DELETE ON mydb.* FROM 'appuser'@'%';
REVOKE ALL PRIVILEGES ON mydb.* FROM 'appuser'@'%';
-- Change password
ALTER USER 'appuser'@'%' IDENTIFIED BY 'NewPassword!';
-- Drop user
DROP USER 'appuser'@'%';
-- Reset root password (MySQL 8+)
-- 1. Stop MySQL: systemctl stop mysql
-- 2. Start with skip-grant: mysqld --skip-grant-tables &
-- 3. Connect: mysql -u root
-- 4. FLUSH PRIVILEGES; ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewPwd!';
-- 5. Restart normally
DDL — tables, indexes, ALTER TABLE
-- Create table
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL DEFAULT '',
status ENUM('active', 'inactive', 'banned') NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_email (email),
INDEX idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ALTER TABLE (online DDL in MySQL 8 with ALGORITHM=INPLACE, LOCK=NONE)
ALTER TABLE users ADD COLUMN phone VARCHAR(20) AFTER email;
ALTER TABLE users MODIFY COLUMN name VARCHAR(200) NOT NULL;
ALTER TABLE users DROP COLUMN phone;
ALTER TABLE users RENAME COLUMN old_name TO new_name; -- MySQL 8+
ALTER TABLE users ADD INDEX idx_name (name);
ALTER TABLE users DROP INDEX idx_name;
ALTER TABLE users ADD CONSTRAINT fk_orders FOREIGN KEY (user_id) REFERENCES users(id);
-- Online schema change (check lock requirement first)
ALTER TABLE large_table ADD COLUMN new_col INT DEFAULT 0, ALGORITHM=INPLACE, LOCK=NONE;
-- If ALGORITHM=INPLACE fails: use pt-online-schema-change or gh-ost for zero-downtime
-- Truncate vs Delete
TRUNCATE TABLE sessions; -- fast, resets AUTO_INCREMENT, can't rollback (DDL)
DELETE FROM sessions; -- slow for large tables, transactional, preserves structure
-- Check table sizes
SELECT
table_name,
ROUND((data_length + index_length) / 1024 / 1024, 1) AS size_mb,
table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY size_mb DESC;
EXPLAIN — query analysis
-- EXPLAIN shows the query execution plan
EXPLAIN SELECT * FROM users WHERE email = 'alice@example.com';
EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 10;
-- EXPLAIN ANALYZE (MySQL 8.0.18+) — actually runs the query + shows real timing
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 123;
-- What to look for in EXPLAIN output:
-- type column (from best to worst):
-- system — single row (constant table)
-- const — primary key or unique index lookup
-- eq_ref — unique index join
-- ref — non-unique index lookup (GOOD)
-- range — index range scan (ACCEPTABLE)
-- index — full index scan (SLOW for large tables)
-- ALL — full table scan (BAD — add an index)
-- key column: which index is being used (NULL = no index used)
-- rows column: estimated rows scanned (lower is better)
-- filtered column: % of rows filtered (after reading)
-- Extra column: watch for these:
-- Using index = covering index (best case)
-- Using where = filter applied after reading
-- Using filesort = sort can't use index (consider index)
-- Using temporary = temp table (GROUP BY/ORDER BY mismatch)
-- Impossible WHERE = query can never return rows
-- Force an index (for testing)
SELECT * FROM orders FORCE INDEX (idx_user_id) WHERE user_id = 123;
-- Find slow queries via slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- log queries > 1 second
SHOW VARIABLES LIKE 'slow_query_log_file';
-- Parse: mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
Indexes — types and patterns
-- B-Tree index (default, most common)
CREATE INDEX idx_email ON users (email);
CREATE INDEX idx_status_date ON orders (status, created_at); -- composite
-- Composite index column order rule: put equality filters first, range last
-- Good: WHERE status = 'active' AND created_at > '2026-01-01'
-- Index: (status, created_at)
-- Covering index — include all columns needed by query (avoids table lookup)
CREATE INDEX idx_covering ON orders (user_id, status, created_at, total);
-- SELECT user_id, total FROM orders WHERE user_id = 1 AND status = 'paid'
-- → "Using index" — never touches the main table
-- Prefix index (for long VARCHAR/TEXT)
CREATE INDEX idx_slug ON articles (slug(50)); -- index first 50 chars
-- FULLTEXT index (for search)
CREATE FULLTEXT INDEX ft_content ON articles (title, body);
SELECT * FROM articles WHERE MATCH(title, body) AGAINST ('kubernetes deployment' IN BOOLEAN MODE);
-- Unique index
CREATE UNIQUE INDEX uk_email ON users (email);
-- Or in CREATE TABLE: UNIQUE KEY uk_email (email)
-- INVISIBLE index (MySQL 8+) — test impact before dropping
ALTER TABLE orders ALTER INDEX idx_status INVISIBLE; -- disable for optimizer
-- test queries...
ALTER TABLE orders ALTER INDEX idx_status VISIBLE; -- re-enable
-- Index stats
SHOW INDEX FROM orders;
SELECT index_name, cardinality FROM information_schema.statistics
WHERE table_schema = 'mydb' AND table_name = 'orders';
-- Unused indexes (expensive to maintain — drop them)
SELECT * FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL AND count_star = 0
ORDER BY object_schema, object_name;
Transactions and locking
-- Transaction basics
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
ROLLBACK; -- undo everything since START TRANSACTION
-- Isolation levels
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- avoids phantom reads in most cases
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- MySQL default
-- REPEATABLE READ: snapshot at first read, may see phantom rows in range queries
-- READ COMMITTED: fresh snapshot per statement (PostgreSQL default behavior)
-- SELECT ... FOR UPDATE — locks rows (pessimistic locking)
START TRANSACTION;
SELECT * FROM orders WHERE id = 42 FOR UPDATE; -- other transactions block
UPDATE orders SET status = 'processing' WHERE id = 42;
COMMIT;
-- SELECT ... LOCK IN SHARE MODE — shared lock (readers ok, writers block)
SELECT * FROM inventory WHERE product_id = 5 LOCK IN SHARE MODE;
-- Find blocking queries
SELECT
r.trx_id AS waiting_trx,
r.trx_mysql_thread_id AS waiting_thread,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id;
-- Kill a blocking query
KILL 1234; -- thread ID from SHOW FULL PROCESSLIST or above query
Backup and restore
# mysqldump — logical backup
mysqldump -u root -p mydb > mydb.sql # single database
mysqldump -u root -p mydb users orders > partial.sql # specific tables
mysqldump -u root -p --all-databases > all.sql # everything
mysqldump -u root -p --single-transaction mydb > mydb.sql # InnoDB: consistent snapshot without lock
mysqldump -u root -p --no-data mydb > schema.sql # schema only
mysqldump -u root -p --no-create-info mydb > data.sql # data only
# Compress backup
mysqldump -u root -p --single-transaction mydb | gzip > mydb.sql.gz
# Restore
mysql -u root -p mydb < mydb.sql
zcat mydb.sql.gz | mysql -u root -p mydb
# mydumper / myloader — parallel dump/restore (much faster than mysqldump)
mydumper -u root -p pwd -B mydb -o /backup/mydb/ -t 4 --trx-consistency-only
myloader -u root -p pwd -B mydb -d /backup/mydb/ -t 4
# Check binary log position (for replication)
SHOW MASTER STATUS; -- or SHOW BINARY LOG STATUS (MySQL 8.2+)
# Point-in-time recovery
mysqlbinlog --start-datetime="2026-03-14 09:00:00" \
--stop-datetime="2026-03-14 10:30:00" \
/var/lib/mysql/binlog.000042 | mysql -u root -p
Performance schema — diagnostic queries
-- Top 10 slowest queries
SELECT
digest_text,
count_star AS executions,
ROUND(avg_timer_wait / 1e9, 3) AS avg_ms,
ROUND(sum_timer_wait / 1e9, 3) AS total_ms,
ROUND(sum_rows_examined / count_star) AS avg_rows_examined
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC LIMIT 10;
-- Current running queries (> 5 seconds)
SELECT id, user, host, db, command, time, state, LEFT(info, 100) AS query
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 5
ORDER BY time DESC;
-- Table I/O stats — which tables are most accessed?
SELECT object_name, count_read, count_write, count_fetch
FROM performance_schema.table_io_waits_summary_by_table
WHERE object_schema = 'mydb'
ORDER BY count_read + count_write DESC LIMIT 10;
-- Buffer pool usage (InnoDB)
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';
-- Innodb_buffer_pool_read_requests / Innodb_buffer_pool_reads
-- = buffer pool hit ratio — should be > 99%
-- Connection stats
SHOW GLOBAL STATUS LIKE 'Connections'; -- total since start
SHOW GLOBAL STATUS LIKE 'Threads_connected'; -- current
SHOW GLOBAL STATUS LIKE 'Max_used_connections'; -- peak
-- Query cache hit rate (if enabled — deprecated MySQL 8)
SHOW GLOBAL STATUS LIKE 'Qcache%';
Track PostgreSQL EOL dates and releases at ReleaseRun.
Founded
2023 in London, UK
Contact
hello@releaserun.com