MySQL Create User
Every connection to MySQL logs in as an account. Using root for everything is risky: one leaked password or one buggy query can damage every database. CREATE USER creates separate accounts for each application and person, and GRANT gives each one only the rights it needs.
1. Accounts Are user@host
CREATE USER [IF NOT EXISTS] 'user_name'@'host'
IDENTIFIED BY 'password'
[DEFAULT ROLE role]
[WITH MAX_QUERIES_PER_HOUR n ...] -- resource limits come before password options
[PASSWORD EXPIRE INTERVAL n DAY]
[FAILED_LOGIN_ATTEMPTS n PASSWORD_LOCK_TIME n]
[ACCOUNT LOCK | ACCOUNT UNLOCK];
2. Creating a User
CREATE USER 'shop_app'@'localhost' IDENTIFIED BY 'Sh0p#App2026';
SELECT user, host, plugin, account_locked FROM mysql.user WHERE user = 'shop_app';
Result: 1 row
| user | host | plugin | account_locked |
|---|---|---|---|
| shop_app | localhost | caching_sha2_password | N |
caching_sha2_password plugin by default. Very old client libraries may need an upgrade to connect.3. A New User Has No Privileges
SHOW GRANTS FOR 'shop_app'@'localhost';
Result: 1 row
| Grants for shop_app@localhost |
|---|
| GRANT USAGE ON *.* TO `shop_app`@`localhost` |
USAGE means "can log in, nothing more". Logged in as shop_app, reading the orders table fails:
SELECT * FROM shop.orders;
ERROR 1142 (42000): SELECT command denied to user 'shop_app'@'localhost' for table 'orders'
4. Granting Privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_app'@'localhost';
SHOW GRANTS FOR 'shop_app'@'localhost';
Result: 2 rows
| Grants for shop_app@localhost |
|---|
| GRANT USAGE ON *.* TO `shop_app`@`localhost` |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `shop`.* TO `shop_app`@`localhost` |
SELECT * FROM shop.orders;
Result: 2 rows
| id | total |
|---|---|
| 1 | 499.00 |
| 2 | 1299.00 |
DROP TABLE shop.orders;
ERROR 1142 (42000): DROP command denied to user 'shop_app'@'localhost' for table 'orders'
| Level | Syntax | Example |
|---|---|---|
| Global | ON *.* | Administrators only |
| Database | ON shop.* | An application's own database |
| Table | ON shop.orders | A reporting user for one table |
| Column | GRANT SELECT (id, total) ON shop.orders | Hide sensitive columns |
5. IF NOT EXISTS and Duplicate Users
CREATE USER 'shop_app'@'localhost' IDENTIFIED BY 'x';
ERROR 1396 (HY000): Operation CREATE USER failed for 'shop_app'@'localhost'
CREATE USER IF NOT EXISTS 'shop_app'@'localhost' IDENTIFIED BY 'x';
SHOW WARNINGS;
Query OK, 0 rows affected, 1 warning
Result: 1 row
| Level | Code | Message |
|---|---|---|
| Note | 3163 | Authorization ID 'shop_app'@'localhost' already exists. |
Note that the same name with another host is a different account:
CREATE USER 'shop_app'@'10.0.0.%' IDENTIFIED BY 'Sh0p#Net2026';
SELECT user, host FROM mysql.user WHERE user = 'shop_app' ORDER BY host;
Result: 2 rows
| user | host |
|---|---|
| shop_app | 10.0.0.% |
| shop_app | localhost |
6. Roles: Reusable Groups of Privileges
Instead of granting the same list to every analyst, create a role once and grant the role:
CREATE ROLE 'reader';
GRANT SELECT ON shop.* TO 'reader';
CREATE USER 'meera'@'localhost' IDENTIFIED BY 'Me3ra#2026' DEFAULT ROLE 'reader';
GRANT 'reader' TO 'meera'@'localhost';
SHOW GRANTS FOR 'meera'@'localhost' USING 'reader';
Result: 3 rows
| Grants for meera@localhost |
|---|
| GRANT USAGE ON *.* TO `meera`@`localhost` |
| GRANT SELECT ON `shop`.* TO `meera`@`localhost` |
| GRANT `reader`@`%` TO `meera`@`localhost` |
7. Password Expiry, Lockout and Limits
CREATE USER 'intern'@'localhost' IDENTIFIED BY 'Int3rn#2026'
WITH MAX_QUERIES_PER_HOUR 500
PASSWORD EXPIRE INTERVAL 90 DAY
FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 1;
SELECT user, password_lifetime, max_questions FROM mysql.user WHERE user = 'intern';
Result: 1 row
| user | password_lifetime | max_questions |
|---|---|---|
| intern | 90 | 500 |
ALTER USER 'intern'@'localhost' ACCOUNT LOCK;
SELECT user, account_locked FROM mysql.user WHERE user = 'intern';
Result: 1 row
| user | account_locked |
|---|---|
| intern | Y |
A locked account can't log in until ALTER USER … ACCOUNT UNLOCK.
8. Best Practices
- One account per application and per person; never share root.
- Restrict the host:
'app'@'10.0.0.%'rather than'%'. - Grant the minimum privileges (least privilege), at database or table level.
- Use roles for teams; use strong passwords and consider the
validate_passwordcomponent. - Store application passwords in a secrets manager or environment variables, not in source code.
9. Summary
- An account is
'user'@'host';CREATE USER … IDENTIFIED BYcreates it with a password. - New accounts have no privileges (USAGE); add them with
GRANT … ON db.* TO …. - Roles bundle privileges; policies add expiry, lockout and limits.
- Follow least privilege and restrict hosts.