TL;DR: 多语言数据库设计需要在翻译表、locale 列和 JSON 存储之间做出选择。本文涵盖 SaaS i18n 的 schema 模式、回退策略和查询优化。示例来自 tanstackship.com。
引言
当你的 SaaS 应用需要支持多种语言时,数据库 schema 是基础。不好的设计会导致查询复杂、性能问题和维护困难。正确的方法取决于你的内容类型和访问模式。关于 i18n 整体架构,先从我们的 i18n for SaaS: Architecture Patterns Compared 入手。
Schema 设计模式
| 模式 | 灵活性 | 查询复杂度 | 性能 | 适用场景 |
|---|---|---|---|---|
| 翻译表 | 高 | 中 | 良好 | 动态内容 |
| Locale 列 | 低 | 低 | 优秀 | 少量 locale 固定场景 |
| JSON/JSONB 列 | 中 | 低 | 良好 | 简单翻译 |
| EAV(Entity-Attribute-Value) | 很高 | 高 | 差 | 不推荐 |
| 每个 locale 独立数据库 | 最高 | 高 | 不定 | 企业多区域 |
翻译表模式(推荐)
-- 基础内容表(语言无关)
CREATE TABLE blog_posts (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
author_id TEXT NOT NULL,
published_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
-- 翻译表(每个 locale 一行)
CREATE TABLE blog_post_translations (
blog_post_id TEXT NOT NULL REFERENCES blog_posts(id),
locale TEXT NOT NULL,
title TEXT NOT NULL,
excerpt TEXT,
body TEXT NOT NULL,
meta_description TEXT,
is_auto_translated BOOLEAN DEFAULT false,
updated_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (blog_post_id, locale)
);
-- 带回退链的查询
SELECT
COALESCE(t.title, f.title) as title,
COALESCE(t.body, f.body) as body
FROM blog_posts p
LEFT JOIN blog_post_translations t
ON t.blog_post_id = p.id AND t.locale = 'de'
LEFT JOIN blog_post_translations f
ON f.blog_post_id = p.id AND f.locale = 'en'
WHERE p.slug = 'getting-started';
Locale 列模式(简单、少许 locale)
-- Locale 列:简单但不可扩展
CREATE TABLE products (
id TEXT PRIMARY KEY,
name_en TEXT NOT NULL,
name_zh TEXT,
name_de TEXT,
name_ja TEXT,
description_en TEXT NOT NULL,
description_zh TEXT,
description_de TEXT,
description_ja TEXT,
price DECIMAL(10, 2) NOT NULL
);
适合 2-3 个 locale 且 schema 简单的场景。语言多了就难以维护。
JSONB 模式(灵活)
-- JSONB 翻译:灵活但查询难度大
CREATE TABLE pages (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL,
translations JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
);
-- JSONB 内容示例
{
"en": {
"title": "About Us",
"body": "<h1>Our Story</h1><p>...</p>"
},
"de": {
"title": "Über Uns",
"body": "<h1>Unsere Geschichte</h1><p>...</p>"
},
"zh": {
"title": "关于我们",
"body": "<h1>我们的故事</h1><p>...</p>"
}
}
性能优化
| 策略 | 影响 | 实现方式 |
|---|---|---|
| (locale, slug) 复合索引 | 高 | CREATE INDEX idx_locale_slug ON blog_post_translations(locale, blog_post_id) |
| 活跃 locale 物化视图 | 中 | 预连接常见 locale 配对 |
| 翻译查询 Redis 缓存 | 高 | 首次 DB 查询后缓存 |
| 限制回退深度 | 中 | 最大 2 层回退(目标 → en) |
| 批量加载所有翻译 | 高 | 单次查询获取所有 locale,在应用层过滤 |
结语
翻译表模式对大多数 SaaS 应用是最优选择——可以扩展到任意数量的 locale、支持回退链、与 ORM 集成良好。Locale 列仅适用于 2-3 种语言的简单场景。完全避免使用 EAV 模式。始终为 locale + 查询键组合建立索引。
关于 i18n 以外的数据建模,参见 Data Modeling: Users, Organizations, and Permissions。SQL 索引最佳实践参见 SQL Indexing Strategies for Web Applications,Database Migrations: Strategies for Zero-Downtime 提供 schema 变更的部署指导。