MySQL 实践

要点

  • MySQL 是国内使用最广泛的关系型数据库,生态成熟、云服务商支持好
  • 与 PostgreSQL 相比,JSON 支持较弱,无原生向量扩展
  • 与 Hono 集成使用 Drizzle 或 Prisma,schema 定义与 PostgreSQL 类似
  • 字符集推荐 utf8mb4,避免中文和 emoji 存储问题
  • 自增主键和 UUID 主键都可以,选择取决于业务需求
  • 云托管 MySQL(RDS、CDB)是生产环境的常见选择

1. 安装与启动

1.1 本地开发

macOS:

brew install mysql
brew services start mysql

Linux(Ubuntu):

sudo apt install mysql-server
sudo systemctl start mysql

Docker:

docker run -d \
  --name mysql \
  -e MYSQL_ROOT_PASSWORD=root \
  -e MYSQL_DATABASE=hono_app \
  -p 3306:3306 \
  mysql:8

1.2 创建数据库

mysql -u root -p
CREATE DATABASE hono_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

2. 与 Hono 集成

2.1 Drizzle ORM

pnpm add drizzle-orm mysql2
pnpm add -D drizzle-kit
// src/schema.ts
import { mysqlTable, varchar, text, json, timestamp, int } from 'drizzle-orm/mysql-core'
 
export const users = mysqlTable('users', {
  id: varchar('id', { length: 36 }).primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }).notNull(),
  metadata: json('metadata'),
  createdAt: timestamp('created_at').defaultNow(),
})
 
export const conversations = mysqlTable('conversations', {
  id: varchar('id', { length: 36 }).primaryKey(),
  userId: varchar('user_id', { length: 36 }).notNull(),
  title: varchar('title', { length: 255 }).notNull(),
  model: varchar('model', { length: 100 }).notNull(),
  messageCount: int('message_count').default(0),
  createdAt: timestamp('created_at').defaultNow(),
})
// src/lib/db.ts
import { createPool } from 'mysql2/promise'
import { drizzle } from 'drizzle-orm/mysql2'
import * as schema from '../schema'
 
const pool = await createPool({
  uri: process.env.DATABASE_URL,
  connectionLimit: 20,
})
 
export const db = drizzle(pool, { schema })
// src/routes/users.ts
import { Hono } from 'hono'
import { db } from '../lib/db'
import { users } from '../schema'
import { eq } from 'drizzle-orm'
 
const app = new Hono()
 
app.get('/users', async (c) => {
  const allUsers = await db.select().from(users)
  return c.json(allUsers)
})
 
app.post('/users', async (c) => {
  const body = await c.req.json()
  await db.insert(users).values(body)
  return c.json({ ok: true }, 201)
})
 
export default app

2.2 Prisma

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        String   @id @default(uuid())
  email     String   @unique @db.VarChar(255)
  name      String   @db.VarChar(255)
  metadata  Json?
  createdAt DateTime @default(now())
 
  @@map("users")
}

3. 字符集配置

MySQL 默认字符集是 latin1,中文和 emoji 会乱码。必须用 utf8mb4

-- 创建数据库时指定
CREATE DATABASE hono_app
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;
 
-- 修改现有数据库
ALTER DATABASE hono_app
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

表级别也需要设置:

ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

utf8mb4 支持完整的 Unicode(包括 emoji),utf8 是 MySQL 的历史遗留名称,实际只支持 3 字节 UTF-8。

4. 主键选择

4.1 自增主键

import { mysqlTable, int, varchar } from 'drizzle-orm/mysql-core'
import { autoincrement } from 'drizzle-orm/mysql-core'
 
export const users = mysqlTable('users', {
  id: int('id').primaryKey().autoincrement(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }).notNull(),
})

优势:

  • 存储空间小(4 字节 vs UUID 的 16 字节或 36 字符)
  • 索引紧凑,查询性能好
  • 插入性能好(顺序写入)

劣势:

  • 可猜测,API 安全性差
  • 分布式环境会冲突

4.2 UUID 主键

import { mysqlTable, varchar } from 'drizzle-orm/mysql-core'
import { v4 } from 'uuid'
 
export const users = mysqlTable('users', {
  id: varchar('id', { length: 36 }).primaryKey().$defaultFn(() => v4()),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }).notNull(),
})

优势:

  • 不可猜测
  • 分布式友好

劣势:

  • 存储空间大
  • 随机插入导致页分裂,索引性能差

4.3 选择建议

  • 内部系统、不需要暴露 ID:自增主键
  • API 场景、需要安全性:UUID 主键
  • 高并发写入:自增主键或 UUID v7(带时间戳,可排序)

5. JSON 类型

MySQL 5.7+ 支持 JSON 类型,但功能不如 PostgreSQL 的 JSONB:

import { mysqlTable, json } from 'drizzle-orm/mysql-core'
 
export const documents = mysqlTable('documents', {
  id: varchar('id', { length: 36 }).primaryKey(),
  title: varchar('title', { length: 255 }).notNull(),
  metadata: json('metadata'),
})
// 插入 JSON
await db.insert(documents).values({
  id: 'doc-1',
  title: 'Hono Guide',
  metadata: { author: 'Alice', tags: ['hono', 'typescript'] },
})
 
// 查询 JSON 字段
import { sql } from 'drizzle-orm'
 
const docs = await db
  .select()
  .from(documents)
  .where(sql`JSON_EXTRACT(metadata, '$.author') = 'Alice'`)

MySQL 的 JSON 查询不如 JSONB 方便,不能直接建 GIN 索引。如果需要频繁的 JSON 查询,考虑用生成列 + 索引:

ALTER TABLE documents
  ADD COLUMN author VARCHAR(255) GENERATED ALWAYS AS (JSON_EXTRACT(metadata, '$.author')) VIRTUAL;
 
CREATE INDEX idx_documents_author ON documents(author);

6. 与 PostgreSQL 的差异

特性MySQLPostgreSQL
JSON 查询JSON_EXTRACT()->> 操作符
JSON 索引生成列 + 索引GIN 索引
向量支持无原生支持pgvector 扩展
UUID 类型无(用 CHAR(36))原生 UUID 类型
自增主键AUTO_INCREMENTSERIALGENERATED
事务隔离默认 REPEATABLE READ默认 READ COMMITTED
字符串函数CONCAT()SUBSTRING()`
日期函数NOW()DATE_FORMAT()NOW()TO_CHAR()

迁移代码时需要注意这些差异。使用 ORM 可以屏蔽大部分差异。

7. 云托管 MySQL

生产环境通常使用云托管 MySQL:

  • 阿里云 RDS MySQL:国内首选,生态完善
  • 腾讯云 CDB:腾讯云环境
  • AWS RDS MySQL:国际化项目
  • PlanetScale:Serverless MySQL,基于 Vitess

云托管的优势:

  • 自动备份、高可用
  • 读写分离、分库分表
  • 监控告警

连接云托管 MySQL 与本地没有区别,只是连接字符串不同:

const pool = await createPool({
  uri: process.env.DATABASE_URL,  // 云托管提供的连接字符串
})

8. 性能优化

8.1 索引

import { mysqlTable, varchar, index } from 'drizzle-orm/mysql-core'
 
export const conversations = mysqlTable('conversations', {
  id: varchar('id', { length: 36 }).primaryKey(),
  userId: varchar('user_id', { length: 36 }).notNull(),
  createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
  userIdIdx: index('idx_user_id').on(table.userId),
  createdAtIdx: index('idx_created_at').on(table.createdAt),
}))

8.2 查询分析

EXPLAIN SELECT * FROM conversations
WHERE user_id = 'user-1'
ORDER BY created_at DESC
LIMIT 10;

8.3 慢查询日志

MySQL 可以开启慢查询日志,记录执行时间超过阈值的查询:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- 1 秒

9. 适用场景

MySQL 适合以下场景:

  1. 团队熟悉 MySQL:降低学习成本
  2. 云服务商提供优质 MySQL 托管:阿里云、腾讯云
  3. 读多写少:MySQL 的读性能优秀
  4. 不需要复杂 JSON 查询或向量检索:这些场景 PostgreSQL 更合适
  5. 已有 MySQL 基础设施:避免引入新技术栈

如果项目需要向量检索、复杂 JSON 查询,或者团队没有 MySQL 经验,建议选 PostgreSQL。

总结

MySQL 是成熟稳定的关系型数据库,生态完善、云服务商支持好。

这一节涉及到的几个实践:

  1. 与 Hono 集成:Drizzle 或 Prisma,连接池管理
  2. 字符集:必须用 utf8mb4,避免中文和 emoji 问题
  3. 主键选择:自增主键性能好,UUID 安全性好
  4. JSON 类型:功能不如 PostgreSQL JSONB,频繁查询用生成列 + 索引
  5. 与 PostgreSQL 差异:语法、函数、类型有差异,ORM 可以屏蔽
  6. 云托管:生产环境常用 RDS、CDB 等托管服务
  7. 性能优化:索引、查询分析、慢查询日志

MySQL 适合团队熟悉、不需要复杂 JSON 或向量检索的项目。如果需要 pgvector 或强大的 JSON 查询,建议选 PostgreSQL。

下一篇看 SQLite 与轻量项目——SQLite 的特点、Cloudflare D1、适用场景。