This commit is contained in:
2026-05-24 11:42:38 +08:00
commit 30d312ac35
521 changed files with 146481 additions and 0 deletions
@@ -0,0 +1,268 @@
---
tags: [MySQL, 架构, 进程模型, Server]
create time: 2026-05-16 00:00
---
# MySQL 架构与进程模型
## 概述
理解 MySQL 的内部架构是性能调优和故障排查的前提。本文将从 Client-Server 模型出发,逐层拆解 MySQL 的四大功能层以及不同存储引擎下的线程调度机制。
## 四层架构
MySQL 遵循经典的三层 Client-Server 架构,但在服务端内部被清晰划分为四层:
```mermaid
graph BT
subgraph "应用层"
A["应用程序<br/>(Go / Java / Python)"]
end
subgraph "连接层<br/>Connection Layer"
B["Thread Pool"]
C["Authentication"]
D["Connection Management"]
end
subgraph "SQL 层<br/>SQL Layer"
E["Query Cache ⚠️ 8.0 已移除"]
F["Parser → Preprocessor"]
G["Optimizer"]
H["Executor"]
end
subgraph "存储引擎层<br/>Storage Engine"
I["InnoDB"]
J["MyISAM"]
K["Memory"]
L["Archive"]
end
subgraph "文件系统"
M["Data Files"]
N["Binlog"]
O["Redo Log"]
end
A --> B
B --> F
F --> G --> H --> I
H --> J
H --> K
H --> L
I --> M
I --> N
I --> O
```
### 各层职责
**连接层**负责处理客户端的连接请求,包括身份验证、权限校验、SSL/TLS 协商以及线程分配。MySQL 采用「一连接一线程」模型——每个客户端连接对应一个独立的服务器线程,这意味着万级并发时会产生巨大的线程开销。
> [!NOTE] 为什么高并发场景要考虑线程池?
> MySQL 原生没有内置线程池(Percona Server 提供)。当并发量超过数千时,上下文切换的开销会成为瓶颈。解决方案有三:
> - **ProxySQL / MaxScale**:前端代理层做连接复用
> - **MySQL Thread Pool Plugin**:企业版功能或 Percona 分支
> - **连接池**:在应用层控制并发连接数(如 Go 的 `sql.DB`)
**SQL 层**是整个数据库的核心,完成 SQL 语句的解析、优化和执行。这一层实现了 SQL 标准语法、查询优化策略、函数系统和事务管理。值得注意的是,**很多用户熟悉的 SQL 特性其实都在这一层实现**——比如子查询改写、JOIN 顺序优化等。
**存储引擎层**提供了可插拔的存储方案。不同的引擎对索引、锁、事务的支持各不相同。InnoDB 承担了默认的事务型负载,而 MyISAM、Memory 等引擎则针对特定场景做了优化。由于大多数生产环境只使用 InnoDB,现代运维中这一层其实非常安静。
**文件系统层**将页(Page,默认 16KB)写入磁盘。InnoDB 有自己的 Buffer Pool 来管理数据缓存,同时也依赖操作系统的 Page Cache。**Linux 内核的 I/O 调度器对 MySQL 性能有直接影响**。
## 线程模型详解
MySQL 服务启动时会创建一组后台线程,同时为每个连接分配专用线程:
```mermaid
classDiagram
class MasterThread {
+binlog dump
+purge redo log
+truncate binary log
+DD event cleanup
+change buffer merge
}
class ConnectionThread {
+parse query
+optimize
+execute
+send result
}
class WorkerThreads {
+InnoDB read worker
+InnoDB log writer
+InnoDB page cleaner
}
class ThreadPool {
<<optional>>
+acceptor thread
+worker pool
+idle stack management
}
MasterThread ..> WorkerThreads : "coordinates"
ThreadPool --> ConnectionThread : "dispatches"
```
### 关键后台线程
| 线程 | 职责 |
|------|------|
| **Master Thread** | InnoDB 专属,协调写操作、刷新脏页、合并 Change Buffer |
| **Log Thread** | 将 Redo Log Buffer 刷入 Redo Log File |
| **Page Cleaner** | 主动将脏页从 Buffer Pool 刷盘,避免崩溃恢复时间过长 |
| **Purge Thread** | 删除 Undo Log 中标记为废弃的行记录 |
| **DD Event Cleanup** | 清理数据字典中的过期对象信息 |
### 连接线程生命周期
```mermaid
stateDiagram-v2
[*] --> New: 客户端发起 TCP 连接
New --> Handshake: 握手协议 + 认证
Handshake --> Authenticated: 认证成功
Handshake --> Closed: 认证失败
Authenticated --> Sleeping: 发送命令
Sleeping --> Executing: 收到新命令
Executing --> Sending: 执行完毕
Sending --> Sleeping: 返回结果集
Sleeping --> Closed: 客户端断开 / 超时
Sleeping --> Killed: 管理员 kill 掉
```
> [!QUESTION] 什么是 MySQL 连接超时?
> 如果客户端空闲太久不发消息会发生什么?MySQL 有两个关键超时参数:
> - **`wait_timeout`**(默认 28800 秒 = 8 小时):非交互式连接的闲置超时
> - **`interactive_timeout`**(默认 28800 秒):交互式连接(如 mysql CLI)的闲置超时
>
> 连接池中如果未正确配置这些参数,可能出现大量 ZOMBIE 连接占满 `MaxConnections`。这就是为什么 Go 的 `sql.DB.ConnMaxLifetime` 应该设为比 wait_timeout 更短的值。
## mysqld 启动流程
```mermaid
flowchart TD
A["加载 my.cnf 配置"] --> B["初始化全局变量"]
B --> C["加载插件"]
C --> D["初始化存储引擎<br/>innodb_init()"]
D --> E["打开数据目录"]
E --> F["恢复未提交事务<br/>redo log crash recovery"]
F --> G["启动后台线程"]
G --> H["监听端口"]
H --> I["就绪 ✨"]
```
启动过程中的关键阶段:
1. **配置加载**:按优先级读取配置文件 `/etc/my.cnf` → `/etc/mysql/my.cnf` → `~/.my.cnf`,命令行参数优先级最高
2. **引擎初始化**:InnoDB 会在启动时进行 Crash Recovery——重放 Redo Log 恢复到一致状态。如果数据文件损坏严重,这一步会卡住或报错
3. **Crash Recovery**:innodb_force_recovery 参数可以在恢复期间限制操作级别(1~6),用于紧急导出数据
## 核心架构图
```mermaid
graph TB
subgraph "连接管理"
C1["Accept Thread"] --> C2["Connection Pool"]
C2 --> C3["Read Command"]
end
subgraph "SQL 处理流水线"
C3 --> P1["Syntax Parser"]
P1 --> P2["AST Semantic Check"]
P2 --> P3["Preprocessing"]
P3 --> P4["Optimization"]
P4 --> P5["Execution"]
end
subgraph "InnoDB 执行路径"
P5 --> I1["Buffer Pool Read Page"]
I1 --> I2{"Hit?"}
I2 -->|"Yes"| I3["Return Result"]
I2 -->|"No"| I4["Read from Disk"]
I4 --> I1
I5["Change Buffer"] -.-> I1
end
C1 --> C3
style P4 fill:#FF9F43,color:#000
style I1 fill:#00B6BC,color:#fff
style I3 fill:#00D866,color:#fff
```
## 实战:在线诊断工具
在生产环境中快速定位问题,需要掌握以下几组命令行工具。它们分别作用于不同的架构层级:
### 1. SHOW PROCESSLIST — SQL 层视角
```sql
-- 最基础的线程状态查看
SHOW FULL PROCESSLIST;
-- 等效的 SQL 查询(可结合 WHERE 过滤)
SELECT * FROM information_schema.processlist WHERE COMMAND != 'Sleep';
```
每个线程在 `PROCESSLIST` 中表现为一条记录,关键列:
| 列 | 含义 | 常见值 |
|----|------|--------|
| `ID` | 线程 ID,`KILL id` 使用 | |
| `State` | 当前执行阶段 | `Sending data`, `Creating sort index`, `Locked` |
| `Time` | 当前状态的持续秒数 | > 5s 需警惕 |
| `Info` | 正在执行的 SQL | NULL 表示空闲 |
> [!TIP] State 解读口诀
> - **`Waiting for handler lock`** → 行锁竞争,配合 `Innodb_row_lock_waits` 排查
> - **`Creating sort index`** → 大 ORDER BY/GROUP BY,检查是否缺少索引
> - **`Sending data`** → 不仅是在"发送结果",而是**正在扫描和生成数据**,往往是最耗时的阶段
### 2. SHOW STATUS — 全局计数器
```sql
-- 查看慢查询总数(取决于 slow_query_log 配置)
SHOW GLOBAL STATUS LIKE 'Slow_queries';
-- 当前并发连接数 vs 上限
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_connections';
-- InnoDB 缓冲池命中率(接近 100% 为佳)
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
```
> [!QUESTION] 为什么 Buffer Pool 命中率不是越高越好?
> 理论上 Hit Rate 越接近 100% 越好,但当数据集小于 Buffer Pool 时,Hit Rate 会固定在 100% 以上(因为缓存了多次)。生产环境中 **98%~99%** 已经足够优秀,不必盲目增大 `innodb_buffer_pool_size`。
### 3. Performance Schema — MySQL 5.7+ 的深层监控
`performance_schema` 是 MySQL 内置的性能采集框架,比 `SHOW STATUS` 更细粒度:
```sql
-- 当前等待事件(锁、I/O、网络)TOP 10
SELECT EVENT_NAME, COUNT_STAR, AVG_TIMER_WAIT
FROM performance_schema.events_waits_summary_global_by_event_name
ORDER BY COUNT_STAR DESC LIMIT 10;
-- 最近 10 条耗时 > 1s 的语句
SELECT DIGEST_TEXT, COUNT_STAR, AVG_TIMER_WAIT/1e12 AS avg_ms
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1e12
ORDER BY AVG_TIMER_WAIT DESC LIMIT 10;
```
> [!NOTE] Performance Schema 的性能代价
> 开启 PS 会带来约 **5%~15%** 的 CPU 开销。建议在低峰期采样分析,或仅对目标线程启用 instrumentation。
---
## 关联笔记
- [[hhs/GORM/01-安装与初始化]] — GORM 连接 MySQL 的配置参数
- [[hhs/Redis/01-安装与部署]] — Redis 单线程 vs MySQL 多线程模型对比
@@ -0,0 +1,338 @@
---
tags: [MySQL, 安装, Docker, 配置]
create time: 2026-05-16 00:00
---
# MySQL 安装与初始化
## 概述
本文系统讲解 MySQL Community Edition 的安装部署和初始化配置,涵盖三种主流方式:**Docker Compose**(推荐开发环境)、**YUM/RPM 包安装**、**通用二进制包安装**。同时深入解析 `my.cnf` 配置文件的核心参数体系,以及 `mysql_install_db` 初始化时的底层行为。
> [!TIP] 学习建议
>
> 1. **初学者**优先掌握 Docker Compose 方式,快速搭建可复现的开发环境;
> 2. **运维/生产场景**参考二进制安装流程,理解服务初始化的完整链条;
> 3. `my.cnf` 参数部分按优先级阅读,先吃透 InnoDB 和网络两大块,日志和 Binlog 可在后续进阶时补充。
>
> *思考题:为什么 InnoDB Buffer Pool 通常建议设为物理内存的 50%~70%,而不是 90%?*(答案见下文 InnoDB 参数段落的注释)
## Docker Compose 方式(推荐开发环境)
这是最轻量且可复现的方案,适合日常开发和 CI 测试。
```yaml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql-dev
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpassword # 首次启动自动创建 root 用户
MYSQL_DATABASE: app_db # 自动创建初始数据库
MYSQL_USER: app_user # 创建普通应用账户
MYSQL_PASSWORD: app_password # 该账户对 MYSQL_DATABASE 拥有完全权限
ports:
- "3306:3306" # 宿主机 3306 → 容器内 3306
volumes:
- ./init-db:/docker-entrypoint-initdb.d # 首次启动自动执行 SQL
- mysql-data:/var/lib/mysql # 持久化数据(容器删除后不丢失)
- ./my-custom.cnf:/etc/mysql/conf.d/my.cnf # 覆盖默认配置
command: >
--character-set-server=utf8mb4 # 统一字符集
--collation-server=utf8mb4_unicode_ci # 排序规则(不区分大小写)
--default-authentication-plugin=mysql_native_password # 兼容旧版客户端认证
--max-connections=500 # 最大并发连接数
--innodb-buffer-pool-size=256M # 开发机无需太大
volumes:
mysql-data:
```
### 启动与常用操作
```bash
docker compose up -d # 后台启动
docker compose down # 停止并移除容器(保留 volume)
docker compose logs -f # 查看实时日志
docker compose exec mysql mysql -u root -p # 进入容器内客户端
docker compose ps # 查看容器状态
docker compose restart # 重启(配置变更后适用)
```
> [!QUESTION] 生产环境应该硬编码密码吗?
> 当然不。上述 `docker-compose.yml` 中的明文密码仅适用于本地开发。
> 生产环境应使用 Docker Secrets、环境变量文件 `.env` 或密钥管理工具(如 HashiCorp Vault)来注入凭据。
>
> ```bash
> # .env 文件示例(加入 .gitignore!)
> MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASS}
> MYSQL_DATABASE=myapp_prod
> ```
>
> Docker Compose 会自动将 `${VAR}` 替换为环境变量值。
### init-db 目录妙用
`docker-entrypoint-initdb.d/` 下只要是 `.sql` 或 `.sh` 脚本,会在数据库**首次启动**时按字母序自动执行:
| 文件类型 | 执行时机 | 典型用途 |
|---------|---------|---------|
| `.sql` | 首次初始化 | DDL建表、默认数据、权限分配 |
| `.sh` | 首次初始化 | 调用 `mysql` CLI 执行动态逻辑 |
> [!WARNING] 注意
> - 容器已有数据时,init-db 脚本**不会被重新执行**。修改脚本后需删除 volume 重建:`docker compose down -v && docker compose up -d`
> - `.sh` 脚本中若需连接 MySQL,务必等待 ready(可通过 `docker compose wait` 或健康检查机制)
## 二进制安装(生产环境参考)
对于需要精细控制的场景,官方推荐的两种方式:YUM/RPM 包适合标准发行版;通用二进制包提供最大灵活性。
### 方式一:YUM/RPM(CentOS / RHEL / Amazon Linux)
```bash
# 1. 下载 YUM 仓库配置(以 EL9 为例)
sudo rpm -Uvh https://dev.mysql.com/get/mysql80-community-release-el9-3.noarch.rpm
# 2. 安装 MySQL Server
sudo yum install mysql-server -y
# 3. 启动服务并设置开机自启
sudo systemctl start mysqld
sudo systemctl enable mysqld
# 4. 获取临时 Root 密码(首次安装后打印到日志)
sudo grep 'temporary password' /var/log/mysqld.log
# 5. 安全加固(修改默认密码、移除匿名账户等)
sudo mysql_secure_installation
```
> [!TIP] 临时密码在哪?
> MySQL 8.0 首次启动时会在 `/var/log/mysqld.log` 中生成一个随机临时密码。
> 找到类似 `A temporary password is generated for root@localhost: xxxxxxxx` 的行,使用该密码登录 `mysql_secure_installation`。
### 方式二:通用二进制包(跨发行版兼容)
适用于非标准发行版或需要自定义目录结构的场景:
```bash
# 1. 下载并解压(替换实际版本号)
wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.xx-linux-glibc2.28-x86_64.tar.xz
tar -xvf mysql-8.0.xx-linux-glibc2.28-x86_64.tar.xz
sudo mv mysql-8.0.xx-linux-glibc2.28-x86_64 /usr/local/mysql
# 2. 创建专用系统用户(不建议用 root 运行 MySQL)
cd /usr/local/mysql
sudo groupadd mysql
sudo useradd -r -g mysql -s /bin/false mysql
# 3. 设置权限 + 初始化数据目录
sudo chown -R mysql:mysql .
sudo bin/mysqld --initialize --user=mysql --datadir=/data/mysql
# 4. 配置文件(可选,放在 /etc/my.cnf)
# 指定 basedir=/usr/local/mysql 和 datadir=/data/mysql
# 5. 初始化 SSL 证书(MySQL 8.0 必需)
sudo bin/mysql_ssl_rsa_setup --datadir=/data/mysql
# 6. 启动服务
sudo bin/mysqld_safe --user=mysql &
# 7. 验证登录
sudo bin/mysql -u root -p
```
> [!QUESTION] caching_sha2_password vs mysql_native_password?
> MySQL 8.0 将默认认证插件从 `mysql_native_password` 切换为 `caching_sha2_password`:
> - **caching_sha2_password**(新默认):更强的 SHA2 加密缓存机制,推荐所有新版客户端使用
> - **mysql_native_password**(旧版):兼容性最好,老旧语言驱动可能不支持 SHA2
>
> *如果你的连接报 `client does not support authentication protocol` 错误,临时解决方案是切换回旧版认证:*
> ```sql
> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'new_password';
> ```
>
> *但长期方案应是升级客户端库。*
## 配置文件 my.cnf
### 配置文件加载优先级
MySQL 按以下顺序查找配置文件,**后读取的覆盖先读取的**(命令行参数始终最高):
```mermaid
graph LR
E["~/.my.cnf"] --> D["$MYSQL_HOME/my.cnf"]
D --> C["/etc/mysql/my.cnf"]
C --> B["/etc/my.cnf"]
B --> A["--defaults-file=<指定路径>"]
A --> F["命令行参数"]
F -.->|"最终生效"| Z["生效配置"]
```
> [!NOTE] 关键理解
> - `/etc/my.cnf` 通常是入口文件,内部可能 `!include` 其他目录;
> - `--defaults-file` 是**唯一**使用的配置文件,跳过所有默认路径(极少场景使用);
> - Docker 中常用 `--config` 或 `conf.d/` 目录方式挂载自定义配置。
### 核心配置参数详解
MySQL 配置集中在 `[mysqld]` 段落下。按功能域分类如下:
#### 网络
```ini
# ==================== 网络 ====================
port = 3306 # MySQL 默认端口
bind-address = 0.0.0.0 # 监听所有网卡(Docker 内用 127.0.0.1)
max_connections = 500 # 最大并发连接数
max_connect_errors = 1000000 # 同一主机连续中断次数上限,超限将被 block_host
```
#### 字符集
```ini
# ==================== 字符集 ====================
character-set-server = utf8mb4 # 支持 emoji 的完整 UTF-8 实现
collation-server = utf8mb4_unicode_ci # 不区分大小写的排序规则
```
*为什么用 `utf8mb4` 而非 `utf8`?* MySQL 的 `utf8` 只支持最多 3 字节,无法存储 emoji;`utf8mb4` 才是标准 UTF-8(最长 4 字节)。
#### InnoDB 存储引擎
```ini
# ==================== InnoDB ====================
innodb_buffer_pool_size = 1G # 缓存数据和索引的物理内存占比
innodb_log_file_size = 512M # Redo Log 文件大小
innodb_flush_log_at_trx_commit = 1 # ACID 一致性级别控制
innodb_flush_method = O_DIRECT # 绕过 OS page cache,避免双重缓冲
innodb_file_per_table = 1 # 每张表独立 .ibd 文件
innodb_io_capacity = 2000 # SSD 建议调高,HDD 约 100~200
```
**关键概念解释:**
| 参数 | 原理 | 调整建议 |
|------|------|---------|
| `innodb_buffer_pool_size` | InnoDB 的核心缓存区域,存放数据页和索引页 | **物理内存的 50%~70%**——为 OS 和其他进程预留空间,占满 90% 反而导致性能下降 |
| `innodb_log_file_size` | Redo Log 保证持久性(WAL 机制) | 越大容纳未刷盘事务越多,但重启恢复越慢;`512M` 是通用起点 |
| `O_DIRECT` | 直接 I/O 模式 | 启用后 MySQL 自己管理 Buffer Pool,不再经过 OS Cache(避免双重缓冲浪费) |
### 慢查询日志
```ini
# ==================== 日志 ====================
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
log_error = /var/log/mysql/error.log
```
**使用方式:** 启用后可以通过 `mysqldumpslow` 分析或直接用文本工具查看超时查询:
```bash
# 找出执行时间最长的 10 条
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# 实时追踪慢查询(运维调试利器)
tail -f /var/log/mysql/slow.log
```
### Binlog(二进制日志)配置
Binlog 是 MySQL 最核心的日志类型,用于主从复制和数据恢复:
```ini
# ==================== Binlog ====================
server_id = 1 # 集群内唯一标识(1~2^32-1)
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW # 记录每一行变更明细
binlog_expire_logs_seconds = 604800 # 7 天后自动清理
max_binlog_size = 100M # 单文件上限(实际可能略超)
gtid_mode = ON # 全局事务 ID,简化主从切换
enforce_gtid_consistency = ON # 只允许 GTID 安全的事务
```
> [!TIP] Binlog Format 对比
> | 格式 | 内容 | 优点 | 缺点 |
> |------|------|------|------|
> | **STATEMENT** | 原始 SQL | 文件小,审计直观 | 函数调用可能导致主从不一致 |
> | **ROW**(推荐) | 每行变更前后值 | 数据一致性最强 | 文件较大 |
> | **MIXED** | 混合模式 | 兼顾两者 | 逻辑复杂,难以预测 |
>
> **结论**:生产环境统一用 `ROW`,现代磁盘成本已不再是瓶颈。
> [!QUESTION] innodb_flush_log_at_trx_commit 怎么选?
> | 值 | 行为 | 丢失数据风险 | 性能 |
> |---|------|------------|------|
> | **1** | 每次事务 commit 都刷盘 | **零**(ACID 完全符合) | 最低 |
> | **2** | 每次 commit 写入 OS 缓存,每秒刷盘 | 系统断电丢失 < 1 秒 | 高 |
> | **0** | 每秒都写并刷盘,commit 仅写入缓冲区 | 可能丢多秒数据 | 最高 |
>
> **建议**:金融/支付类业务必须用 1;内容类业务可以用 2 换取性能提升。
## mysql_install_db 初始化原理
当首次启动 MySQL(或手动执行 `mysql_install_db`)时,底层会完成以下几件事:
```mermaid
flowchart TD
S["mysqld 启动"] --> I["数据目录检查"]
I -->|为空| D["执行 mysql_install_db"]
I -->|非空| C["跳过初始化,直接加载现有数据"]
D --> A["创建系统数据库"]
A --> A1["mysql schema — 用户权限、角色、插件"]
A --> A2["sys schema — 性能分析视图集合"]
A --> A3["performance_schema — 运行时指标采集"]
A --> A4["information_schema — SQL 元数据虚拟表"]
A --> B["创建管理员账户"]
B --> B1["root@localhost — 超级管理员"]
B --> B2["使用 caching_sha2_password 认证"]
A --> E["写入其他系统文件"]
E --> E1["ibdata1 — InnoDB 系统表空间"]
E --> E2["auto.cnf — Server UUID"]
C --> F["加载 my.cnf 配置并监听端口"]
```
**逐段拆解:**
| 步骤 | 说明 | 常见关联问题 |
|------|------|-------------|
| 创建 system schemas | `mysql` 存权限,`performance_schema` 供性能监控,`information_schema` 是 SQL 标准提供的元数据字典 | 删了这些库会导致实例无法正常运行 |
| 创建 root 账户 | 8.0 不再允许匿名访问 `test` 库;密码通过临时日志输出而非自动设为空 | 忘记临时密码 → 删除 data dir 重新初始化 |
| 初始化 InnoDB 表空间 | `ibdata1` 存放系统表和 undo log;`ib_logfile*` 是 Redo Log(两份互为镜像) | Redo Log 损坏 → 实例无法启动 |
| 写入 auto.cnf | 包含唯一的 Server UUID,主从复制和 GTID 依赖此标识 | 克隆虚拟机未重置 UUID → 主从冲突 |
### 数据目录结构
初始化完成后,数据目录的组织方式如下:
```
/var/lib/mysql/ (datadir 根目录)
├── ibdata1 # InnoDB 系统表空间(存放系统表 + undo log)
├── ib_logfile0 # Redo Log 文件 1
├── ib_logfile1 # Redo Log 文件 2(与 1 互为镜像)
├── auto.cnf # Server UUID(UUID 不变,主从复制不冲突)
├── mysql/ # 系统数据库(user, role, db, columns_priv 等 .ibd 文件)
├── performance_schema/ # 运行时性能数据采集
├── sys/ # 基于 performance_schema 的友好视图
├── app_db/ # 每个用户数据库一个目录
│ ├── users.ibd # 单表独立表空间(innodb_file_per_table=1)
│ └── orders.ibd
└── relay-log.index # 主从复制的中继日志索引(仅作为 replica 时存在)
```
## 关联笔记
- [[hhs/Redis/01-安装与部署]] — Redis 的安装方式与 MySQL 对比
- [[hhs/EXAM/Week05]] — Docker Compose 多服务编排示例
- [[hhs/DEV/Go-Database]] — Go 驱动连接 MySQL 的参数配置
@@ -0,0 +1,345 @@
---
tags: [MySQL, 客户端, CLI, 工具]
create time: 2026-05-16T00:00
---
# MySQL 客户端工具
## 概述
本文件汇总 MySQL 生态中的各类客户端工具及其用法,从终端到图形界面,帮助你在不同场景下高效地操作数据库。
> [!TIP] 如何选择客户端?
> - **日常排查** → `mysql` CLI 或 DataGrip / DBeaver
> - **脚本自动化** → `mysql -N -s` 非交互式模式
> - **结构设计与 ER 图** → MySQL Workbench
> - **多库种混用** → DBeaver 社区版
> - **Cluster 管理** → `mysqlsh` JS 模式
## mysql CLI 命令行客户端
`mysql` 是 MySQL 自带的交互式客户端,适合快速调试、脚本自动化和远程服务器操作。
### 基本连接
```bash
# 最简方式(本地,root 无密码模式)
mysql
# 指定用户、主机、端口
mysql -u root -p -h 127.0.0.1 -P 3306
# 通过 Unix Socket 连接(更快,绕过 TCP 栈)
mysql -u root -S /var/run/mysqld/mysqld.sock
# SSH 隧道方式(连接远程服务器上的 MySQL)
# 另开终端执行 tunnel:
ssh -L 3306:127.0.0.1:3306 user@remote-server
# 然后本地直接连:
mysql -u root -p --protocol=TCP
```
> [!NOTE] 三种连接通道对比
> | 方式 | 延迟 | 适用场景 | 安全 |
> |------|------|---------|------|
> | TCP (`-h 127.0.0.1`) | ~1ms | 本地开发、负载均衡后端 | 明文传输 |
> | Unix Socket (`-S`) | <1μs | 本机直连,性能最优 | 文件系统权限控制 |
> | SSH Tunnel | ~10-50ms | 跨机房、生产环境 | SSH 加密通道 |
```bash
# 使用 SSL 连接生产环境(推荐)
mysql -u app_user -p --ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--ssl-cert=/etc/mysql/client-cert.pem \
--ssl-key=/etc/mysql/client-key.pem \
-h prod-db.example.com
# 压缩传输大结果集
mysql -u root -p --compress -h 192.168.1.10 -e "SELECT * FROM large_table;"
```
### 配置文件加载顺序
`mysql` 启动时按以下顺序读取配置文件,后读到的覆盖前面的值:
```bash
# 查看当前配置加载路径(从低优先级到高优先级)
mysql --print-defaults
```
```text
mysql would have been started with the following arguments:
--socket=/var/run/mysqld/mysqld.sock --port=3306 --default-character-set=utf8mb4
```
> [!IMPORTANT] 常见配置优化项
>
> 在 `~/.my.cnf`(仅自己可见,chmod 600)中声明默认参数,避免每次手动输入:
> ```ini
> [client]
> user = myuser
> host = 127.0.0.1
> port = 3306
> password = secret
> default-character-set = utf8mb4
>
> [mysql]
> auto-rehash # 自动补全表名/列名(默认开启)
> column-widths = 120 # 加大显示宽度,避免截断
> ```
> ⚠️ 安全提醒:`password` 明文写入配置文件存在风险。生产环境中建议使用 `mysql_config_editor` 生成 `.mylogin.cnf` 加密登录文件:
> ```bash
> mysql_config_editor set --login-path=local --host=localhost --user=root --password
> # 之后只需:
> mysql --login-path=local
> ```
### 常用快捷命令
在 `mysql>` 提示符下,以 `\` 开头的命令是**客户端内置指令**,不会发送到服务端:
```sql
\h -- 显示帮助
\? -- 同 \h
\G -- 竖排输出(长字段友好)
\t -- 切换 Tab / CSV 格式输出
\c -- 取消当前输入
\u db_name -- 切换数据库
\d new_delim -- 修改语句终止符(分号冲突时用)
\q -- 退出
\e -- 用 $EDITOR 编辑当前 SQL(打开 vi/nano)
\s -- 查看服务器状态(版本、字符集等)
\. filename -- 执行 SQL 脚本(与 source 等价)
pager less -- 设置分页输出(大结果集必备)
nopager -- 恢复默认
```
> [!TIP] 实用技巧:`\e` + Enter 编辑
>
> 当 SQL 较长时,直接敲 `\e` 回车会打开 `$EDITOR` 指定的编辑器,写好 SQL 保存退出后自动执行。配合 `set editor=vim` 可自定义编辑器。
### 竖排输出示例
```sql
mysql> SELECT * FROM users WHERE id = 1\G
*************************** 1. row ***************************
id: 1
username: alice
email: alice@example.com
created_at: 2026-01-15 10:30:00
updated_at: 2026-05-10 14:22:00
profile_json: {"age": 28, "city": "Shanghai", "role": "admin"}
status: 1
1 row in set (0.00 sec)
```
> [!EXAMPLE] 什么时候用 `\G`?
>
> 当一个表的字段很多(如 JSON 类型的大字段),横排输出会被截断或挤在一起。竖排模式下每个字段独占一行,可读性大幅提升。
### 非交互式执行
```bash
# 直接执行 SQL 字符串
mysql -u root -p -e "SELECT COUNT(*) FROM users;"
# 从文件导入(常用于初始化建表)
mysql -u root -p db_name < init.sql
# 导出到文件(适合小数据量提取)
mysql -u root -p -e "SELECT * FROM users;" db_name > output.csv
# 静默模式(适合脚本解析输出)
mysql -s -N -e "SHOW DATABASES;"
# -s = silent(紧凑格式),-N = 不显示列名
```
> [!TIP] 管道组合技巧
>
> 将 mysql 与其他 unix 工具组合,实现更强大的数据处理能力:
> ```bash
> # 只提取某列并去重计数
> mysql -N -u root -p -e "SELECT role FROM users;" db_name | sort | uniq -c | sort -rn
>
> # 实时观察查询变化(类似 watch)
> watch -n 1 "mysql -N -u root -p -e 'SELECT COUNT(*) FROM orders WHERE status=\"pending\";' app_db"
> ```
## mysqlsh(MySQL Shell)
`mysqlsh` 是 Oracle 官方新一代管理工具,支持 **SQL**、**JavaScript**、**Python** 三种模式,内置丰富的 DBA API,是 InnoDB Cluster 管理的核心入口。
> [!NOTE] mysql vs mysqlsh
>
> `mysql` 是最原始的客户端,专注纯 SQL 交互;而 `mysqlsh` 更像是一个「数据库开发平台」——它提供了面向对象的数据访问 API、结构化输出格式化,以及集群管理的完整工具链。建议在生产运维场景中优先使用 `mysqlsh`。
### 启动与模式切换
```bash
# 默认进入 SQL 模式
mysqlsh
# JavaScript 模式(连接 Cluster 管理)
mysqlsh --js
dba.createCluster('myCluster')
# Python 模式
mysqlsh --py
# 一步直达指定实例(自动进入 SQL 模式)
mysqlsh root@192.168.1.10:3306
mysqlsh://root@prod-db:3306/app_db # 指定初始库
```
### 实用功能
```javascript
// ---- JS 模式:Schema 级别的操作 ----
const schema = dba.getSchema('app_db');
schema.getTable('users').select().where('status = 1').limit(10).execute();
// ---- SQL 模式:结构化输出 ----
\output json // 输出切换为 JSON,便于后续处理
SELECT * FROM users LIMIT 5;
\output text // 切回文本
// ---- 元数据概览 ----
\status // 连接状态
\dba.status() // InnoDB Cluster 集群状态
\connect app_user@app-db // 切换连接
```
```python
# ---- Python 模式:批量 CRUD ----
session = db.create_session("app_user@prod-db:3306/app_db")
result = session.sql("SELECT id, name FROM products WHERE price > 100").execute()
for row in result.fetch_all():
print(f"{row[0]}: {row[1]}")
```
> [!TIP] mysqlsh 的隐藏技能
>
> - **自动生成文档**: `\status --json` 可直接喂给日志分析系统
> - **对象浏览器**: tab 自动补全对 `dba`, `session`, `schema` 全部生效
> - **代码片段**: 输入 `dba.` 后 tab 可查看所有可用方法(createCluster, cloneInstance, checkInstanceConfiguration 等)
## 图形界面工具对比
| 工具 | 类型 | 平台 | 特点 | 费用 |
|------|------|------|------|------|
| **MySQL Workbench** | 官方 GUI | Win/Mac/Linux | ER 图设计、迁移工具、执行计划可视化 | 免费 |
| **DBeaver** | 社区万能 | Win/Mac/Linux | 支持几乎所有数据库,插件丰富 | 社区版免费 |
| **HeidiSQL** | Windows 首选 | Windows | 轻量、启动快、批量操作方便 | 免费开源 |
| **Navicat** | 商业旗舰 | Win/Mac/Linux | 功能最全面、同步/备份/结构设计一体化 | 付费 |
| **DataGrip** | JetBrains | Win/Mac/Linux | IDE 集成好、代码补全强大 | 付费(JetBrains 全家桶) |
| **TablePlus** | 现代审美 | Mac/Win | 原生应用体验流畅、快捷键优秀 | 付费 |
### 工具选择决策流程
```mermaid
flowchart TD
A[开始选型] --> B{操作系统}
B -->|"macOS"| C["TablePlus / DataGrip"]
B -->|"Windows"| D{预算?}
B -->|"Linux / 跨平台"| E["DBeaver / DataGrip"]
D -->|"免费"| F["HeidiSQL"]
D -->|"付费"| G{"需要多库兼容?"}
G -->|"是"| H["Navicat"]
G -->|"仅 MySQL"| I["MySQL Workbench"]
C --> J["搭配 mysql CLI 完成全流程"]
F --> J
H --> J
I --> J
E --> J
```
### DBeaver 快速上手
```sql
-- 在 DBeaver 中可以享受的功能:
-- 1. Ctrl+Space 智能代码补全
-- 2. Ctrl+Shift+F 格式化 SQL
-- 3. Ctrl+/ 单行注释,Ctrl+Shift+/ 块注释
-- 4. 右键表 -> Open Table Data 直接浏览数据
-- 5. EXPLAIN 分析器可视化展示执行计划
```
> [!TIP] DBeaver 小技巧
>
> - 选中某张表按 **F5** 刷新元数据(DDL 变更后必做)
> - 右键查询结果 → Export 支持 Excel/CSV/JSON/SQL INSERT 多种格式
> - 「SQL Editor」支持数据源模板(Template),一键插入常用查询骨架
## 其他命令行工具
```bash
# ---- mysqlimport ----
# 批量导入 CSV 数据到表中
mysqlimport -u root -p --local --fields-terminated-by=',' app_db data.csv
# ---- mysqldump ----
# 逻辑备份(后续章节详述)
mysqldump -u root -p --single-transaction --routines --triggers app_db > backup.sql
# ---- mysqlbinlog ----
# 解析 Binary Log
mysqlbinlog --start-datetime="2026-05-01 00:00:00" mysql-bin.000001 > decoded.sql
# ---- mysqlcheck ----
# 检查和修复表
mysqlcheck -u root -p --auto-repair --check --optimize app_db
# ---- perror ----
# 查看 MySQL 错误码含义
perror 1064
# 1064 = You have an error in your SQL syntax
```
### mysqldump 进阶用法
```bash
# 只导结构,不导数据(适合迁移 schema)
mysqldump -u root -p --no-data app_db > schema.sql
# 只导数据,不导结构(适合增量同步)
mysqldump -u root -p --no-create-info app_db > data.sql
# 排除特定表(下划线前缀的配置表)
mysqldump -u root -p --ignore-table=app_db._config --ignore-table=app_db._history app_db > backup.sql
# 分库备份(循环所有库)
mysqldump -u root -p --all-databases --single-transaction --master-data=2 > all_dbs_$(date +%F).sql
# 按时间段还原(误删恢复利器)
mysqlbinlog --stop-datetime="2026-05-15 14:32:00" mysql-bin.000001 | mysql -u root -p
```
### mysqlbinlog 定位故障时间线
```bash
# 查看所有事件(含二进制语句)
mysqlbinlog --force-read mysql-bin.000001 | grep -iE "BEGIN|COMMIT|DROP|TRUNCATE|ALTER"
# 只看到具体的 SQL(不含内部细节)
mysqlbinlog --base64-decode mysql-bin.000001 | grep -C 3 "DELETE FROM users"
# 按 pos 点精确恢复(精准到事务级别)
mysqlbinlog --start-position=154 --stop-position=892 mysql-bin.000001 | mysql -u root -p
```
> [!IMPORTANT] 冷备份 vs 热备份
>
> - `mysqldump --single-transaction`:**热备份**,基于 MVCC 一致性快照,不影响线上写入(InnoDB 专属)
> - `mysqldump --lock-all-tables`:**冷备份**,全局读锁,写请求全部阻塞(MyISAM 需此方式)
> - `mydumper`:多线程逻辑备份工具,速度远胜 mysqldump,适合百 GB 级以上数据库
## 关联笔记
### MySQL 系列
- [[hhs/MySQL/README]] — MySQL 知识库总目录
- [[hhs/MySQL/01-入门基础/02-安装与初始化]] — 安装方法与初始化配置
- [[hhs/MySQL/07-高可用与分布式/34-备份与恢复]] — mysqldump、xtrabackup 完整指南
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — binlog 原理与 mysqlbinlog 深入
- [[hhs/MySQL/08-工程实践/39-安全加固]] — 账号权限、SSL、审计最佳实践
- [[hhs/MySQL/01-入门基础/01-MySQL 架构与入门]] — 理解客户端与服务端通信基础
@@ -0,0 +1,305 @@
---
tags: [MySQL, 数据类型, INT, VARCHAR, JSON]
create time: 2026-05-16 00:30
---
# MySQL 数据类型全景
## 概述
MySQL 的数据类型可按存储的内容分为六大类。**选型的黄金法则**:用满足业务需求的最小类型,节省空间、提升索引效率与查询性能。
| 大类 | 包含类型 | 核心考量 |
|------|---------|---------|
| **整数** | TINYINT / SMALLINT / MEDIUMINT / INT / BIGINT | 够用就行,别上来就 BIGINT |
| **浮点/定点** | FLOAT / DOUBLE / DECIMAL | **金额永远用 DECIMAL** |
| **字符串** | CHAR / VARCHAR / TEXT 系列 | 定长选 CHAR,不定长选 VARCHAR |
| **日期时间** | DATE / TIME / DATETIME / TIMESTAMP / YEAR | 跨时区用 TIMESTAMP,否则 DATETIME |
| **JSON** | JSON | 灵活扩展字段,搭配 Generated Column 做索引 |
| **枚举/集合** | ENUM / SET | 值域固定且极少变才考虑 |
| **二进制** | BINARY / VARBINARY / BLOB 系列 | 存文件用对象存储,数据库里只存引用 |
> [!QUESTION] 为什么总强调"用小的就行"?
> 想象一张百万行的订单表,如果主键用了 `BIGINT(8 bytes)` 而非 `INT(4 bytes)`,仅此一列就多花 4MB。这还没算上二级索引——InnoDB 的二级索引叶子节点会完整存储主键值,每个二级索引同样多花 4MB。表越大,连锁放大效应越惊人。
### 整数类型
```mermaid
graph TD
INT_TYPES["整数类型家族"]
INT_TYPES --> TINY["TINYINT<br/>1 byte | ±128"]
INT_TYPES --> SMALL["SMALLINT<br/>2 bytes | ±32K"]
INT_TYPES --> MED["MEDIUMINT<br/>3 bytes | ±8M"]
INT_TYPES --> INTN["INT<br/>4 bytes | ±21亿"]
INT_TYPES --> BIG["BIGINT<br/>8 bytes | ±9.2×10^18"]
style INT_TYPES fill:#5F2799,color:#fff
style TINY fill:#C44569,color:#fff
style SMALL fill:#C44569,color:#fff
style MED fill:#C44569,color:#fff
style INTN fill:#C44569,color:#fff
style BIG fill:#FF6B6B,color:#000
```
| 类型 | 有符号范围 | 无符号范围 | 存储 | 典型用途 |
|------|-----------|-----------|------|---------|
| **TINYINT** | -128 ~ 127 | 0 ~ 255 | 1 byte | 状态标识、布尔标志、年龄 |
| **SMALLINT** | -32K ~ 32K | 0 ~ 65K | 2 bytes | 短编码 ID |
| **MEDIUMINT** | -8M ~ 8M | 0 ~ 16M | 3 bytes | 中等范围计数 |
| **INT** | ±21 亿 | 0 ~ 42 亿 | 4 bytes | 常规自增主键、外键 |
| **BIGINT** | ±9.2×10¹⁸ | — | 8 bytes | Snowflake ID、金额计算 |
> [!TIP] 能用小的就不用大的
> `TINYINT UNSIGNED` 能存 255,够用就别用 `INT`。每列差 3 字节,百万行就是 3MB 额外开销,还会导致索引变厚、缓存命中率下降。
### 浮点与定点
| 类型 | 精度 | 存储 | 适用场景 |
|------|------|------|---------|
| **FLOAT** | 单精度 7 位 | 4 bytes | 科学计算、不需要精确的场景 |
| **DOUBLE** | 双精度 15 位 | 8 bytes | 高精度科学计算 |
| **DECIMAL(M,D)** | 精确定点 M-D 位整数 + D 位小数 | 可变(约每 9 位数字 4 字节) | **金额计算!绝对不要用浮点数存钱** |
```sql
-- ❌ 错误示范:浮点数误差累积
SELECT 0.1 + 0.2; -- 结果可能是 0.30000000000000004
-- ✅ 正确做法:DECIMAL
CREATE TABLE orders (
amount DECIMAL(10, 2) NOT NULL -- 最大 99999999.99
);
INSERT INTO orders VALUES (19.99 + 29.99); -- 精确等于 49.98
```
> [!QUESTION] 为什么不能用 FLOAT/DOUBLE 存金额?
> IEEE 754 浮点数无法精确表示 0.1 这样的十进制小数。在财务场景中,微小的舍入误差经过多次加减后会累积成显著差异。DECIMAL 以字符串形式存储每一位数字,保证运算精确。
## 字符串类型
| 类型 | 存储规则 | 最大长度 | 特点 |
|------|---------|---------|------|
| **CHAR(n)** | 固定长度,不足空格填充 | 0~255 | 适合长度固定的数据(MD5 hash、状态码) |
| **VARCHAR(n)** | 变长,前缀记录实际长度 | 0~65535(受行大小限制) | 最常用的字符串类型 |
| **TINYTEXT** | 1 字节长度前缀 | 255 | 极短文本 |
| **TEXT** | 2 字节前缀 | 65535 | 文章摘要、评论 |
| **MEDIUMTEXT** | 3 字节前缀 | 16MB | 长文章、富文本 |
| **LONGTEXT** | 4 字节前缀 | 4GB | 超大文本(日志、JSON 文档) |
### CHAR vs VARCHAR 的选择
```sql
-- ✅ 适合 CHAR:定长数据
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
status TINYINT DEFAULT 1,
country_code CHAR(3) NOT NULL, -- ISO 3166-1 alpha-3
phone_prefix CHAR(4), -- +86, +1, +44...
email VARCHAR(255) UNIQUE -- 不定长,用 VARCHAR
);
-- 为什么 gender 有时用 CHAR(1) 而非 TINYINT?
-- CHAR(1) 语义更明确,但本质上两者存储相同。
-- 关键是保持一致性——团队规范比个人偏好更重要。
```
> [!NOTE] VARCHAR 的长度陷阱
> MySQL 行大小上限 65535 字节,但这不只是所有 VARCHAR 加起来的大小。还要考虑:
> - 每列的 1~2 字节长度前缀
> - NULL 位图(允许 NULL 的列)
> - 实际存储使用 **utf8mb4** 的话,每个字符最多占 4 字节
>
> 所以 `VARCHAR(255)` 在 utf8mb4 下最大占用 255 × 4 + 2 ≈ 1022 字节。
## 日期和时间类型
| 类型 | 格式 | 存储 | 时区感知 |
|------|------|------|---------|
| **DATE** | `YYYY-MM-DD` | 3 bytes | 无 |
| **TIME** | `HH:MM:SS` | 3 bytes | 无(时间段) |
| **DATETIME** | `YYYY-MM-DD HH:MM:SS` | 8 bytes | 无(存储原始值) |
| **TIMESTAMP** | `YYYY-MM-DD HH:MM:SS` | 4 bytes | **有**(UTC 存储,显示时转换) |
| **YEAR** | `YYYY` | 1 byte | — |
```sql
-- TIMESTAMP 的时区自动转换特性
CREATE TABLE events (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
event_time DATETIME, -- 存入什么就读出什么
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 自动填当前 UTC 时间
);
INSERT INTO events (name, event_time) VALUES ('Meeting', '2026-05-16 14:00:00');
-- 在上海时区 (UTC+8) 显示:14:00:00
-- 在纽约时区 (UTC-4) 显示:14:00:00(DATETIME 不变)
-- 但如果用 TIMESTAMP,它会转换成纽约本地时间 02:00:00
```
> [!WARNING] TIMESTAMP 有保质期
> `TIMESTAMP` 的范围是 `1970-01-01 00:00:01` 到 `2038-01-19 03:14:07`(32-bit 上限)。如果你的系统需要支持 2038 年之后的数据,请用 `DATETIME`。
## JSON 类型
MySQL 5.7+ 引入原生 JSON 类型,支持部分查询和索引能力。
```sql
CREATE TABLE employees (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
attributes JSON -- 灵活扩展字段
);
INSERT INTO employees VALUES
(1, 'Alice', '{"dept": "Engineering", "skills": ["Go", "Python"], "level": 5}'),
(2, 'Bob', '{"dept": "Marketing", "skills": ["SEO", "Content"], "level": 3}');
-- JSON 路径查询
SELECT name, attributes->>'$.dept' AS department
FROM employees
WHERE attributes->>'$.level' >= 4;
-- JSON 数组包含判断
SELECT name FROM employees
WHERE JSON_CONTAINS(attributes->'$[*]', '"Go"');
-- 生成虚拟列 + 索引(最佳实践)
ALTER TABLE employees
ADD COLUMN dept VARCHAR(50) GENERATED ALWAYS AS (attributes->>'$.dept') VIRTUAL,
ADD INDEX idx_dept (dept);
```
```mermaid
flowchart LR
A["JSON Column<br/>原始存储"] --> B["JSON Document"]
B --> C["Scalar Values"]
B --> D["Arrays"]
B --> E["Nested Objects"]
C --> F["Generated Column"]
D --> F
E --> F
F --> G["Index<br/>Virtual / Stored"]
style A fill:#00B6BC,color:#fff
style F fill:#FF9F43,color:#000
style G fill:#C44569,color:#fff
```
> [!TIP] JSON vs 规范化表设计
> - **适合 JSON**:配置项、标签集合、表单动态字段、低频更新的属性
> - **不适合 JSON**:需要 JOIN 关联、频繁条件过滤、强一致性约束的字段
> - 关键技巧:用 **Generated Column + Index** 让 JSON 字段的筛选走索引
## 枚举与集合
```sql
-- ENUM: 限定可选值列表
CREATE TABLE tasks (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(200),
status ENUM('todo', 'in_progress', 'done', 'cancelled') DEFAULT 'todo',
priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium'
);
-- SET: 多选值(逗号分隔存储)
CREATE TABLE tags (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
categories SET('tech', 'business', 'lifestyle', 'health')
);
INSERT INTO tags VALUES (1, 'Go Tips', 'tech,business');
```
### ENUM vs SET 对比
| 特性 | ENUM | SET |
|------|------|-----|
| **语义** | 单选:从列表中取一个值 | 多选:从列表中取零个或多个值 |
| **内部存储** | 整数索引(1 byte 或 2 bytes) | 位图(最多 64 个成员占 8 bytes) |
| **排序规则** | 按索引顺序,非字母序 | 按位数值排序 |
| **典型场景** | 工单状态、优先级 | 标签分类、权限角色 |
### ENUM 还是 TINYINT?这是永恒争论
```sql
-- 方案 A:ENUM — 数据库层校验 + 省空间
status ENUM('open', 'closed') NOT NULL -- 1 byte
-- 方案 B:TINYINT — 代码层校验 + 灵活
status TINYINT UNSIGNED NOT NULL -- 1 byte
-- 应用层保证只写入 0/1/2/3
```
| 维度 | 选 ENUM | 选 TINYINT |
|------|---------|-----------|
| **校验** | 数据库自动拒绝非法值 | 需应用层保证 |
| **调试** | 查出是数字 2,还要查 schema 才知道含义 | 直接读出原始数字,一目了然 |
| **修改成本** | 增删值需 `ALTER TABLE`(大表很贵) | 随时在应用层加常量枚举 |
| **代码可追溯性** | IDE 找不到所有使用处 | `grep` 就能定位 |
> [!WARNING] ENUM 的反模式警告
> - **不要用 ENUM 存用户可见的文案**——后台查出来是 `2`,前端还得映射回去
> - **不要把 ENUM 当文档用**——队友看不懂 `status = 3` 是什么意思
> - **推荐方案**:中小项目用 TINYINT + Go `const` / Python `IntEnum` 在代码里维护枚举定义;只有在值域极稳定且不需要跨语言共享时再用原生 ENUM
> [!NOTE] ENUM 的本质
> ENUM 在内部存储为整数索引(1, 2, 3...),而不是字符串。这意味着:
> - ENUM 的排序是按索引而非字母顺序
> - 插入不在列表中的值会导致错误(或空字符串,取决于 sql_mode)
> - 修改 ENUM 列表顺序会影响已有数据的解释——**谨慎维护**
## 二进制类型
| 类型 | 说明 | 典型用途 |
|------|------|---------|
| **BINARY(n)** | 定长二进制 | 哈希值(SHA256 = 32 bytes)|
| **VARBINARY(n)** | 变长二进制 | 短二进制数据 |
| **TINYBLOB** | ≤ 255 bytes | — |
| **BLOB** | ≤ 65KB | 缩略图、序列化对象 |
| **MEDIUMBLOB** | ≤ 16MB | 文件附件 |
| **LONGBLOB** | ≤ 4GB | 大文件存储 |
```sql
-- 存储 SHA-256 hash 的推荐方式
CREATE TABLE file_metadata (
id BIGINT PRIMARY KEY,
filename VARCHAR(500),
sha256_hash BINARY(32) NOT NULL, -- CHAR(64) HEX 也可以,但 BINARY 省一半
UNIQUE KEY uk_sha256 (sha256_hash)
);
```
### BINARY vs VARBINARY — 定长与变长的选择
```sql
-- BINARY(32):始终占 32 bytes,不足补 0x00
INSERT INTO t VALUES (X'61'); -- 实际存储: 61 00 00 ... 00(32 bytes)
SELECT HEX(col) FROM t; -- 输出: 610000...(永远 64 个十六进制字符)
-- VARBINARY(32):只存真实长度 + 1 byte 长度前缀
INSERT INTO t VALUES (X'61'); -- 实际存储: 61 01(2 bytes,01 表示长度)
SELECT HEX(col) FROM t; -- 输出: 61(只有 2 个十六进制字符)
```
| 维度 | BINARY(n) | VARBINARY(n) |
|------|-----------|-------------|
| **存储** | 固定 n 字节,右侧用 `0x00` 补齐 | 实际长度 + 1~2 bytes 前缀 |
| **比较规则** | 补齐 0x00 后再逐字节比较 | 按实际长度比较 |
| **适用场景** | 哈希值、加密密钥等定长数据 | 短二进制流、序列化片段 |
> [!TIP] BINARY vs CHAR 的对称性
> 理解 `BINARY` 就理解了 `CHAR`——它们是对称的定长类型。区别仅在于:**CHAR 补空格,BINARY 补零**。同理 `VARBINARY` 对标 `VARCHAR`。类比记忆比死记硬背更可靠。
> [!WARNING] 大文件不要存在数据库里
> BLOB 系列看似方便,但会带来三个问题:
> 1. **备份膨胀**——数据库 dump 体积翻倍,恢复时间成倍增加
> 2. **内存压力**——SELECT 整行时 BLOB 内容也加载到内存,即使你不需要它
> 3. **无法 CDN 加速**——图片/附件走 OSS + Signed URL 是标准做法
>
> **最佳实践**:数据库只存文件引用(OSS URL / 文件系统路径),文件本体放对象存储。
## 关联笔记
- [[hhs/GORM/02-模型定义]] — GORM Struct Tag 如何映射这些 MySQL 类型
- [[hhs/Redis/02-核心数据类型]] — MySQL 与 Redis 数据类型的选型对比
@@ -0,0 +1,272 @@
---
tags: [MySQL, 字符集, Collation, utf8mb4, utf8mb3, character_set]
create time: 2026-05-16 00:00
---
# 字符集与排序规则
## 概述
字符集(Character Set)决定「哪些字符能被存储」,排序规则(Collation)决定「这些字符如何比较和排序」。选错字符集不仅会导致乱码,还可能引发索引失效和安全漏洞。
## 字符集速览
MySQL 支持超过 80 种字符集,但生产环境中真正有用的只有几个:
```mermaid
graph TB
subgraph "单字节"
A["latin1"] -->|"遗留系统"| A1["西欧语言"]
end
subgraph "多字节变长"
B["utf8<br/>MySQL 特有"] -->|"仅支持 BMP"| B1["最多 3 字节"]
C["utf8mb4"] -->|"完整 UTF-8"| C1["最多 4 字节"]
end
subgraph "中文相关"
D["gbk"] -->|"国标简体"| D1["2 字节"]
E["gb2312"] -->|"老国标"| E1["2 字节"]
F["big5"] -->|"繁体(台湾)"| F1["2 字节"]
end
subgraph "Unicode 其他"
G["ucs2"] -->|"UTF-16 BE"| G1["定长 2 字节"]
H["utf32"] -->|"UTF-32"| H1["定长 4 字节"]
end
style C fill:#00B6BC,color:#fff
style B fill:#EE5A24,color:#fff
```
### 为什么一定要用 utf8mb4?
MySQL 中的 `utf8` 实际上只是 **utf8mb3** —— 它最多支持 3 字节编码,无法表示四字节字符(emoji、生僻汉字、部分符号)。
```sql
-- 用 utf8 插入 emoji 会报错!
SET NAMES utf8;
INSERT INTO posts (content) VALUES ('Hello 🚀');
-- ERROR 1366: Incorrect string value: '\xF0\x9F\x9A\x80'
-- 换 utf8mb4 就没有问题
SET NAMES utf8mb4;
INSERT INTO posts (content) VALUES ('Hello 🚀'); -- OK ✅
```
> [!WARNING] utf8mb4 的性能影响
> 相比 latin1,utf8mb4 每个字符多占 1~3 字节。这会带来:
> - 同样的 VARCHAR 长度存的内容更少
> - 索引体积增大,缓存命中率下降
> - 网络传输量增加
>
> **建议**:除非明确知道不需要中文/emoji,否则一律使用 utf8mb4。现代 SSD 和内存足够应对这个开销。
## 排序规则 Collation
排序规则命名格式:`<字符集>_<语言>_<后缀>`,后缀含义:
| 后缀 | 含义 | 示例 |
|------|------|------|
| `_ci` | Case **I**nsensitive(忽略大小写) | `utf8mb4_general_ci` |
| `_cs` | Case **S**ensitive(区分大小写) | `utf8mb4_general_cs` |
| `_bin` | Binary(按字节比较) | `utf8mb4_bin` |
> [!NOTE] MySQL 8.0 的新后缀
> 升级到 8.0 后,你会发现默认排序规则多了两个新后缀:
> - `_ai_ci` = **A**ccent **I**nsensitive — 忽略重音差异(é = e)
> - `_as_ci` = **A**ccent **S**ensitive — 保留重音差异(é ≠ e)
>
> 这解决了一个历史痛点:`utf8mb4_general_ci` 在处理带重音的拉丁字母时不够精确。
### 常见 Collation 对比
```mermaid
flowchart TB
subgraph "不区分大小写 ci"
GC["utf8mb4_general_ci<br/>⚡ 快速但略不精确"]
UC["utf8mb4_unicode_ci<br/>📐 基于 Unicode 标准"]
AC["utf8mb4_0900_ai_ci<br/>✅ MySQL 8.0 默认"]
end
subgraph "区分大小写"
GS["utf8mb4_general_cs"]
US["utf8mb4_unicode_cs"]
AS["utf8mb4_0900_as_ci<br/>Accent/Space Insensitive"]
end
subgraph "严格二进制 bin"
B2["utf8mb4_bin<br/>直接比较字节值"]
end
IN1["输入 'abc' vs 'ABC'"] --> GC
IN1 --> UC
IN1 --> AC
IN2["输入 'abc' vs 'ABC'"] --> GS
IN2 --> US
IN2 --> AS
IN3["输入 'A' vs 'a'"] --> B2
GC --> R1["结果: a = b = c"]
UC --> R1
AC --> R1
GS --> R2["结果: a ≠ A"]
US --> R2
AS --> R2
B2 --> R3["结果: A ≠ a (0x41 ≠ 0x61)"]
style AC fill:#00B6BC,color:#fff
style B2 fill:#EE5A24,color:#fff
style GC fill:#C44569,color:#fff
```
### 该选哪个 Collation?
| 场景 | 推荐 | 理由 |
|------|------|------|
| **中文项目** | `utf8mb4_0900_as_ci` | MySQL 8.0 默认排序,基于 ICU 标准 |
| **英文项目** | `utf8mb4_0900_ai_ci` | AI = Accent Insensitive,忽略重音符号 |
| **邮箱/用户名** | `utf8mb4_bin` | 严格区分大小写 |
| **密码存储** | 永远不用 Collation——用 Hash | bcrypt/argon2 |
> [!TIP] 一个常见的选型误区
> 很多人看到中文项目就选 `utf8mb4_unicode_ci`,但在 MySQL 8.0 下推荐优先使用 `utf8mb4_0900_*` 系列。它们基于 ICU 标准,对亚洲语言(中日韩)的排序更准确,性能也更好。`utf8mb4_unicode_ci` 本质上是 Unicode 4.0.0 的实现,而 0900 基于 Unicode 9.0.0。
```sql
-- 设置整个数据库的默认字符集和排序规则
CREATE DATABASE app_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
-- 单个表的覆盖
CREATE TABLE usernames (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_bin; -- 用户名严格区分大小写
```
## 层级关系:字符集 → Collation → 作用域
```mermaid
graph BT
subgraph "Server 层(全局默认)"
S["server<br/>utf8mb4_0900_ai_ci"]
end
subgraph "Database 层(库级覆盖)"
DB["database<br/>utf8mb4_unicode_ci"]
end
subgraph "Table 层(表级覆盖)"
T["table<br/>utf8mb4_bin"]
end
subgraph "Column 层(列级最高优先级)"
C["column<br/>utf8mb4_general_ci"]
end
S --> DB --> T --> C
INFO["优先级:Column > Table > Database > Server<br/>每层都会覆盖上层的设置"]
style S fill:#5F2799,color:#fff
style DB fill:#3D97BE,color:#fff
style T fill:#FF9F43,color:#000
style C fill:#C44569,color:#fff
style INFO fill:#333,color:#fff
```
查询当前配置:
```sql
-- 查看所有层级的字符集和排序规则
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'collation_server';
SHOW CREATE DATABASE app_db;
SHOW CREATE TABLE users\G
-- 当前会话的字符集
SHOW SESSION VARIABLES LIKE 'character_set%';
```
## Go 应用层字符集配置
光在数据库层面设置 utf8mb4 还不够——连接通道必须一致,否则客户端发送的字节会被服务端用错误的字符集解析,轻则乱码,重则报错。
### DSN 中指定 charset
```go
// ✅ 标准写法:DSN 中加上 charset=utf8mb4
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := sql.Open("mysql", dsn)
```
> [!QUESTION] 为什么 DSN 里也要写 charset?
> MySQL 客户端和服务端建立连接时有一个「握手阶段」,双方会协商使用哪个字符集。如果不在 DSN 中声明,MySQL 驱动会使用服务器默认的字符集。当服务器默认不是 utf8mb4 时(比如 legacy 系统的 latin1),就会出现连接层数据面编码不一致的问题。
### 运行时动态切换
```go
// 某个极端场景下需要临时切换会话字符集
_, err := db.Exec("SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci")
// ⚠️ 注意:这影响整个连接,在连接池场景下要避免频繁调用
```
> [!WARNING] 连接池中的陷阱
> `sql.Open` 不会立即创建连接,而是在首次查询时懒加载。如果你的程序启动后从未执行过 SET NAMES,而服务端的 `character_set_server` 恰好不是 utf8mb4 —— 那第一个请求就可能触发乱码。
>
> **最佳实践**:在初始化连接池后立刻跑一次健康检查,确保连接已正确初始化。
```go
// 建完连接池后立即验证
err = db.Ping() // 这一步会实际创建一个连接
```
## utf8 → utf8mb4 迁移指南
这是一个经典的线上改造场景。如果你的历史系统用了 MySQL 的 `utf8`(其实是 utf8mb3),逐步迁移到 utf8mb4 的步骤如下:
### 迁移步骤
```sql
-- Step 1: 检查当前哪些表用了旧 utf8
SELECT table_schema, table_name, column_name, character_set_name
FROM information_schema.columns
WHERE character_set_name IS NOT NULL
AND character_set_name != 'utf8mb4'
ORDER BY table_schema, table_name;
-- Step 2: 修改表的默认字符集(不影响已有数据)
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Step 3: 修改数据库默认字符集(新建表会自动继承)
ALTER DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Step 4: 修改应用层 DSN,确保连接层也使用 utf8mb4
-- user:pass@tcp(host:3306)/db?charset=utf8mb4&parseTime=True
```
> [!NOTE] ALTER TABLE CONVERT 的影响
> - 对于小表(万行级别),几乎是瞬时完成
> - 对于大表(千万行+),`CONVERT TO` 会重建整张表:复制全量数据 → 重建索引 → 替换原表
> - **建议在低峰期操作**,或使用 `pt-online-schema-change` 进行在线 DDL,避免业务中断
>
> 如果只是改默认字符集而不影响现有列定义,可以用:
> ```sql
> ALTER TABLE users DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
> -- 注意:这是 DEFAULT,不改变已有列的字符集!
> ```
### 常见踩坑
| 坑 | 现象 | 解法 |
|----|------|------|
| **只改了表,没改连接** | 能插入 emoji,但查出来是 `???` | DSN 加 `charset=utf8mb4` |
| **只改了表默认值,没 CONVERT** | 已有列仍是 utf8mb3 | 必须用 `CONVERT TO` 重新编码 |
| **GORM AutoMigrate 覆盖** | 手动改了字符集,下次 AutoMigrate 又被还原 | 禁用自动迁移,改用 migration 脚本管理 |
### 关联笔记
- [[hhs/Redis/02-核心数据类型]] — Redis 的 STRING 类型对多字节字符的处理
- [[hhs/GORM/02-模型定义]] — GORM 建模时的字符串长度设置要点
- [[hhs/MySQL/02-SQL核心/06-DDL 建表与结构变更]] — 大表 DDL 的在线变更策略
- [[hhs/MySQL/08-工程实践/40-常见踩坑]] — 隐式转换等更多常见问题
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog Format(ROW vs STATEMENT)对字符集的影响
+24
View File
@@ -0,0 +1,24 @@
---
tags: [MySQL, Database, 入门]
create time: 2026-05-20 23:55
---
# 一、入门基础
## 概述
本章从零开始介绍 MySQL:它的整体架构长什么样、怎么装起来、用什么工具连接、以及数据类型和字符集这些最基础但最容易踩坑的知识点。
## 本章文档
| # | 主题 | 说明 |
|---|------|------|
| 1 | [[hhs/MySQL/01-入门基础/01-MySQL 架构与入门]] | MySQL 分几层(客户端 → SQL → 引擎 → 存储);`mysqld` 是怎么启动的 |
| 2 | [[hhs/MySQL/01-入门基础/02-安装与初始化]] | 本地安装、Docker Compose 快速启动、基本配置文件 |
| 3 | [[hhs/MySQL/01-入门基础/03-客户端工具]] | 命令行 `mysql`、DBeaver、Workbench,选一个顺手的就行 |
| 4 | [[hhs/MySQL/01-入门基础/04-数据类型全景]] | 整型、浮点、字符串、日期时间、JSON——每种类型适合存什么 |
| 5 | [[hhs/MySQL/01-入门基础/05-字符集与排序规则]] | 为什么一定要用 `utf8mb4`、中文搜索不出来的坑 |
## 关联笔记
- [[hhs/MySQL/README]] — MySQL 知识库总目录