I’ve been working with MySQL for years now, and if there’s one thing I’ve learned, it’s that nobody actually memorizes every command. Even after writing thousands of queries, I still find myself pausing to double-check syntax for a JOIN clause or a window function I don’t use every day. So I finally sat down and put together the cheat sheet I wish I’d had when I started — the one I actually keep open in a browser tab while I work.
This isn’t a textbook. It’s a reference. I’ve organized it so you can jump straight to the section you need, copy the syntax, adjust it, and get back to work. Whether you’re a backend developer, a data analyst, a DBA, or someone prepping for an interview, this guide covers the commands you’ll reach for daily and the edge cases that trip people up.
Let’s get into it.
Table of Contents
- Getting Started: Connecting to MySQL
- Database Management Commands
- Table Management Commands
- Data Types Reference
- CRUD Operations: Insert, Select, Update, Delete
- Filtering and Sorting Data
- Joins Explained with Examples
- Aggregate Functions and Grouping
- Subqueries and Common Table Expressions
- Indexes and Performance
- Views, Stored Procedures, and Triggers
- Transactions and Locking
- User Management and Security
- Backup and Restore
- Troubleshooting Common Errors
- Best Practices
- Real-World Use Cases
- Frequently Asked Questions
- Common Mistakes to Avoid
- Interview Questions
- Printable Quick-Reference Summary
- Official Documentation Links
1. Getting Started: Connecting to MySQL
Before you can run a single query, you need to connect. I still use the command line for quick tasks even though I have GUI tools installed, because it’s faster once your fingers know the commands.
# Connect as root, prompted for password
mysql -u root -p
# Connect to a specific host and port
mysql -h 127.0.0.1 -P 3306 -u myuser -p
# Connect directly to a database
mysql -u myuser -p mydatabase
# Run a single query from the terminal without entering the shell
mysql -u myuser -p -e "SHOW DATABASES;"
Once you’re inside the MySQL shell, you’ll see a prompt like mysql>. From here, every command (except a few shell helpers) needs to end with a semicolon.
-- Check your MySQL version
SELECT VERSION();
-- Check current user
SELECT CURRENT_USER();
-- Check which database you're using
SELECT DATABASE();
-- Exit the shell
EXIT;
-- or
QUIT;
2. Database Management Commands
| Task | Command |
|---|---|
| List all databases | SHOW DATABASES; |
| Create a new database | CREATE DATABASE dbname; |
| Create only if it doesn’t exist | CREATE DATABASE IF NOT EXISTS dbname; |
| Switch to a database | USE dbname; |
| Delete a database | DROP DATABASE dbname; |
| Delete safely | DROP DATABASE IF EXISTS dbname; |
| Show current database | SELECT DATABASE(); |
| Show database creation statement | SHOW CREATE DATABASE dbname; |
A quick example I use when spinning up a new project:
CREATE DATABASE IF NOT EXISTS ecommerce_app
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE ecommerce_app;
I always specify utf8mb4 explicitly. The default charset on older MySQL installs can still be latin1, and that becomes a painful migration later if you’re storing emojis, names with accents, or multilingual content.
3. Table Management Commands
Creating and modifying tables is where you’ll spend a lot of your schema-design time.
-- Basic table creation
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Expected output:
Query OK, 0 rows affected (0.03 sec)
Other table commands I use constantly:
-- List all tables in the current database
SHOW TABLES;
-- Describe a table's structure
DESCRIBE users;
-- or shorthand
DESC users;
-- Show the exact CREATE statement (great for docs or migrations)
SHOW CREATE TABLE users;
-- Rename a table
RENAME TABLE users TO app_users;
-- Delete a table permanently
DROP TABLE users;
-- Delete only if it exists
DROP TABLE IF EXISTS users;
-- Empty a table but keep the structure (fast, resets AUTO_INCREMENT)
TRUNCATE TABLE users;
Altering Tables
-- Add a column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Add a column with a position
ALTER TABLE users ADD COLUMN middle_name VARCHAR(50) AFTER username;
-- Modify a column's type
ALTER TABLE users MODIFY COLUMN phone VARCHAR(30);
-- Rename a column (MySQL 8.0+)
ALTER TABLE users RENAME COLUMN phone TO phone_number;
-- Drop a column
ALTER TABLE users DROP COLUMN middle_name;
-- Add a foreign key
ALTER TABLE orders
ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE;
-- Add an index
ALTER TABLE users ADD INDEX idx_email (email);
-- Drop an index
ALTER TABLE users DROP INDEX idx_email;
I lean on ALTER TABLE constantly during early development, but on production tables with millions of rows, I always test the migration on a staging copy first — some ALTER operations lock the table and can take down a busy app for minutes.
4. Data Types Reference
Picking the right data type matters more than people think — it affects storage size, query speed, and data integrity.
| Category | Type | Notes |
|---|---|---|
| Integer | TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT | Use the smallest type that fits your range |
| Decimal | DECIMAL(M,D) | Exact precision — use for money |
| Float | FLOAT, DOUBLE | Approximate — avoid for currency |
| String | VARCHAR(n), CHAR(n) | VARCHAR for variable length, CHAR for fixed |
| Text | TEXT, MEDIUMTEXT, LONGTEXT | For large blocks of text |
| Binary | BLOB, MEDIUMBLOB, LONGBLOB | For binary data like files |
| Date/Time | DATE, DATETIME, TIMESTAMP, TIME, YEAR | TIMESTAMP is timezone-aware, DATETIME is not |
| Boolean | BOOLEAN (alias for TINYINT(1)) | Stores 0 or 1 |
| JSON | JSON | Native JSON support since MySQL 5.7 |
| Enum | ENUM('a','b','c') | Restricts values to a fixed list |
A note from experience: I default to DECIMAL(10,2) for any money field. I learned the hard way that FLOAT rounding errors can silently corrupt financial totals.
5. CRUD Operations: Insert, Select, Update, Delete
Insert
-- Insert a single row
INSERT INTO users (username, email, password_hash)
VALUES ('johndoe', 'john@example.com', 'hashed_pw_here');
-- Insert multiple rows in one statement
INSERT INTO users (username, email, password_hash) VALUES
('alice', 'alice@example.com', 'hash1'),
('bob', 'bob@example.com', 'hash2'),
('carol', 'carol@example.com', 'hash3');
-- Insert or update if duplicate key exists
INSERT INTO users (id, username, email, password_hash)
VALUES (1, 'johndoe', 'john@example.com', 'hash')
ON DUPLICATE KEY UPDATE email = VALUES(email);
Expected output:
Query OK, 1 row affected (0.01 sec)
Select
-- Select everything (avoid in production code, be explicit)
SELECT * FROM users;
-- Select specific columns
SELECT username, email FROM users;
-- Select with alias
SELECT username AS name, email AS contact FROM users;
-- Distinct values
SELECT DISTINCT country FROM users;
-- Limit results
SELECT * FROM users LIMIT 10;
-- Pagination
SELECT * FROM users LIMIT 10 OFFSET 20;
Update
-- Update matching rows
UPDATE users
SET email = 'newemail@example.com'
WHERE id = 1;
-- Update multiple columns
UPDATE users
SET email = 'new@example.com', updated_at = NOW()
WHERE username = 'johndoe';
I always write the WHERE clause first in my head before typing UPDATE. Running an update without a WHERE clause updates every row in the table — I’ve seen this happen to a teammate on a production database, and it’s not fun to fix at 2 a.m.
Delete
-- Delete specific rows
DELETE FROM users WHERE id = 5;
-- Delete with a condition on a joined table
DELETE u FROM users u
JOIN inactive_flags f ON u.id = f.user_id
WHERE f.flagged = 1;
-- Delete everything (dangerous, use TRUNCATE instead if intentional)
DELETE FROM users;
6. Filtering and Sorting Data
-- Basic WHERE
SELECT * FROM orders WHERE status = 'shipped';
-- Multiple conditions
SELECT * FROM orders WHERE status = 'shipped' AND total > 100;
-- OR conditions
SELECT * FROM orders WHERE status = 'shipped' OR status = 'delivered';
-- IN operator
SELECT * FROM orders WHERE status IN ('shipped', 'delivered', 'processing');
-- BETWEEN
SELECT * FROM orders WHERE total BETWEEN 50 AND 200;
-- LIKE pattern matching
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE username LIKE 'j%';
-- NULL checks
SELECT * FROM users WHERE phone IS NULL;
SELECT * FROM users WHERE phone IS NOT NULL;
-- Sorting
SELECT * FROM orders ORDER BY created_at DESC;
SELECT * FROM orders ORDER BY total ASC, created_at DESC;
7. Joins Explained with Examples
Joins confuse people early on, so I like to think of them visually: you’re combining rows from two or more tables based on a related column.
-- INNER JOIN: only matching rows from both tables
SELECT o.id, u.username, o.total
FROM orders o
INNER JOIN users u ON o.user_id = u.id;
-- LEFT JOIN: all rows from the left table, matched rows from the right
SELECT u.username, o.id AS order_id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- RIGHT JOIN: all rows from the right table
SELECT u.username, o.id AS order_id
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- FULL OUTER JOIN (MySQL doesn't support this directly — simulate with UNION)
SELECT u.username, o.id
FROM users u LEFT JOIN orders o ON u.id = o.user_id
UNION
SELECT u.username, o.id
FROM users u RIGHT JOIN orders o ON u.id = o.user_id;
-- SELF JOIN: joining a table to itself
SELECT e.name AS employee, m.name AS manager
FROM employees e
JOIN employees m ON e.manager_id = m.id;
-- CROSS JOIN: every combination of rows
SELECT colors.name, sizes.name
FROM colors CROSS JOIN sizes;
I use LEFT JOIN far more often than INNER JOIN in reporting queries, because I usually want to see everything from the “main” table even when there’s no match — like showing every user even if they haven’t placed an order yet.
8. Aggregate Functions and Grouping
SELECT COUNT(*) FROM orders;
SELECT SUM(total) FROM orders;
SELECT AVG(total) FROM orders;
SELECT MIN(total), MAX(total) FROM orders;
-- Group by with aggregates
SELECT user_id, COUNT(*) AS order_count, SUM(total) AS total_spent
FROM orders
GROUP BY user_id;
-- Filter groups with HAVING (not WHERE)
SELECT user_id, SUM(total) AS total_spent
FROM orders
GROUP BY user_id
HAVING total_spent > 500;
-- Group by multiple columns
SELECT status, DATE(created_at) AS order_date, COUNT(*) AS total
FROM orders
GROUP BY status, DATE(created_at);
A distinction that trips up a lot of beginners: WHERE filters rows before grouping, HAVING filters groups after aggregation. You can’t use an aggregate function like SUM() inside WHERE.
9. Subqueries and Common Table Expressions
-- Subquery in WHERE
SELECT username FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);
-- Subquery in SELECT
SELECT username,
(SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) AS order_count
FROM users;
-- Correlated subquery
SELECT * FROM orders o
WHERE total > (SELECT AVG(total) FROM orders WHERE user_id = o.user_id);
-- Common Table Expression (CTE) — MySQL 8.0+
WITH high_value_orders AS (
SELECT user_id, SUM(total) AS total_spent
FROM orders
GROUP BY user_id
HAVING SUM(total) > 1000
)
SELECT u.username, h.total_spent
FROM users u
JOIN high_value_orders h ON u.id = h.user_id;
-- Recursive CTE (great for hierarchical data like org charts)
WITH RECURSIVE subordinates AS (
SELECT id, name, manager_id FROM employees WHERE id = 1
UNION ALL
SELECT e.id, e.name, e.manager_id
FROM employees e
JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;
CTEs made a real difference for me once MySQL 8.0 rolled out. Before that, I was nesting subqueries three levels deep and it was miserable to debug. Now I write the logical steps top to bottom and it reads almost like a story.
10. Indexes and Performance
-- Create a basic index
CREATE INDEX idx_username ON users(username);
-- Composite index (order matters)
CREATE INDEX idx_status_date ON orders(status, created_at);
-- Unique index
CREATE UNIQUE INDEX idx_email ON users(email);
-- Full-text index (for text search)
CREATE FULLTEXT INDEX idx_content ON articles(content);
-- Show indexes on a table
SHOW INDEX FROM users;
-- Drop an index
DROP INDEX idx_username ON users;
-- Analyze a query's execution plan
EXPLAIN SELECT * FROM orders WHERE status = 'shipped';
-- More detailed analysis (MySQL 8.0.18+)
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'shipped';
My rule of thumb: index columns you filter (WHERE), join (ON), or sort (ORDER BY) on frequently. Don’t over-index — every index speeds up reads but slows down writes, since MySQL has to update the index on every INSERT, UPDATE, or DELETE.
Other performance habits I stick to:
- Avoid
SELECT *in application code — fetch only the columns you need. - Use
LIMITwhen you don’t need the full result set. - Batch large
INSERToperations instead of looping one row at a time. - Watch out for functions on indexed columns in
WHEREclauses (e.g.,WHERE YEAR(created_at) = 2024) — they prevent MySQL from using the index.
11. Views, Stored Procedures, and Triggers
-- Create a view
CREATE VIEW active_users AS
SELECT id, username, email FROM users WHERE status = 'active';
-- Query a view like a table
SELECT * FROM active_users;
-- Drop a view
DROP VIEW active_users;
-- Stored procedure
DELIMITER //
CREATE PROCEDURE GetUserOrders(IN userId INT)
BEGIN
SELECT * FROM orders WHERE user_id = userId;
END //
DELIMITER ;
-- Call it
CALL GetUserOrders(5);
-- Trigger example
DELIMITER //
CREATE TRIGGER before_order_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
SET NEW.created_at = NOW();
END //
DELIMITER ;
-- Drop a procedure or trigger
DROP PROCEDURE IF EXISTS GetUserOrders;
DROP TRIGGER IF EXISTS before_order_insert;
I reach for views mostly to simplify reporting queries that get reused across a dashboard. Stored procedures I use more sparingly now — they’re powerful, but they push logic into the database layer, which can make version control and testing harder for a team used to application-side code.
12. Transactions and Locking
-- Start a transaction
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- Commit if everything succeeded
COMMIT;
-- Roll back if something went wrong
ROLLBACK;
-- Set isolation level
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Lock a table explicitly
LOCK TABLES accounts WRITE;
-- ... do work ...
UNLOCK TABLES;
Transactions are non-negotiable for anything involving money or multi-step data changes. I never write a multi-table update without wrapping it in START TRANSACTION / COMMIT, because a crash halfway through can leave your data in an inconsistent state otherwise.
13. User Management and Security
-- Create a new user
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'StrongPassword123!';
-- Grant specific privileges
GRANT SELECT, INSERT, UPDATE ON ecommerce_app.* TO 'appuser'@'localhost';
-- Grant all privileges (use sparingly)
GRANT ALL PRIVILEGES ON ecommerce_app.* TO 'admin_user'@'localhost';
-- Apply privilege changes
FLUSH PRIVILEGES;
-- Show a user's privileges
SHOW GRANTS FOR 'appuser'@'localhost';
-- Revoke privileges
REVOKE INSERT ON ecommerce_app.* FROM 'appuser'@'localhost';
-- Change a user's password
ALTER USER 'appuser'@'localhost' IDENTIFIED BY 'NewStrongerPassword456!';
-- Delete a user
DROP USER 'appuser'@'localhost';
Security tips I follow on every project:
- Never connect your application with the
rootaccount. - Use least-privilege grants — give each app user only what it needs.
- Always use parameterized queries or prepared statements in application code to prevent SQL injection. Never concatenate raw user input into a query string.
- Rotate passwords and audit
SHOW GRANTSoutput periodically. - Restrict remote root login (
root@'%') — bind it tolocalhostunless you have a specific reason not to. - Enable SSL/TLS for connections over untrusted networks.
14. Backup and Restore
# Backup a single database
mysqldump -u root -p ecommerce_app > backup.sql
# Backup all databases
mysqldump -u root -p --all-databases > full_backup.sql
# Backup a specific table
mysqldump -u root -p ecommerce_app users > users_backup.sql
# Restore from a backup file
mysql -u root -p ecommerce_app < backup.sql
# Backup with compression
mysqldump -u root -p ecommerce_app | gzip > backup.sql.gz
# Restore a compressed backup
gunzip < backup.sql.gz | mysql -u root -p ecommerce_app
I schedule automated mysqldump backups on a cron job for smaller projects, and for larger production systems I pair logical backups with binary log replication so I can do point-in-time recovery if needed.
15. Troubleshooting Common Errors
| Error | Likely Cause | Fix |
|---|---|---|
ERROR 1045: Access denied for user | Wrong username/password | Double-check credentials, reset password if needed |
ERROR 1049: Unknown database | Database doesn’t exist or typo | SHOW DATABASES; to confirm the name |
ERROR 1062: Duplicate entry | Violates a unique constraint | Check existing data or use ON DUPLICATE KEY UPDATE |
ERROR 1064: SQL syntax error | Typo or wrong keyword order | Read the error position carefully, check quotes and commas |
ERROR 1146: Table doesn't exist | Wrong table name or wrong database selected | Run USE dbname; and SHOW TABLES; |
ERROR 1215: Cannot add foreign key constraint | Column types mismatch or missing index | Ensure both columns have the same type and the referenced column is indexed |
ERROR 2002: Can't connect to local MySQL server | MySQL service isn’t running | Start the service (sudo service mysql start) |
ERROR 1698: Access denied for user 'root'@'localhost' | Auth plugin mismatch | Use ALTER USER to set mysql_native_password or reset via safe mode |
Table is Lock wait timeout exceeded | Long-running transaction holding a lock | Find and kill the blocking query with SHOW PROCESSLIST; |
-- Find what's currently running (helpful for locks/slow queries)
SHOW PROCESSLIST;
-- Kill a specific process
KILL 1234;
-- Check for slow queries
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
16. Best Practices
- Always back up before running schema migrations on production.
- Use
EXPLAINbefore optimizing — don’t guess at what’s slow. - Normalize your schema, but don’t be afraid to denormalize for read-heavy reporting tables when it makes sense.
- Name your constraints and indexes explicitly instead of letting MySQL auto-generate names — it makes debugging error messages much easier.
- Use
utf8mb4for new projects, always. - Keep transactions short — long-running transactions hold locks and hurt concurrency.
- Version-control your schema changes with a migration tool (Flyway, Liquibase, or a framework’s built-in migrations).
- Set sensible
NOT NULLand default value constraints at the database level — don’t rely solely on application code for data integrity.
17. Real-World Use Cases
E-commerce order tracking: I use a LEFT JOIN between users and orders combined with GROUP BY and SUM() to build a “top customers by lifetime spend” report for marketing teams.
Content search: For a blog platform, I’ve used FULLTEXT indexes with MATCH() AGAINST() to power an in-app search bar without needing a separate search engine for a small-to-medium dataset.
Audit logging: I’ve set up AFTER UPDATE triggers that write old and new values into a separate audit_log table, which has saved me more than once when tracking down who changed what and when.
Dashboard reporting: Views combined with scheduled mysqldump exports have let me hand off read-only reporting access to analysts without giving them access to the live production schema.
18. Frequently Asked Questions
What’s the difference between DELETE and TRUNCATE? DELETE removes rows one at a time, can be filtered with WHERE, and can be rolled back inside a transaction. TRUNCATE removes all rows at once, resets AUTO_INCREMENT, and is generally not transaction-safe in the same way.
What’s the difference between VARCHAR and TEXT? VARCHAR is stored inline with the row and has a defined max length, making it faster for indexing and sorting. TEXT is meant for larger content and has some storage and indexing limitations.
How do I find duplicate rows?
SELECT email, COUNT(*)
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
How do I copy a table’s structure without the data?
CREATE TABLE users_copy LIKE users;
How do I copy both structure and data?
CREATE TABLE users_copy AS SELECT * FROM users;
Why isn’t my index being used? Common reasons: a function is wrapping the indexed column in the WHERE clause, the column has low cardinality (like a boolean), or the optimizer decided a full scan is cheaper for a small table.
Is MySQL case-sensitive? Table names can be case-sensitive depending on the OS and filesystem. String comparisons depend on the collation — utf8mb4_general_ci is case-insensitive, while a _bin collation is case-sensitive.
19. Common Mistakes to Avoid
- Running
UPDATEorDELETEwithout aWHEREclause. - Storing passwords in plain text instead of using a proper hashing algorithm at the application layer.
- Using
FLOATfor currency values. - Forgetting to index foreign key columns.
- Overusing
SELECT *in production code. - Not setting a character set/collation explicitly and inheriting inconsistent defaults across tables.
- Ignoring
EXPLAINoutput and guessing at performance fixes. - Leaving the default
rootaccount accessible remotely. - Mixing business logic heavily into triggers, making the system hard to reason about.
20. Interview Questions
- What’s the difference between
INNER JOINandLEFT JOIN? - Explain ACID properties in the context of MySQL transactions.
- What’s the difference between a clustered and non-clustered index? Does InnoDB use clustered indexes?
- How would you optimize a slow query?
- What’s the difference between
HAVINGandWHERE? - Explain normalization and name the first three normal forms.
- What’s the difference between
CHARandVARCHAR? - How does a
FOREIGN KEYconstraint withON DELETE CASCADEbehave? - What isolation levels does MySQL support, and what’s the default for InnoDB?
- How would you design a schema for a many-to-many relationship?
- What’s the difference between
COMMITandROLLBACK? - How do you prevent SQL injection in application code?
21. Printable Quick-Reference Summary
DATABASES CREATE DATABASE db; DROP DATABASE db; USE db; SHOW DATABASES;
TABLES CREATE TABLE t (...); DROP TABLE t; DESCRIBE t; SHOW TABLES;
ALTER ALTER TABLE t ADD/DROP/MODIFY COLUMN ...;
INSERT INSERT INTO t (cols) VALUES (...);
SELECT SELECT cols FROM t WHERE ... ORDER BY ... LIMIT ...;
UPDATE UPDATE t SET col = val WHERE ...;
DELETE DELETE FROM t WHERE ...;
JOINS INNER JOIN / LEFT JOIN / RIGHT JOIN / CROSS JOIN ... ON ...;
AGGREGATE COUNT() SUM() AVG() MIN() MAX() ... GROUP BY ... HAVING ...;
INDEXES CREATE INDEX idx ON t(col); DROP INDEX idx ON t;
TRANSACTIONS START TRANSACTION; COMMIT; ROLLBACK;
USERS CREATE USER; GRANT; REVOKE; FLUSH PRIVILEGES;
BACKUP mysqldump -u user -p db > file.sql
RESTORE mysql -u user -p db < file.sql
Keep this section bookmarked — it’s the 20% of commands you’ll use 80% of the time.
22. Official Documentation Links
- MySQL Reference Manual: https://dev.mysql.com/doc/refman/en/
- MySQL 8.0 Reference Manual: https://dev.mysql.com/doc/refman/8.0/en/
- mysqldump Documentation: https://dev.mysql.com/doc/refman/en/mysqldump.html
- MySQL Data Types: https://dev.mysql.com/doc/refman/en/data-types.html
- MySQL Security Guidelines: https://dev.mysql.com/doc/refman/en/security.html
I keep updating this sheet as MySQL evolves and as I run into new edge cases myself — if you bookmark it, you’re bookmarking something that reflects real, hands-on usage rather than just a copy of the manual.
