Connecting and Login
Default port is 3306. Build a URL or DSN for your host with the MySQL connection string generator.
| Command | What it does |
|---|---|
| Log in to localhost, prompt for the password | |
| Log in and select the shop database | |
| Connect to a remote host and port | |
| Force TCP instead of the local socket | |
| Connect through a specific Unix socket | |
| TLS, checking the certificate and host name | |
| Save credentials in an obfuscated file | |
| Log in with saved credentials | |
| Deprecated; the password shows in the process environment | |
| Run one statement and exit | |
| Who you logged in as and the current database | |
| Connection status: server version, user, TLS, charset | |
| Leave the client (or , or Ctrl+D) |
On Amazon RDS with IAM auth, a rejected token shows up as Access denied for user on RDS MySQL.
Example: save a login path once, then connect to production over TLS without typing the password again.
mysql_config_editor set --login-path=prod --host=db.example.com --user=app --password
mysql --login-path=prod --ssl-mode=VERIFY_IDENTITY --ssl-ca=rds-ca.pem shop
mysql_config_editor print --all # passwords are shown as *****
Databases
| Command | What it does |
|---|---|
| List every database you can see | |
| List databases matching a pattern | |
| Create a database with the server defaults | |
| Create with an explicit charset and collation | |
| Switch the current database | |
| Show the current database | |
| Charset, collation and encryption of a database | |
| Change the default charset for new tables | |
| DestructiveDelete the database and every table in it | |
| DestructiveSame, without an error when it is missing |
There is no . Create the new database and move each table with .
Example: rename the shop database to store by moving every table across, then drop the empty one.
CREATE DATABASE store CHARACTER SET utf8mb4;
RENAME TABLE shop.users TO store.users, shop.orders TO store.orders;
SHOW TABLES FROM shop; -- empty set
DROP DATABASE shop;
Tables
| Command | What it does |
|---|---|
| List tables in the current database | |
| List tables in another database by pattern | |
| List only views | |
| Columns, types, nulls, keys and defaults | |
| Same, plus collation and comments | |
| The full CREATE TABLE statement, one field per line | |
| Engine, row estimate, data and index size | |
| Empty copy with the same columns and indexes | |
| Add a column at a set position | |
| Change a column's type or nullability | |
| Rename a column, keeping its definition | |
| DestructiveRemove a column and its data | |
| Add a foreign key | |
| Rename a table | |
| DestructiveDelete every row and reset AUTO_INCREMENT | |
| DestructiveDelete the table and its data |
Example: the two tables every other example on this page uses.
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
plan ENUM('free', 'pro', 'team') NOT NULL DEFAULT 'free',
credit DECIMAL(10, 2) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
total DECIMAL(10, 2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
meta JSON NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users (id)
);
Users and Privileges
An account is a user name plus a host, so and are two different accounts.
| Command | What it does |
|---|---|
| List users and their auth plugin | |
| Create a user that can log in from any host | |
| Allow logins only from a subnet | |
| Full rights on one database | |
| Read-only on one database | |
| Rights on one table | |
| What a user is allowed to do | |
| Your own privileges | |
| Take rights away | |
| Change a password | |
| Block logins without dropping the user | |
| Rename an account | |
| DestructiveDelete an account | |
| Reload grant tables; only needed after editing them directly |
Example: a read-only reporting user limited to the office network, checked before you hand it out.
CREATE USER 'report'@'10.0.%' IDENTIFIED BY 'Rep0rt!only';
GRANT SELECT ON shop.* TO 'report'@'10.0.%';
SHOW GRANTS FOR 'report'@'10.0.%';
-- GRANT USAGE ON *.* TO `report`@`10.0.%`
-- GRANT SELECT ON `shop`.* TO `report`@`10.0.%`
Insert, Update, Delete
| Command | What it does |
|---|---|
| Insert one row | |
| Insert several rows in one statement | |
| Insert the result of a query | |
| First AUTO_INCREMENT id from your last insert | |
| Skip rows that hit a unique key, as warnings | |
| Upsert: insert, or update the existing row | |
| DestructiveDeletes the old row, then inserts | |
| Update matching rows | |
| Update with a date cutoff | |
| Update with a join | |
| Delete matching rows | |
| Delete in batches to keep locks short | |
| Delete with a join |
The function in still works but is deprecated. Use a row alias () after , or a derived table after .
Example: record a user's daily order count with one upsert, safe to run again for the same day.
CREATE TABLE daily_orders (
user_id BIGINT UNSIGNED, day DATE, orders INT, PRIMARY KEY (user_id, day)
);
INSERT INTO daily_orders (user_id, day, orders)
SELECT * FROM (
SELECT user_id, DATE(created_at) AS day, COUNT(*) AS n FROM orders
WHERE created_at >= CURDATE() GROUP BY user_id, DATE(created_at)
) AS t
ON DUPLICATE KEY UPDATE orders = t.n;
Select and Filtering
| Command | What it does |
|---|---|
| Filter rows by value | |
| Match any value in a list | |
| Pattern match; any run, one character | |
| Exclude a pattern | |
| Regular expression match | |
| Inclusive range on both ends | |
| NULL check; never matches | |
| Not equal ( also works) | |
| Sort by several columns | |
| Page 3 of 20 rows | |
| Unique values | |
| Count and sum per group | |
| Filter groups after aggregating | |
| Add a grand total row | |
| Conditional column | |
| Rows that have a match in another table |
Example: the top five customers by October spend, using a CTE ().
WITH october AS (
SELECT user_id, SUM(total) AS spent, COUNT(*) AS orders
FROM orders
WHERE status = 'paid' AND created_at >= '2026-10-01' AND created_at < '2026-11-01'
GROUP BY user_id
)
SELECT u.email, o.spent, o.orders
FROM october o JOIN users u ON u.id = o.user_id
ORDER BY o.spent DESC
LIMIT 5;
Joins and Unions
| Command | What it does |
|---|---|
| Inner join: rows with a match on both sides | |
| Every user, orders or NULL | |
| Users with no orders | |
| Every user; same as the LEFT JOIN flipped | |
| Join on a column both tables share | |
| Every combination of rows | |
| Combine results, removing duplicates | |
| Combine and keep duplicates; faster | |
| Rows in both results (8.0.31+) | |
| Rows in the first result only (8.0.31+) |
MySQL has no . Combine a LEFT and a RIGHT join with .
Example: a full outer join between users and leads on email.
SELECT u.email, l.source FROM users u LEFT JOIN leads l ON l.email = u.email
UNION
SELECT l.email, l.source FROM users u RIGHT JOIN leads l ON l.email = u.email;
Common Functions
| Command | What it does |
|---|---|
| Current datetime, date and UTC datetime | |
| Format a date; is minutes, is month | |
| Days between two dates (30) | |
| Difference in any unit | |
| Date arithmetic | |
| First non-NULL value | |
| Replace NULL with a default | |
| Inline if/else | |
| Join strings; NULL if any part is NULL | |
| Part of a string, 1-based | |
| Everything after the last @ | |
| Join a group's values into one string | |
| Round to 2 decimal places |
Example: a monthly revenue report with formatted months and a comma-separated list of statuses.
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month,
ROUND(SUM(total), 2) AS revenue,
GROUP_CONCAT(DISTINCT status ORDER BY status SEPARATOR ', ') AS statuses
FROM orders
GROUP BY month
ORDER BY month DESC;
Import, Export and Backup
| Command | What it does |
|---|---|
| Back up one database without locking InnoDB tables | |
| Include procedures, triggers and events | |
| Back up several databases, with CREATE DATABASE | |
| Back up the whole server | |
| Schema only, no rows | |
| One table, filtered rows | |
| Compressed backup | |
| Import a .sql file into a database | |
| Import a compressed dump | |
| Run a .sql file from inside the client | |
| Write a CSV on the server host | |
| The only folder INTO OUTFILE and LOAD DATA may use | |
| Load a CSV from your machine |
needs on the server and on the client.
In 1bench: the MySQL GUI exports a table, with your current filter and sort, to CSV, JSON, NDJSON or SQL on your own machine, so there is no INTO OUTFILE path or secure_file_priv folder to fight, and it imports CSV, JSON and SQL files the same way.
Example: copy a production database to staging in one pipe, without writing a dump file.
mysqldump --login-path=prod --single-transaction --routines --triggers shop \
| mysql --login-path=staging shop_staging
CLI Output and Options
| Command | What it does |
|---|---|
| Print each column on its own line | |
| Tab-separated output, no borders | |
| Same, without the header row | |
| Table borders even when output is piped | |
| HTML table output ( for XML) | |
| Rough CSV; breaks on values with commas or tabs | |
| Page wide results with horizontal scroll | |
| Same, from inside the client; turns it off | |
| Copy everything you run and see to a file | |
| Echo each statement before its result | |
| Refuse UPDATE and DELETE without a key in WHERE | |
| Warnings from the last statement | |
| Built-in syntax help for any statement |
Your typed history is in . Lines that look like or are left out of it.
Example: export a query to CSV from the shell, with a header row, for a spreadsheet.
mysql --login-path=prod -B -e "
SELECT id, user_id, total, status, created_at FROM shop.orders
WHERE created_at >= '2026-10-01'" | tr '\t' ',' > orders-october.csv
head -2 orders-october.csv # id,user_id,total,status,created_at
Indexes and EXPLAIN
| Command | What it does |
|---|---|
| Every index, column order and cardinality | |
| Add a composite index | |
| Add a unique index | |
| Add an index, fail rather than block writes | |
| Full-text index for MATCH ... AGAINST | |
| Hide an index from the optimizer to test dropping it | |
| Remove an index | |
| The plan: index used, rows estimated | |
| The plan as an indented tree | |
| Run the query and show real times and row counts | |
| Make the optimizer use an index | |
| Refresh index statistics |
In EXPLAIN output, means a full table scan and means no index was used.
Example: check that a user's order history uses the composite index, then confirm with real timings.
CREATE INDEX idx_orders_user ON orders (user_id, created_at);
EXPLAIN SELECT id, total FROM orders WHERE user_id = 7 ORDER BY created_at DESC LIMIT 10;
-- type: ref, key: idx_orders_user, Extra: Backward index scan
EXPLAIN ANALYZE SELECT id, total FROM orders WHERE user_id = 7 ORDER BY created_at DESC LIMIT 10;
Processes and Transactions
| Command | What it does |
|---|---|
| Running connections, truncated statements | |
| Same, full statement text | |
| Busy connections, longest first | |
| Stop the running statement, keep the connection | |
| DestructiveClose the connection and roll back its transaction | |
| Your own connection id | |
| Begin a transaction ( also works) | |
| Save the transaction | |
| Undo everything since START TRANSACTION | |
| Mark a point to roll back to | |
| Undo only back to the savepoint | |
| Lock the row until commit | |
| Change isolation for this session | |
| Last deadlock, lock waits, buffer pool stats | |
| Current connection count | |
| Connection limit before "Too many connections" |
In 1bench: the MySQL GUI cancels a running query from the tab, sending KILL QUERY on a side connection, so you do not need to look up the thread id first.
Example: move credit between two users atomically, locking both rows first.
START TRANSACTION;
SELECT id, credit FROM users WHERE id IN (1, 2) FOR UPDATE;
UPDATE users SET credit = credit - 25.00 WHERE id = 1;
UPDATE users SET credit = credit + 25.00 WHERE id = 2;
COMMIT;
JSON
| Command | What it does |
|---|---|
| Read a JSON value, quoted () | |
| Read it unquoted () | |
| Long form of , array index from 0 | |
| Filter on a JSON field | |
| Array contains a value | |
| Set or add a key | |
| Remove a key | |
| Build a JSON object from columns | |
| Aggregate rows into a JSON array | |
| Index a JSON field through a generated column |
Example: turn each order's items array into rows with to count units sold per SKU.
SELECT jt.sku, SUM(jt.qty) AS units
FROM orders,
JSON_TABLE(meta, '$.items[*]' COLUMNS (sku VARCHAR(40) PATH '$.sku', qty INT PATH '$.qty')) AS jt
GROUP BY jt.sku
ORDER BY units DESC;
TiDB Notes
TiDB speaks the MySQL protocol on port 4000, so the client and the SQL above work. These rows are TiDB only.
| Command | What it does |
|---|---|
| Connect to a local TiDB (default port 4000) | |
| TiDB version and build details | |
| Read the table as it was 5 minutes ago | |
| Random ids that spread writes across nodes | |
| Delete rows automatically after 90 days | |
| Bulk load CSV files fast | |
| Optimizer hint to pick an index | |
| Allow one query up to 8 GiB of memory | |
| Slowest recent statements | |
| Check SQL mode, which TiDB keeps MySQL compatible |
Example: after a bad delete, read the rows as they were before it ran. Stale reads only reach back as far as the GC window (, 10 minutes by default).
SELECT COUNT(*) FROM orders WHERE user_id = 7; -- 0 after the bad delete
SELECT * FROM orders AS OF TIMESTAMP NOW() - INTERVAL 5 MINUTE
WHERE user_id = 7; -- the rows as they were
Gotchas
- or without a clause changes every row, and autocommit is on by default, so there is no undo. Start the client with , or run first and check the row count before .
- is not an upsert. It deletes the old row and inserts a new one, which fires delete triggers, cascades foreign keys and drops any column you left out back to its default. Use .
- without locks tables while it runs. With it, InnoDB gets a consistent snapshot, but MyISAM tables are still not consistent.
- in MySQL is an alias for , which cannot store emoji or other 4-byte characters. Use .
- (no space) puts the password in shell history and process lists, and (with a space) treats Secret as the database name. Use alone or a login path.
- on a connection running a large write rolls back everything it did, which can take as long as the write itself. Use when you only want the statement stopped.