加载中...

加载中...

MySQL 是 Web 应用里最常打交道的数据库。它的坑大多不在语法,而在配置和设计:字符集选错存不了 emoji、金额用了 FLOATGROUP BY 突然报错、锁等待查不出是谁卡着。这篇从安装配置讲到索引优化、事务锁、慢查询和主从复制。命令行客户端工具部分见 MySQL 命令行使用教程,SQL 语法见 SQL 命令使用教程

一、安装与初始化

1.1 安装

# Ubuntu / Debian
sudo apt update && sudo apt install mysql-server

# CentOS / RHEL(8 以后官方仓库是 mysql-server,需先配好源)
sudo dnf install mysql-server

# macOS
brew install mysql

Windows 从 官方下载页取安装包。建议直接上 8.0——5.7 已停止维护,且 8.0 的性能和功能(窗口函数、CTE、JSON 增强)提升明显。

1.2 初始化与安全加固

sudo mysql_secure_installation   # 设置 root 密码、删匿名用户、禁远程 root
sudo systemctl enable --now mysql

mysql_secure_installation 会问几件事:是否设置 root 密码、是否删除匿名账号、是否禁止 root 远程登录、是否删除 test 库。建议全部选是

1.3 配置文件

MySQL 按顺序读 /etc/my.cnf/etc/mysql/my.cnf~/.my.cnf,后面的覆盖前面的。

[mysqld]
datadir                   = /var/lib/mysql
socket                    = /var/run/mysqld/mysqld.sock
log-error                 = /var/log/mysql/error.log

# 字符集:必须用 utf8mb4
character-set-server      = utf8mb4
collation-server          = utf8mb4_unicode_ci

# InnoDB 核心参数
innodb_buffer_pool_size   = 4G      # 建议物理内存的 50%-70%
innodb_log_file_size      = 512M    # 重做日志大小,影响写入吞吐
innodb_flush_log_at_trx_commit = 1  # 1=最安全,2=更快但崩溃可能丢 1 秒

max_connections           = 200

MySQL 8.0 重要变更查询缓存(query_cache_size / query_cache_type)已被彻底移除。老教程里几乎人人都写 query_cache_size=128M,在 8.0 上会导致启动失败(未知变量)。8.0 不再有查询缓存这一层,优化重心转向 innodb_buffer_pool_size 和索引设计。

1.4 字符集:为什么必须 utf8mb4

MySQL 里有个历史遗留的坑:utf8 其实是 utf8mb3,每个字符最多 3 字节,存不了 emoji 和部分生僻汉字。真正的 UTF-8 是 utf8mb4(最多 4 字节)。

-- 查看当前字符集
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';

建库时必须显式指定:

CREATE DATABASE company_db
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

如果表已经建成 utf8mb3,想插入 emoji 会直接报错(Incorrect string value)。改起来也麻烦,要 ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4,大表还可能锁很久。所以一开始就用 utf8mb4

utf8mb4_unicode_ciutf8mb4_0900_ai_ci(8.0 默认)都是大小写不敏感的排序规则。它们的区别在于 Unicode 排序算法版本,0900_ai_ci 更新更准。新项目用 8.0 默认的就行。

二、数据类型选择

选类型是设计阶段最容易埋雷的地方。

用途推荐类型说明
主键BIGINT UNSIGNED AUTO_INCREMENT别用 INT,容易用完(21 亿)
金额DECIMAL(10,2)绝不用 FLOAT/DOUBLE
短字符串VARCHAR(n)按实际需要设 n
长文本TEXT别在 TEXT 上建索引
布尔TINYINT(1)MySQL 没有独立布尔类型
时间戳TIMESTAMP / DATETIME见下方说明
枚举TINYINT + 应用层字典少用 ENUM
JSONJSON5.7+,可索引生成列

关于金额FLOAT/DOUBLE 是二进制浮点,存在精度误差,0.1 + 0.2 不等于 0.3。金额、价格、任何需要精确十进制运算的场景,一律用 DECIMAL

salary DECIMAL(10,2)     -- ✅ 精确,10 位总长、2 位小数
salary FLOAT             -- ❌ 浮点误差,金额不可用

关于时间

  • DATETIME 存"字面时间",不做时区转换(范围 1000-9999 年)
  • TIMESTAMP 存"时间点",写入时按会话时区转换,以 UTC 存储(范围 1970-2038 年,有 2038 问题)
  • 跨时区的系统用 TIMESTAMP;只在本时区、且想要更大范围用 DATETIME

关于 VARCHAR(n) 的 n:它限制的是字符数而非字节数(utf8mb4VARCHAR(100) 最多占 400 字节)。行内所有 VARCHAR 加起来别超过约 65535 字节,否则建表失败。

三、建表与约束

CREATE TABLE employees (
    id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name        VARCHAR(50)  NOT NULL,
    email       VARCHAR(100) NOT NULL UNIQUE,
    department  VARCHAR(50),
    salary      DECIMAL(10,2) NOT NULL DEFAULT 0,
    hire_date   DATE,
    created_at  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个值得说明的点:

  • ENGINE=InnoDB:8.0 起是默认引擎,支持事务和行锁。别用 MyISAM(不支持事务,表级锁)。
  • DEFAULT CURRENT_TIMESTAMPTIMESTAMP 支持;DATE 的默认值想用表达式需要 8.0.13+ 且要加括号:DEFAULT (CURRENT_DATE)
  • ON UPDATE CURRENT_TIMESTAMP:更新行时自动刷新时间,比应用层维护省事。
  • CHECK 约束:MySQL 8.0.16+ 才真正强制,之前的版本会接受但不检查。

修改表结构:

ALTER TABLE employees ADD COLUMN phone VARCHAR(20);
ALTER TABLE employees MODIFY COLUMN phone VARCHAR(30);          -- 改类型(MySQL 专有)
ALTER TABLE employees CHANGE COLUMN phone mobile VARCHAR(30);   -- 改名 + 改类型
ALTER TABLE employees DROP COLUMN mobile;

MODIFYCHANGE 都是 MySQL 专有语法。PostgreSQL 用 ALTER COLUMN ... TYPE,标准 SQL 也是那一套。写可移植的 SQL 要注意这点。

四、数据操作

4.1 插入

-- 单条
INSERT INTO employees (name, email, department, salary)
VALUES ('张三', 'zhang@company.com', '技术部', 15000);

-- 批量(一次多行比循环单行快得多)
INSERT INTO employees (name, email, department, salary) VALUES
('李四', 'li@company.com', '市场部', 12000),
('王五', 'wang@company.com', '财务部', 13000);

-- 从查询结果插入
INSERT INTO employees_archive (name, email)
SELECT name, email FROM employees WHERE hire_date < '2020-01-01';

4.2 UPSERT:存在就更新

INSERT INTO employees (id, name, salary)
VALUES (1, '张三', 16000)
ON DUPLICATE KEY UPDATE salary = VALUES(salary);

冲突(主键或唯一键重复)时执行 UPDATE。这是 MySQL 的写法;PostgreSQL 和 SQLite 用 ON CONFLICT ... DO UPDATE

REPLACE INTO employees (id, name, salary) VALUES (1, '张三', 16000);

REPLACE INTO先删后插(先删冲突行,再插入),会触发 ON DELETE 级联和触发器、且会换掉自增主键。除非你确实想要"删旧插新",否则优先用 ON DUPLICATE KEY UPDATE

4.3 查询、更新、删除

SELECT name, salary FROM employees
WHERE department = '技术部' AND salary > 14000
ORDER BY hire_date DESC
LIMIT 5 OFFSET 0;

UPDATE employees SET salary = salary * 1.1 WHERE id = 1;

DELETE FROM employees WHERE id = 5;

五、查询进阶

5.1 连接

-- 假设 employees 有 department_id 列指向 departments.id
SELECT e.name, e.salary, d.name AS dept_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;

-- 左连接:所有员工,没有部门的也列出
SELECT e.name, d.name AS dept_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;

-- 找出没有部门的员工(反连接)
SELECT e.name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id
WHERE d.id IS NULL;

上面三个例子都要求 employees 表真的有 department_id。很多示例代码会忘记这一步——只建了 department VARCHAR(50) 就直接 JOIN ... ON e.department_id,跑起来报 Unknown column。设计时想清楚:部门是字符串还是外键引用?

5.2 GROUP BY 与 ONLY_FULL_GROUP_BY

SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
HAVING COUNT(*) > 3
ORDER BY cnt DESC;

MySQL 5.7 起默认开启 ONLY_FULL_GROUP_BY,导致老代码里这种写法会报错:

-- ❌ 报错:name 不在 GROUP BY 里,也不是聚合函数
SELECT department, name, COUNT(*) FROM employees GROUP BY department;

这个模式的目的是防止"选了不在分组里的列,值又不确定"的歧义查询。正确做法是用聚合函数(GROUP_CONCAT(name)MAX(name))或者把列加进 GROUP BY。查当前设置:

SELECT @@sql_mode;

5.3 窗口函数(MySQL 8.0+)

SELECT
    name,
    department,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
    RANK()       OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank_skip
FROM employees;

窗口函数能在不合并行的前提下做分组内排名、移动平均、同比环比。8.0 之前只能用变量或自连接绕,很难写也很难优化。

5.4 CTE(MySQL 8.0+)

WITH dept_stats AS (
    SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
)
SELECT d.name, s.cnt, s.avg_salary
FROM dept_stats s
JOIN departments d ON d.name = s.department
WHERE s.cnt > 3;

递归 CTE 可以遍历树形结构:

WITH RECURSIVE org AS (
    SELECT id, name, manager_id, 1 AS level FROM employees WHERE manager_id IS NULL
    UNION ALL
    SELECT e.id, e.name, e.manager_id, o.level + 1
    FROM employees e JOIN org o ON e.manager_id = o.id
)
SELECT * FROM org ORDER BY level;

递归 CTE 默认最大递归深度受 cte_max_recursion_depth 限制(默认 1000),层级很深的树要调整这个变量。

5.5 JSON 字段(5.7+)

CREATE TABLE events (id BIGINT PRIMARY KEY, payload JSON);

INSERT INTO events VALUES (1, '{"user": "张三", "action": "login", "ip": "1.2.3.4"}');

-- 取值
SELECT JSON_EXTRACT(payload, '$.user') AS user FROM events;
SELECT payload->>'$.user' AS user FROM events;      -- 简写,返回文本
SELECT payload->'$.user' AS user FROM events;       -- 简写,返回 JSON

-- 条件查询
SELECT * FROM events WHERE JSON_EXTRACT(payload, '$.action') = 'login';

-- 生成列 + 索引(JSON 字段本身不能建索引,要借助生成列)
ALTER TABLE events
  ADD COLUMN action VARCHAR(20) AS (JSON_UNQUOTE(payload->>'$.action')) STORED,
  ADD INDEX idx_action (action);

JSON 字段适合"结构不定"的数据,但不适合替代关系建模。能用普通列表达的,就别塞 JSON——查询和索引都更麻烦。

六、索引与优化

6.1 索引类型与创建

CREATE INDEX idx_dept_salary ON employees(department, salary);   -- 复合索引
CREATE UNIQUE INDEX uidx_email ON employees(email);              -- 唯一索引
CREATE INDEX idx_name_prefix ON employees(name(10));            -- 前缀索引(长字符串)
SHOW INDEX FROM employees;                                       -- 查看
DROP INDEX idx_dept_salary ON employees;                         -- 删除(MySQL 要带 ON)

6.2 最左前缀原则

复合索引 (department, salary) 能加速:

  • WHERE department = ?
  • WHERE department = ? AND salary > ?

不能加速:

  • WHERE salary > ?(跳过了最左列)

建复合索引时,把区分度高、最常单独查询的列放最左。

6.3 索引失效的典型场景

-- ❌ 对索引列做运算/函数 → 索引失效,全表扫描
SELECT * FROM employees WHERE YEAR(hire_date) = 2023;

-- ✅ 改成范围比较
SELECT * FROM employees
WHERE hire_date >= '2023-01-01' AND hire_date < '2024-01-01';

-- ❌ 前导通配符
SELECT * FROM employees WHERE name LIKE '%三';

-- ❌ 隐式类型转换(phone 是 VARCHAR,传了数字)
SELECT * FROM employees WHERE phone = 13800138000;

-- ❌ OR 连接了未索引的列(可能退化为全表扫描)
SELECT * FROM employees WHERE email = 'x@y.com' OR department = '技术部';

6.4 EXPLAIN 看执行计划

EXPLAIN SELECT * FROM employees WHERE department = '技术部' AND salary > 14000;

-- 8.0.18+:实际执行并给真实耗时
EXPLAIN ANALYZE SELECT * FROM employees WHERE department = '技术部';

重点看这两列:

  • type:访问类型,从好到坏大致是 system > const > eq_ref > ref > range > index > ALL。看到 ALL 就是全表扫描。
  • rows:预估扫描行数。数字很大说明索引没起作用。
  • key:实际用了哪个索引。为 NULL 说明没用索引。

EXPLAIN ANALYZE 会真的执行查询(注意别在写查询上用),输出各步骤的实际耗时,定位慢点比 EXPLAIN 准确。

6.5 慢查询日志

排查性能问题的第一站。

SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'slow_query_log_file';

-- 动态开启(重启失效,要持久化写进 my.cnf)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;        -- 超过 1 秒的记录
[mysqld]
slow_query_log      = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time     = 1
log_queries_not_using_indexes = 1      # 连未走索引的也记(日志会变多)

日志里的查询可以用 mysqldumpslowpt-query-digest(Percona Toolkit)聚合分析,找出最耗时的几条。

6.6 OPTIMIZE 与 ANALYZE

OPTIMIZE TABLE employees;   # 整理碎片、回收空间(大表耗时且会锁表,慎用)
ANALYZE TABLE employees;    # 更新索引统计信息,帮助优化器选对索引

七、事务与锁

7.1 事务

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;     -- 失败时 ROLLBACK

InnoDB 才支持事务(MyISAM 不支持)。

7.2 隔离级别

SELECT @@transaction_isolation;                                 -- 查看(8.0)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;         -- 设置
级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED可能可能
REPEATABLE READInnoDB 基本避免
SERIALIZABLE

MySQL InnoDB 默认是 REPEATABLE READ,而 PostgreSQL 默认是 READ COMMITTED。这个差异会让同样的并发代码在两个库上表现不同——写与并发相关的逻辑时必须确认。

7.3 锁

-- 悲观锁:锁定读到的行,直到事务结束
SELECT * FROM orders WHERE id = 100 FOR UPDATE;

-- 共享锁(只读锁定,其他事务可读不可写)
SELECT * FROM orders WHERE id = 100 LOCK IN SHARE MODE;

InnoDB 的锁粒度:

  • 行锁:锁定索引记录。注意:如果 WHERE 没走索引,行锁会退化成锁全表(因为要扫描所有行)。
  • 间隙锁(Gap Lock)REPEATABLE READ 下会锁住索引记录之间的"间隙",防止其他事务插入——这是 InnoDB 避免幻读的机制,也是死锁的常见来源。
  • 表锁:DDL 操作、或显式 LOCK TABLES

查看锁等待:

-- 当前锁等待情况(8.0)
SELECT * FROM performance_schema.data_lock_waits;

-- 正在运行的事务
SELECT * FROM information_schema.innodb_trx\G

7.4 死锁

死锁是两个事务互相持有对方需要的锁。InnoDB 会自动检测并回滚其中一个(报 ERROR 1213: Deadlock found)。

SHOW ENGINE INNODB STATUS\G

输出里的 LATEST DETECTED DEADLOCK 段会给出最近一次死锁的两个事务和各自持有的锁,是排查死锁的第一手资料。

减少死锁的经验

  • 多个事务按相同顺序访问表和行(比如都按 id 升序更新)
  • 事务尽量短,别在事务里做网络调用/耗时计算
  • 用合适的索引,避免行锁退化成表级/间隙锁

锁等待超时由 innodb_lock_wait_timeout 控制(默认 50 秒):

SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SET GLOBAL innodb_lock_wait_timeout = 30;

八、用户与权限

CREATE USER 'app'@'localhost' IDENTIFIED BY 'StrongPass!2026';
GRANT SELECT, INSERT, UPDATE, DELETE ON company_db.* TO 'app'@'localhost';
SHOW GRANTS FOR 'app'@'localhost';
REVOKE DELETE ON company_db.* FROM 'app'@'localhost';
DROP USER 'app'@'%';

'user'@'host' 是 MySQL 的特色:同一用户名从不同主机连,是不同账号'app'@'localhost''app'@'%'(任意主机)需要分别授权。

别给应用账号 ALL PRIVILEGES ON *.*。最小权限原则——应用只需要它那几个库的 CRUD,就不该给它 DROP DATABASE 的能力。

九、备份与恢复

9.1 逻辑备份(mysqldump)

# 生产必备参数
mysqldump -u root -p \
  --single-transaction \
  --routines --triggers --events \
  --set-gtid-purged=OFF \
  company_db | gzip > company_db-$(date +%F).sql.gz

--single-transaction 对 InnoDB 表建立一致性快照,不锁表,备份期间业务照常写入。

# 恢复
gunzip < company_db-2026-09-22.sql.gz | mysql -u root -p company_db

9.2 物理备份(XtraBackup)

mysqldump 是逻辑备份(导出 SQL 文本),库大了会很慢(几十 GB 以上不实用)。生产环境的大库用物理备份工具 Percona XtraBackup,直接复制数据文件,支持热备和增量。

9.3 定时备份脚本

#!/bin/bash
set -euo pipefail
BACKUP_DIR=/backups/mysql
mkdir -p "$BACKUP_DIR"
DATE=$(date +%F_%H%M)

# 用 --login-path 避免在脚本/进程列表里暴露密码
mysqldump --login-path=backup \
    --single-transaction --routines --triggers \
    --all-databases | gzip > "$BACKUP_DIR/all-$DATE.sql.gz"

find "$BACKUP_DIR" -name 'all-*.sql.gz' -mtime +7 -delete
# 每天凌晨 2 点
0 2 * * * /usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1

别在 crontab 里写 -pPASSWORD。crontab 文件、ps aux 输出都可能被别人看到明文密码。用 mysql_config_editor 生成登录路径(见 MySQL 命令行教程 §1.3)。

十、存储过程、函数、触发器、事件

10.1 存储过程

DELIMITER //

CREATE PROCEDURE IncreaseSalaries(IN dept VARCHAR(50), IN pct FLOAT)
BEGIN
    UPDATE employees
    SET salary = salary * (1 + pct / 100)
    WHERE department = dept;
END //

DELIMITER ;

CALL IncreaseSalaries('技术部', 10);

DELIMITER //客户端命令(不是 SQL),用来临时把语句结束符从 ; 改成 //,否则过程体里的分号会提前结束 CREATE PROCEDURE

参数名别和列名重名。如果参数叫 department,过程体里的 WHERE department = department 会变成自己和自己比,永远为真(或产生歧义)。上面的例子用 dept 避开就是这个原因。

10.2 函数

DELIMITER //

CREATE FUNCTION GetEmployeeCount(dept VARCHAR(50))
RETURNS INT
DETERMINISTIC
READS SQL DATA
BEGIN
    DECLARE cnt INT;
    SELECT COUNT(*) INTO cnt FROM employees WHERE department = dept;
    RETURN cnt;
END //

DELIMITER ;

SELECT GetEmployeeCount('市场部');

DETERMINISTIC 告诉优化器"同样输入总是同样输出",否则在开启 binlog 的服务器上创建函数会被拒绝。

10.3 触发器

CREATE TABLE audit_log (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    action      VARCHAR(20),
    table_name  VARCHAR(50),
    record_id   BIGINT,
    change_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

DELIMITER //
CREATE TRIGGER trg_employee_after_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    INSERT INTO audit_log (action, table_name, record_id)
    VALUES ('UPDATE', 'employees', NEW.id);
END //
DELIMITER ;

触发器里可以引用 NEW(新行)和 OLD(旧行)。触发器逻辑对应用透明,容易变成"隐形业务逻辑",能用应用层做的事就别塞触发器。

10.4 事件调度

SET GLOBAL event_scheduler = ON;

CREATE EVENT daily_audit_cleanup
ON SCHEDULE EVERY 1 DAY
STARTS CURRENT_TIMESTAMP
DO
    DELETE FROM audit_log WHERE change_time < NOW() - INTERVAL 30 DAY;

事件相当于数据库内置的定时任务。注意 event_scheduler 默认是 OFF,不打开的话事件不会执行。

十一、主从复制(企业级)

单机 MySQL 有单点风险。生产环境通常配一主一从(或多从),从库可以做读负载分担和故障切换。

主库配置

[mysqld]
server-id        = 1
log-bin          = mysql-bin
binlog_format    = ROW          # 推荐 ROW,复制更精确
gtid_mode        = ON
enforce_gtid_consistency = ON
CREATE USER 'repl'@'%' IDENTIFIED BY 'ReplPass!2026';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

从库配置

[mysqld]
server-id     = 2
relay-log     = relay-bin
read_only     = ON              # 从库只读(超级用户仍可写)
gtid_mode     = ON
enforce_gtid_consistency = ON
CHANGE REPLICATION SOURCE TO
    SOURCE_HOST = '10.0.0.1',
    SOURCE_USER = 'repl',
    SOURCE_PASSWORD = 'ReplPass!2026',
    SOURCE_AUTO_POSITION = 1;    -- 用 GTID 自动定位

START REPLICA;
SHOW REPLICA STATUS\G            -- 看 Replica_IO_Running / Replica_SQL_Running 是否都为 Yes

8.0 术语变更CHANGE MASTER TOCHANGE REPLICATION SOURCE TOSTART SLAVESTART REPLICASHOW SLAVE STATUSSHOW REPLICA STATUS。老命令还能用,但已废弃。

主从延迟是常见问题:SHOW REPLICA STATUS 里的 Seconds_Behind_Source 反映了从库落后主库多少秒。大事务、单线程回放、从库负载高都会加剧延迟。

十二、Python 集成

用官方驱动 mysql-connector-pythonpip install mysql-connector-python):

import mysql.connector
from mysql.connector import Error

connection = None
try:
    connection = mysql.connector.connect(
        host='localhost',
        user='python_user',
        password='secure_pass',
        database='company_db',
        charset='utf8mb4',
    )

    if connection.is_connected():
        cursor = connection.cursor()

        # 参数化查询:%s 是占位符(注意不是 ?)
        cursor.execute(
            "SELECT id, name, salary FROM employees WHERE department = %s",
            ('技术部',),
        )
        for emp_id, name, salary in cursor.fetchall():
            print(f"{emp_id}\t{name}\t{salary}")

        # 插入
        cursor.execute(
            "INSERT INTO employees (name, email, department, salary) VALUES (%s, %s, %s, %s)",
            ('钱七', 'qian@company.com', '人事部', 11000),
        )
        connection.commit()

        cursor.close()

except Error as e:
    print("数据库错误:", e)

finally:
    if connection and connection.is_connected():
        connection.close()

几个要点:

  1. %s 是 MySQL 驱动的占位符(不是 ?,那是 SQLite 的)。它同时处理了转义,是防注入的正道。
  2. charset='utf8mb4' 显式指定,避免连接层用错字符集导致中文乱码。
  3. 连接要 close()。生产代码用连接池(如 mysql-connector-pythonpooling 或 SQLAlchemy),别每次请求新建连接。
  4. autocommit 默认是关的。增删改之后必须 commit(),否则不生效。

十三、常见问题排查

13.1 忘记 root 密码

# 1. 停服务
sudo systemctl stop mysql

# 2. 跳过权限表启动(--skip-networking 必须加,否则这段窗口任何人可免密进入)
sudo mysqld_safe --skip-grant-tables --skip-networking &
-- 3. 免密登录,先刷权限再用 ALTER USER(跳过权限表时权限表在内存里是空的)
mysql -u root
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewStrongPass!2026';
# 4. 关掉临时实例,正常启动
mysqladmin -u root -p shutdown
sudo systemctl start mysql

13.2 错误码速查

错误码含义排查方向
1045Access denied用户名/密码错,或该用户不允许从当前主机连接
1049Unknown database库名写错
1054Unknown column列名写错,或表结构和你以为的不一样
1213Deadlock found死锁,InnoDB 已回滚一个事务
1205Lock wait timeout有长事务持锁,查 data_lock_waits
2002Socket 连接失败socket 路径不对,或服务没起
2003无法连接网络/端口/防火墙,或 bind-address=127.0.0.1 限制
2013Lost connection during query查询太久被杀(max_execution_time/网络中断/服务重启)
1055ONLY_FULL_GROUP_BY 报错见 §5.2

13.3 连接数打满

SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
SHOW VARIABLES LIKE 'max_connections';

SELECT user, host, COUNT(*) FROM information_schema.processlist GROUP BY user, host;

Threads_running 高说明有慢查询堆积(不是连接数问题);Threads_connected 逼近 max_connections 说明有连接泄漏(应用没关连接)或真的需要扩容。

学习资源

  1. MySQL 8.0 官方文档 —— 最权威,尤其看 “Server Administration” 和 “Optimization” 章节
  2. MySQL 官方博客 —— 版本变更(如 query_cache 移除)的权威说明
  3. Use The Index, Luke —— 索引与 SQL 性能的免费经典
  4. Percona Toolkit —— 慢查询分析、在线 DDL、主从校验等运维利器
  5. MySQL Tutorial —— 入门教程
graph TD
A[安装与初始化] --> B[字符集 utf8mb4]
B --> C[设计表与选型]
C --> D[CRUD 与查询]
D --> E[索引与执行计划]
E --> F[事务与锁]
F --> G[慢查询与优化]
G --> H[备份恢复]
H --> I[主从复制]
I --> J[应用集成]

MySQL 上手快,但真正拉开水平差距的是设计和配置:字符集一次选对、金额用 DECIMAL、索引想清楚最左前缀、事务写短一点。这几点做到了,大部分线上问题就不会找到你头上。

参考&致谢

系列教程

全部文章RSS订阅

数据库系列


MySQL 使用全面指南:从入门到高级实践
发布于
2025年11月22日
许可协议。转载请注明来源
评论
数据加载中 ...
 上一篇

阅读全文

PostgreSQL 使用全面指南:从入门到企业级应用
PostgreSQL 使用全面指南:从入门到企业级应用 PostgreSQL 使用全面指南:从入门到企业级应用
PostgreSQL 常被称为"最先进的开源关系型数据库",它的优势不在速度,而在功能完整和可扩展性——JSONB、数组、范围类型、丰富的索引方法、严格的 SQL 标准遵守。本文从安装认证讲到查询进阶、索引选型、事务并发、备份复制,尽量把版
2025-11-22
下一篇 

阅读全文

SQLite使用全面教程:轻量级数据库的终极指南
SQLite使用全面教程:轻量级数据库的终极指南 SQLite使用全面教程:轻量级数据库的终极指南
SQLite 是部署量最大的数据库引擎,从手机 App 到浏览器内核都有它的身影。本文从安装和命令行讲起,逐步覆盖类型系统、外键约束、事务与 WAL、备份恢复和 Python 集成,重点说清那些容易踩的默认值陷阱。
2025-11-22