Multi-Language Database Design SaaS 多语言数据库设计实战

为多语言 SaaS 应用设计数据库 schema — 翻译表、locale 存储、回退链和性能优化策略全解析。

Sam Rivera
Sam Rivera
June 1, 202613 min read
其他语言:Deutsch · English

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 独立数据库最高高不定企业多区域

翻译表模式(推荐)

sql
-- 基础内容表(语言无关)
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)

sql
-- 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 模式(灵活)

sql
-- 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 变更的部署指导。