编程闭关教程 · 系列 08–20 · 编制日期 2026-10-14

MySQL 专题:从安装到 Go 集成到备份,一册全通

数据库四件套 ①。你用过数据库但没系统训练,本页把"最小必要理论 + 照做命令 + 完整可复制 Go 代码 + 常见报错表"一次给全:本机 Docker 起 8.4.12、1GB Linux 服务器原生 apt 部署、database/sql 与 GORM 两种集成、备份与定时脚本、低配调优——全程不跳出教程。
本册承诺一册在手:本机 10 分钟起一个 8.4.12 容器、工作台用环境变量切到 MySQL 跑通 CRUD 与事务、1GB 服务器原生部署并压好内存、配好每天自动备份与一次恢复演练。学完即具备"网站业务主力库"的独立交付能力。
8.4.12
LTS 锁定版本(2026-08-18)
1 周
学时(W10–12 主体)
5
个完整 Go 示例(含事务)
1 套
mysqldump + cron 备份方案
12 条
常见报错对照表
版本纪律:只用 LTS 线,锁版本见 09 版本总表;1GB 服务器不装 Docker(原生安装);工作台默认 SQLite,DB_DRIVER=mysql 切换。术语口径与 08 §8.1 四数据库分工一致。
08 总纲 09 环境·版本 10 Go 内核 11 工作台 12 Vue3 13 Flutter 14 Python 15 C/C++ 16 MySQL 17 PostgreSQL 18 Redis 19 SQLite3 20 部署
本页目录
  1. 01 · 版本与选型:为什么锁 8.4 LTS
  2. 02 · 安装:本机 Docker 与 Linux 原生 apt 两条路
  3. 03 · 核心概念(最小必要)
  4. 04 · SQL 速查(可照抄)
  5. 05 · Go 集成:三种方案完整代码
  6. 06 · 常见场景与最佳实践
  7. 07 · 备份、运维与 1GB 低配调优
  8. 08 · 常见报错对照表(12 条)
  9. 09 · 本模块验收清单
  10. 10 · 来源表
01

版本与选型:为什么锁 8.4 LTS

MySQL 官方是"双轨制":一条 LTS(长期支持,每三年一个大版本,稳定),一条 innovation(创新版,年年小版本尝鲜)。按版本纪律规则 2"只用稳定版 + 支持窗口中点",我们只锁 LTS。

线当前版本(2026-10)取舍说明
LTS 线8.4.12 LTS(2026-08-18)本教程锁定。8.4 是长期支持版,补丁持续到 2032 年前后;语法、认证插件、系统表成熟,驱动生态全部兼容。
新 LTS 线9.7 LTS(9.7.3 补丁)已发布但刚进入支持窗口前段,老驱动/运维脚本兼容性待观察;按规则 2 不抢"刚发布",本教程不用。
innovation 线26.7.x尝鲜特性版,支持窗口短(约 1 个月下一版就换名),生产禁用。知道存在即可。

为什么是 MySQL 而不是 SQLite/PG

按 08 §8.1 分工表:SQLite 是单机/工作台默认;MySQL 是网站/业务系统主力(生态最大、招人最多、你日常点名);PG 留给复杂数据/金融型业务(见 17 模块);Redis 是缓存加速层(见 18 模块)。

本机怎么跑

Windows 本机用 Docker Desktop 4.94.0(Docker Engine 29.8.2)起一个 8.4.12 容器,命令见 §02。不在 Windows 上装原生 MySQL——原生安装又重又难卸载,容器最干净。

1GB 服务器怎么跑

1GB 内存的 Linux 服务器不装 Docker(Docker Desktop 官方要求 1.5GB 起,容器层额外吃内存),直接 apt 原生装 mysql-server,并按 §07 压内存。

一句话选型:本机容器开发、服务器原生部署、版本钉死 8.4.12 LTS——这条路线全程 1GB 内存能跑,且与工作台 DB_DRIVER 开关无缝衔接。

02

安装:本机 Docker 与 Linux 原生 apt 两条路

两条路都给完整命令。先本机(开发调试),再服务器(原生部署)。装完统一做三件事:首次登录、建库建用户、确认字符集 utf8mb4。

2.1 路线 A · 本机 Docker Desktop(开发用)

前置:已装 Docker Desktop 4.94.0(安装见 09 模块)。在一个空目录建 docker-compose.yml,内容如下,root 密码、端口、数据卷全部写死,照抄即可。

# docker-compose.yml —— 本机开发用 MySQL 8.4.12 LTS
services:
  mysql:
    image: mysql:8.4
    container_name: mysql84
    restart: unless-stopped
    # 端口映射:本机 3306 -> 容器 3306;若本机已占 3306 可改左侧为 13306
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: "DevRoot#2026"      # root 密码,仅开发用,别用于生产
      MYSQL_DATABASE: "workbench"              # 首次启动自动建库
      MYSQL_USER: "wb"                         # 首次启动自动建用户并授权上面的库
      MYSQL_PASSWORD: "WbPass#2026"
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_0900_ai_ci
    volumes:
      - mysql84_data:/var/lib/mysql           # 数据卷,删容器不丢数据

volumes:
  mysql84_data:

同目录下启动(PowerShell):

docker compose up -d
docker compose ps                 # 看到 mysql84 状态 running 即成功
docker compose logs -f mysql       # 看启动日志,出现 "ready for connections" 再连

进入 mysql 命令行:

docker exec -it mysql84 mysql -uroot -pDevRoot#2026

2.2 路线 B · Linux 服务器原生 apt(1GB 低配,Debian 13 "trixie")

1GB 服务器用 Debian 13.7 stable(系统选型见 09 模块)。Debian trixie 官方仓库自带的 mysql-server 就是 8.4 系列,直接 apt 装,不需要加 Oracle 第三方源(加源反而引入维护负担)。

# 以 root(或 sudo)执行。Debian 13 trixie 仓库内置 mysql-server 8.4
apt update
apt install -y mysql-server
systemctl enable --now mysql
systemctl status mysql            # active (running)

# 首次安全初始化:改 root 密码、删匿名用户、禁 root 远程、删测试库
mysql_secure_installation
关于版本来源:Debian 13 stable 仓库锁定的是 trixie 当时的 8.4 补丁线,由 Debian 安全团队回移植补丁,与上游 8.4.12 属同一 LTS 大版本、语法与认证完全兼容。本教程的 SQL 与 Go 代码在两条路上行为一致。若你确需精确到上游 8.4.12,可改用 Oracle APT 仓库,但 1GB 小机器不建议——Debian 打包版内存更友好。

2.3 装后统一动作:首次登录、建库建用户、字符集

无论哪条路,连进去后执行下面这段(生产用户不给 root,只给业务库权限):

-- 1) 确认服务器字符集是 utf8mb4(8.4 默认即 utf8mb4,这里复核)
SHOW VARIABLES LIKE 'character_set_server';   -- 期望 utf8mb4

-- 2) 建业务库,明确字符集与排序规则
CREATE DATABASE IF NOT EXISTS workbench
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_0900_ai_ci;

-- 3) 建业务用户,只允许本机/内网连(生产不要 %)
CREATE USER IF NOT EXISTS 'wb'@'127.0.0.1' IDENTIFIED BY 'WbPass#2026';
GRANT ALL PRIVILEGES ON workbench.* TO 'wb'@'127.0.0.1';
FLUSH PRIVILEGES;

-- 4) 验证:用业务用户连,能看到 workbench 库
SHOW DATABASES;

字符集纪律:永远用 utf8mb4(不是旧的 utf8——MySQL 的 utf8 是阉割版,存不下 emoji)。建库、建表、连接 DSN 三处都要 utf8mb4,乱码问题从源头消灭(报错表 §08 有对照)。

03

核心概念(最小必要)

只讲你写业务真正用得上的。教科书式铺开的(如锁的所有粒度、B+树全部细节)这里点到为止,够用即可。

3.1 库 / 表 / 行 / 列 与主键

3.2 索引:B+树与联合索引最左前缀

索引让查询从"全表扫描 N 行"变成"树查找 log(N) 行"。InnoDB 的索引是 B+树:叶子节点存整行数据(主键索引)或主键值(二级索引)。你需要记住两条实战规则:

规则 1 · 给 WHERE/ORDER/JOIN 条件建索引

高频过滤、排序、关联的列建索引;但不要给每列都建——索引占空间、拖慢 INSERT/UPDATE。一张表 3–5 个索引是常态。

规则 2 · 联合索引遵守最左前缀

建 INDEX(a,b,c) 后,查询条件必须从最左列 a 开始连续命中才走索引:WHERE a=?、WHERE a=? AND b=? 走;WHERE b=?、WHERE b=? AND c=? 不走。

3.3 事务 ACID 与隔离级别

事务把"一组写操作"绑成要么全成、要么全败。ACID 中你实际要调的是隔离级别——它决定"一个事务能看到别的未提交事务的什么数据"。8.4 InnoDB 默认 REPEATABLE READ,对绝大多数业务不用改。

隔离级别解决了什么 / 残留什么你的取舍
READ UNCOMMITTED能读到别人未提交的数据(脏读),啥都不防不用。
READ COMMITTED防脏读;同一事务内两次读同一行可能不同(不可重复读)Oracle/PG 默认;高并发对账场景可选。
REPEATABLE READ8.4 InnoDB 默认。防脏读 + 不可重复读;InnoDB 用间隙锁基本防住幻读保持默认,工作台/业务系统用它。
SERIALIZABLE全串行,最严格,锁最重、并发最低极端强一致才用;本教程场景不用。

3.4 EXPLAIN:读执行计划,把慢查询优化掉

一条 SQL 慢不慢,先 EXPLAIN 看。重点看两列:type(访问类型,从好到差:const > ref > range > index > ALL 全表扫描)和 key(实际用到的索引,NULL 就是没走索引)。

示例:一个慢查询逐步优化。假设有订单表 orders(user_id, status, created_at, amount),要"查某用户已完成订单、按时间倒序":

-- 慢:user_id 没索引,type=ALL,扫描全表
EXPLAIN SELECT * FROM orders
WHERE user_id = 1001 AND status = 'done'
ORDER BY created_at DESC LIMIT 20;

-- 第 1 步优化:给过滤列加联合索引(顺序=最左前缀:先 user_id 再 status,再带 created_at)
CREATE INDEX idx_uid_status_ctime ON orders (user_id, status, created_at DESC);

-- 再 EXPLAIN:type 变成 ref,key=idx_uid_status_ctime,rows 从全表降到该用户的行数;
--    ORDER BY 也被索引覆盖,不再额外 filesort。这就是"建对一个索引,慢查询秒回"。

判断标准一句话:EXPLAIN 出来 type=ALL 且 rows 很大 = 必须加索引或改写;出现 Using filesort / Using temporary 在大数据量下也要警惕。慢查询日志怎么开,见 §07。

04

SQL 速查(可照抄)

下面是工作台业务天天用到的 SQL,按 DDL / DML / 查询 / 索引 / 事务 / 函数 / 权限 分好,直接照抄改表名即可。

4.1 DDL:建表 / 改表 / 删表

-- 建表:对齐工作台 Task 模型(见工作台契约 §5)
CREATE TABLE tasks (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  title       VARCHAR(200)    NOT NULL,
  note        TEXT,
  done        TINYINT(1)      NOT NULL DEFAULT 0,
  created_at  DATETIME(6)     NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
  updated_at  DATETIME(6)     NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  deleted_at  DATETIME(6)     NULL DEFAULT NULL,          -- 软删除标记列,见 §06
  PRIMARY KEY (id),
  KEY idx_done (done),
  KEY idx_deleted (deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

ALTER TABLE tasks ADD COLUMN priority INT NOT NULL DEFAULT 0;   -- 加列
ALTER TABLE tasks MODIFY COLUMN title VARCHAR(255) NOT NULL;  改列定义
DROP TABLE IF EXISTS tasks;                                     删表(生产慎用)

4.2 DML:增 / 改 / 删

-- 插入(批量)
INSERT INTO tasks (title, note) VALUES ('写周报', '本周进展'), ('买菜', '周末');

-- 更新(永远带 WHERE!)
UPDATE tasks SET done = 1 WHERE id = 1;

-- 删除(硬删)。生产多用软删:UPDATE tasks SET deleted_at=NOW() WHERE id=1;
DELETE FROM tasks WHERE id = 1;

4.3 查询:WHERE / 排序 / 分组 / JOIN / 分页

-- 条件 + 排序 + 分页(深分页优化见 §06)
SELECT id, title, done, created_at FROM tasks
WHERE done = 0 AND deleted_at IS NULL
ORDER BY id DESC
LIMIT 20 OFFSET 0;

-- 分组统计
SELECT done, COUNT(*) AS cnt FROM tasks WHERE deleted_at IS NULL GROUP BY done;

-- JOIN:用户与其任务(工作台 User 1 : N Task)
SELECT u.username, t.title, t.done
FROM users u
JOIN tasks t ON t.user_id = u.id
WHERE u.id = 1
ORDER BY t.id DESC;

4.4 事务:BEGIN / COMMIT / ROLLBACK / SAVEPOINT

BEGIN;                                   -- 开始事务
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 两条都成功才 COMMIT;任一步出错执行 ROLLBACK 全部回滚
COMMIT;
-- ROLLBACK;

-- SAVEPOINT:事务内局部回滚
BEGIN;
INSERT INTO tasks(title) VALUES ('A');
SAVEPOINT sp1;
INSERT INTO tasks(title) VALUES ('B');
ROLLBACK TO sp1;    -- 回掉 B,保留 A
COMMIT;

4.5 常用函数与用户权限

-- 常用函数
NOW()                  -- 当前时间,带秒
UNIX_TIMESTAMP()       -- 秒级时间戳
CONCAT(a, b)           -- 拼接
IFNULL(x, 0)           -- 空值替换
DATE(created_at)       -- 取日期部分

-- 用户与权限(生产按最小授权)
CREATE USER 'wb'@'127.0.0.1' IDENTIFIED BY '强密码';
GRANT SELECT, INSERT, UPDATE, DELETE ON workbench.* TO 'wb'@'127.0.0.1';   -- 只读/应用账号不给 DROP/ALTER
SHOW GRANTS FOR 'wb'@'127.0.0.1';
REVOKE INSERT ON workbench.* FROM 'wb'@'127.0.0.1';
DROP USER 'wb'@'127.0.0.1';
05

Go 集成:三种方案完整代码

这是本模块核心。方案 A(database/sql 标准库)打功底,方案 B(GORM)是工作台实际用的,方案 C(sqlc)简介。每个方案都给"建表 → CRUD → 事务"完整可运行代码。Go 版本 1.27.x(见 09)。

5.1 方案 A · database/sql + go-sql-driver/mysql v1.10.1

标准库 database/sql 是一切 ORM 的底座。依赖:github.com/go-sql-driver/mysql v1.10.1(2026-09-02 锁定)。连接池三参数必须设,否则生产必踩坑。

-- go.mod(初始化:go mod init mysql-demo)
module mysql-demo

go 1.27

require github.com/go-sql-driver/mysql v1.10.1
// main.go —— database/sql 完整示例:连接池 + CRUD + 事务 + 预处理
package main

import (
	"database/sql"
	"fmt"
	"log"
	"time"

	_ "github.com/go-sql-driver/mysql" // 匿名注册 driver
)

func main() {
	// DSN 关键参数:parseTime=true(否则 time.Time 扫不进来)、loc=Local、charset=utf8mb4
	dsn := "wb:WbPass#2026@tcp(127.0.0.1:3306)/workbench?charset=utf8mb4&parseTime=true&loc=Local"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 连接池三参数(生产必设)
	db.SetMaxOpenConns(25)                 // 最大并发连接数
	db.SetMaxIdleConns(5)                  // 空闲保持的连接数
	db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活,防服务端主动断

	if err := db.Ping(); err != nil {
		log.Fatal("连不上 MySQL:", err)
	}
	fmt.Println("连接 OK")

	createTable(db)
	id := insertTask(db, "用 sql 写周报", "标准库版")
	getTask(db, id)
	updateTask(db, id)
	transferDemo(db)   // 事务示例
	listTasks(db)
}

func createTable(db *sql.DB) {
	_, err := db.Exec(`CREATE TABLE IF NOT EXISTS tasks (
		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
		title VARCHAR(200) NOT NULL,
		note TEXT,
		done TINYINT(1) NOT NULL DEFAULT 0,
		created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
		PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`)
	if err != nil {
		log.Fatal(err)
	}
}

// 预处理插入:? 占位符防 SQL 注入
func insertTask(db *sql.DB, title, note string) int64 {
	res, err := db.Exec("INSERT INTO tasks (title, note) VALUES (?, ?)", title, note)
	if err != nil {
		log.Fatal(err)
	}
	id, _ := res.LastInsertId()
	fmt.Println("插入成功 id =", id)
	return id
}

func getTask(db *sql.DB, id int64) {
	var title string
	var done int
	err := db.QueryRow("SELECT title, done FROM tasks WHERE id = ?", id).Scan(&title, &done)
	if err == sql.ErrNoRows {
		fmt.Println("没这条")
		return
	}
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("读到:title=%s done=%d\n", title, done)
}

func updateTask(db *sql.DB, id int64) {
	_, err := db.Exec("UPDATE tasks SET done = 1 WHERE id = ?", id)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("已标记完成 id =", id)
}

// 事务:两步写,要么全成要么全败
func transferDemo(db *sql.DB) {
	tx, err := db.Begin()
	if err != nil {
		log.Fatal(err)
	}
	// 任一失败都 rollback;全部成功才 commit
	if _, err := tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); err != nil {
		tx.Rollback()
		log.Fatal(err)
	}
	if _, err := tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2"); err != nil {
		tx.Rollback()
		log.Fatal(err)
	}
	if err := tx.Commit(); err != nil {
		log.Fatal(err)
	}
	fmt.Println("事务提交成功")
}

func listTasks(db *sql.DB) {
	rows, err := db.Query("SELECT id, title, done FROM tasks ORDER BY id DESC LIMIT 10")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close() // 必须 Close,否则连接泄漏
	for rows.Next() {
		var id int64
		var title string
		var done int
		if err := rows.Scan(&id, &title, &done); err != nil {
			log.Fatal(err)
		}
		fmt.Printf("  #%d %s done=%d\n", id, title, done)
	}
}

运行:go mod tidy 然后 go run .。

5.2 方案 B · GORM(工作台实际方案,v1.31.2)

工作台(见 11 模块)用 GORM:默认 SQLite,通过 DB_DRIVER 环境变量切 MySQL。版本坑先说清:GORM 模块 tag 是 v1.31.2,但 API 是 v2 线(全系列统一表述)。依赖:gorm.io/gorm v1.31.2 + gorm.io/driver/mysql v1.6.x(驱动随 gorm 主版本取最新兼容补丁,go.sum 锁定)。

工作台切换的核心:只换 DSN 与驱动,模型/handler 一行不改。下面是工作台 internal/db/db.go 的 MySQL 分支写法:

// internal/db/db.go —— 工作台按 DB_DRIVER 切换 sqlite / mysql(契约 §2/§7)
package db

import (
	"fmt"
	"os"
	"time"

	"github.com/glebarez/sqlite" // 工作台默认驱动(纯 Go,无 CGO)
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

func New() (*gorm.DB, error) {
	driver := os.Getenv("DB_DRIVER") // "sqlite"(默认)或 "mysql"

	cfg := &gorm.Config{
		Logger: logger.Default.LogMode(logger.Warn), // 开发可改 logger.Info 看 SQL
	}

	if driver == "mysql" {
		// MySQL DSN:用户:密码@tcp(主机:端口)/库?参数
		dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local",
			os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"),
			os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_NAME"))
		gdb, err := gorm.Open(mysql.Open(dsn), cfg)
		if err != nil {
			return nil, err
		}
		// GORM 底层就是 database/sql,连接池照样要设
		sqlDB, _ := gdb.DB()
		sqlDB.SetMaxOpenConns(25)
		sqlDB.SetMaxIdleConns(5)
		sqlDB.SetConnMaxLifetime(5 * time.Minute)
		return gdb, nil
	}

	// 默认 SQLite 分支(工作台本地默认)
	return gorm.Open(sqlite.Open(os.Getenv("DB_PATH")), cfg)
}

本地用 MySQL 模式跑工作台,只需设环境变量(PowerShell):

$env:DB_DRIVER="mysql"
$env:DB_USER="wb"; $env:DB_PASSWORD="WbPass#2026"
$env:DB_HOST="127.0.0.1"; $env:DB_PORT="3306"; $env:DB_NAME="workbench"
$env:JWT_SECRET="本地开发随机串"
go run ./cmd/server

GORM 的 CRUD / 自动迁移 / 事务 / 钩子,对照工作台模型(契约 §5 的 User、Task):

// CRUD + 自动迁移 + 事务 + 钩子(MySQL 与 SQLite 行为一致)
import "gorm.io/gorm"

// 自动迁移:建表/加列(不删列不改类型),开发期用,生产用迁移工具更稳
db.AutoMigrate(&User{}, &Task{})

// Create
task := Task{Title: "用 GORM 写周报", Note: "ORM 版"}
db.Create(&task)

// Read:主键查 / 条件查
var t Task
db.First(&t, 1)                          // SELECT * FROM tasks WHERE id=1
db.Where("done = ?", false).Find(&tasks) // 条件列表

// Update
db.Model(&t).Update("done", true)
db.Model(&t).Updates(Task{Done: true, Note: "改了"})

// Delete:默认是硬删;配 gorm.DeletedAt 字段即变软删(见 §06)
db.Delete(&t, 1)

// 事务:func 闭包版,自动 commit/rollback
db.Transaction(func(tx *gorm.DB) error {
	if err := tx.Model(&Account{}).Update("balance", gorm.Expr("balance - ?", 100)).Error; err != nil {
		return err // 返回 error 自动回滚
	}
	if err := tx.Model(&Account{}).Where("id=?", 2).Update("balance", gorm.Expr("balance + ?", 100)).Error; err != nil {
		return err
	}
	return nil // 返回 nil 自动提交
})

// 钩子:写库前后自动触发,如创建时加密密码
func (u *User) BeforeCreate(tx *gorm.DB) error {
	u.Password = hashBCrypt(u.Password) // bcrypt 哈希,见 09 版本自核验项
	return nil
}

方案 B 的纪律:模型与 handler 写成"DB_DRIVER 无关",只在 db.go 一处感知数据库。这样本地 SQLite、服务器 MySQL 共用一套代码——这正是工作台契约 §2 的设计本意。

5.3 方案 C(简介)· sqlc v1.31.1 类型安全 SQL 生成

sqlc 一句话定位:你写原生 SQL,它用 Go 类型安全地生成代码——既避免手写 Scan(&...) 错位,又没有 ORM 的黑盒。锁定 v1.31.1(2026-04-22)。适合对 SQL 可控性要求高、不想学 ORM 的场景;工作台仍以 GORM 为主,这里给最小示例:

-- query.sql —— 你写 SQL,注释里命名生成的函数
-- name: GetTask :one
SELECT id, title, done FROM tasks WHERE id = ?;

-- name: ListTasks :many
SELECT id, title, done FROM tasks ORDER BY id DESC LIMIT ?;

-- name: CreateTask :execresult
INSERT INTO tasks (title, note) VALUES (?, ?);
# 配置好 sqlc.yaml(指定引擎 mysql)后一行生成类型安全的 Go 代码
sqlc generate
# 产物里直接拿到 Queries.GetTask(ctx, id) 这种带类型的函数,编译期就对字段。
# 与 database/sql 的关系:sqlc 底层仍生成 database/sql 调用,DSN/连接池设置与方案 A 完全相同。
06

常见场景与最佳实践

业务开发高频遇到的六个点,每个给"怎么做 + 一句原因"。

场景怎么做为什么
分页小偏移用 LIMIT 20 OFFSET n;深分页(offset 上万)改用"游标分页":WHERE id < 上次最大id ORDER BY id DESC LIMIT 20。OFFSET 越大扫的行越多;游标分页永远只扫 20 行。
唯一键防重对业务唯一字段加 UNIQUE KEY(如 username);并发重复插入靠唯一约束兜底,捕获 1062 错误。先查再插在并发下有竞态;数据库唯一约束是最后防线。
软删 vs 硬删业务数据用软删:加 deleted_at 列,GORM 配 gorm.DeletedAt;硬删只用于日志/临时数据。所有查询带 deleted_at IS NULL。软删可恢复、可审计;硬删误删难救。
时间字段用 DATETIME(6) 存 UTC 或本地;DSN 带 parseTime=true&loc=Local;Go 侧用 time.Time。不带 parseTime,time.Time 扫描直接报错(见 §08)。
慢查询定位开 slow_query_log(§07),对慢 SQL 跑 EXPLAIN,按 §3.4 加索引。慢查询是 1GB 服务器最大的内存/CPU 杀手。
连接数管理Go 侧 SetMaxOpenConns(25) 等三参数(§5.1);服务器 max_connections 与应用连接数总和匹配(§07)。连接开爆 = Too many connections 全线拒绝服务。
07

备份、运维与 1GB 低配调优

生产库"能跑"只是开始,"丢不了"才是底线。本节给:全量备份命令、cron 定时脚本、恢复步骤、binlog 一句话、以及 1GB 服务器的内存压法。

7.1 mysqldump 全量备份(一条命令)

# 全量备份 workbench 库到带时间戳的文件(单服务器备份)
mkdir -p /var/backups/mysql
mysqldump -u root -p'强密码' \
  --single-transaction \   # InnoDB 一致性快照,不锁表(关键)
  --routines --triggers \
  workbench | gzip > /var/backups/mysql/workbench_$(date +%F).sql.gz

# 恢复:先建空库再导入
mysql -u root -p'强密码' -e "CREATE DATABASE IF NOT EXISTS workbench DEFAULT CHARSET utf8mb4;"
gunzip -c /var/backups/mysql/workbench_2026-10-14.sql.gz | mysql -u root -p'强密码' workbench

binlog 一句话:binary log 记录所有写操作,可做"时间点恢复"(备份后某时刻误删,先恢复全量备份、再重放 binlog 到误删前一秒)。本教程低配场景先靠每日全量 + binlog 保留 7 天,足够;深度运维见 20 模块。

7.2 cron 定时备份脚本(完整,含清理旧备份)

# /usr/local/bin/mysql-backup.sh —— 每天凌晨 3:10 跑,保留最近 7 天
#!/bin/bash
set -euo pipefail
BK_DIR=/var/backups/mysql
mkdir -p "$BK_DIR"
FILE="$BK_DIR/workbench_$(date +%F).sql.gz"

mysqldump -u root -p'强密码' --single-transaction --routines --triggers workbench | gzip > "$FILE"

# 只留最近 7 天,防止磁盘被备份撑爆
find "$BK_DIR" -name 'workbench_*.sql.gz' -mtime +7 -delete

echo "$(date) backup -> $FILE"
# 加可执行权限 + 挂到 root crontab(crontab -e)
chmod +x /usr/local/bin/mysql-backup.sh
# crontab 行:每天 03:10 执行
10 3 * * * /usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1
备份纪律:备份文件要定期(每月)做一次"恢复演练"——没恢复过的备份等于没有备份。本模块验收 §09 就要求你真做一次恢复演练。

7.3 1GB 低配服务器调优(关键,照抄 my.cnf)

1GB 内存机器上 MySQL 默认配置会 OOM。核心是把 innodb_buffer_pool_size 压小(它默认会拿物理内存的一半左右,1GB 机器拿一半给数据库,系统和 Go 进程就没了)。编辑 /etc/mysql/my.cnf(或 Debian 的 /etc/mysql/mysql.conf.d/mysqld.cnf):

# /etc/mysql/mysql.conf.d/zz-lowmem.cnf —— 1GB 服务器专用,改完 systemctl restart mysql
[mysqld]
innodb_buffer_pool_size = 96M        # 压到 64–128M,小库足够;留内存给系统+Go进程
innodb_log_file_size    = 48M
innodb_flush_log_at_trx_commit = 2   # 低配牺牲一点持久性换性能(有备份兜底)
max_connections         = 30         # 1GB 机器别开大;Go 侧连接池总和要 < 此值
slow_query_log          = 1          # 开慢查询日志
long_query_time         = 1.0        # 超过 1 秒记为慢查询
slow_query_log_file     = /var/log/mysql/slow.log
skip-name-resolve                  # 反查 DNS 拖慢连接,关掉

# 配完重启并确认内存占用
systemctl restart mysql
free -h          # 看 available 还剩多少给 Go 进程

swap 兜底

1GB 机器务必开 1–2GB swap 文件,OOM 时先换出而非直接杀进程。Go 二进制本身很省内存,把 buffer_pool 压到 96M 后通常能稳定跑。具体 swap 配置见 20 模块。

双写/额外开销

低配下 innodb_doublewrite 可保留(它防崩溃坏页,代价不大);若内存仍紧张再考虑。优先压 buffer_pool,而不是关安全特性。

7.4 何时用 MySQL vs SQLite / PG

按 08 §8.1 分工表复述一遍,帮你在具体项目里做判断:

场景选谁理由
个人工具/原型/单机SQLite单文件零运维,1GB 机器首选;工作台本地默认(见 19 模块)。
给客户的网站/管理系统MySQL(本模块)多用户并发写、要独立服务、生态招人多、你点名常用。
金融/复杂查询/JSONB/严格事务PostgreSQL功能最强、类型最丰富(见 17 模块)。
缓存/限流/队列/会话Redis内存加速层,不是主存储(见 18 模块)。
08

常见报错对照表(12 条)

闭关期间卡住,先查这张表。每条都是真实会遇到的报错原文 + 原因 + 照做解法。

报错原文(节选)原因解决
Access denied for user 'wb'@'localhost' (using password: YES)密码错 / 用户不存在 / 来源 IP 不匹配核对密码;确认建用户时 host 是 127.0.0.1 还是 localhost;8.4 默认 caching_sha2_password,go-sql-driver v1.10.1 已支持,DSN 正常即可,别用老驱动。
Unknown database 'workbench'库名拼错或没建库SHOW DATABASES; 看;按 §2.3 建库。
Table 'workbench.tasks' doesn't exist表没建(GORM 自动迁移没跑)确认 db.AutoMigrate() 执行过;或按 §4.1 手动建表。
Error 1062: Duplicate entry 'xxx' for key 'username'违反唯一键,重复插入业务上这是"防重生效";捕获 1062 转为"用户名已存在"提示。
Error 1205: Lock wait timeout exceeded行锁等待超时:长事务没提交/回滚缩短事务、别在事务里做远程调用;查 information_schema.innodb_trx 找卡住的事务并 KILL。
Can't connect to MySQL server on '127.0.0.1:3306' (connect)服务没起 / 端口不对 / 防火墙挡systemctl status mysql;容器用 docker compose ps;核对端口(本机若改了 13306,DSN 同步改);云服务器安全组放行 3306(生产建议只放内网)。
Error 1040: Too many connections连接数打满(应用连接池总和 > max_connections)Go 侧设 SetMaxOpenConns(25);服务器 max_connections 与连接池总和匹配(§7.3);查 SHOW PROCESSLIST; 找泄漏。
Error 1153: Got a packet bigger than 'max_allowed_packet' bytes一次插入/查询的包超过上限(如传大 TEXT/BLOB)调大 max_allowed_packet=64M;或分批插入,别一次塞超大字段。
sql: unknown driver "mysql" (forgotten import?)忘了匿名导入驱动包确认有 import _ "github.com/go-sql-driver/mysql";GORM 方案则确认导入 gorm.io/driver/mysql。
Error 1366: Incorrect string value / 中文变 ????字符集不是 utf8mb4(库/表/连接三处之一)建库建表用 utf8mb4;DSN 带 charset=utf8mb4;已有的表用 ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4。
Scan error on column index N: unsupported Scan... into *time.TimeDSN 没带 parseTime=trueDSN 加 parseTime=true&loc=Local(§5.1),重连。
Error 2006: MySQL server has gone away连接被服务端主动断开(空闲超时 > wait_timeout)Go 侧设 SetConnMaxLifetime(5*time.Minute) 小于服务端 wait_timeout;数据库/sql 会自动重连,业务不用管。
09

本模块验收清单

两项硬验收 + 一组自查。全部打勾,本模块才算通关,可进入 17 PostgreSQL 专题。

验收 ① · 工作台 MySQL 模式本地跑通

本机 Docker 起 8.4.12;设 DB_DRIVER=mysql 及连接环境变量启动工作台;curl /healthz 正常;登录、任务增删改查在 MySQL 库表中真实落库(用 SELECT * FROM tasks; 亲眼看到数据)。

验收 ② · 完成一次备份与恢复演练

跑一次 §7.1 的 mysqldump 命令生成备份;在表里删一行制造"误删";按恢复步骤把备份导入到一个临时库,确认被删的行回来了。cron 脚本已就位。

自查清单(全勾才算过)

10

来源表

本页版本号全部来自全系列唯一版本事实来源(09 版本总表 / versions.md)。

MySQL 8.4.12 LTS 发布说明(2026-08-18);9.7 LTS / 26.7 innovation 双轨说明https://dev.mysql.com/doc/relnotes/mysql/8.4/en/
go-sql-driver/mysql v1.10.1(2026-09-02);caching_sha2_password 支持说明https://github.com/go-sql-driver/mysql/releases
GORM v1.31.2(v2 API 线,模块 tag v1.x 序列)与 mysql 驱动https://pkg.go.dev/gorm.io/gorm · https://pkg.go.dev/gorm.io/driver/mysql
sqlc v1.31.1(2026-04-22),类型安全 SQL 生成https://github.com/sqlc-dev/sqlc/releases
Docker Desktop 4.94.0 / Docker Engine 29.8.2(本机容器);1GB 服务器不装 Docker 的依据https://docs.docker.com/desktop/release-notes/ · https://docs.docker.com/engine/release-notes/29/
Debian 13.7 "trixie" stable 仓库内置 mysql-server 8.4 打包https://www.debian.org/releases/
mysqldump --single-transaction、InnoDB buffer pool、慢查询日志等官方手册https://dev.mysql.com/doc/refman/8.4/en/
四数据库分工(SQLite/MySQL/PG/Redis)与工作台 DB_DRIVER 契约:本系列 08 §8.1、11 工作台契约file:///D:/02-Docs/金融知识/08-闭关总纲.html · file:///D:/02-Docs/金融知识/11-工作台实战.html

(编者整理:本页的安装步骤、Go 示例、cron 脚本、my.cnf 调优参数、报错对照表为基于官方文档与工作台契约的实战化整理,无外部独立来源。)