Trang chủ / Blog / Thiết kế Database cho SaaS Multi-tenant: PostgreSQL vs Schema-per-tenant

Thiết kế Database cho SaaS Multi-tenant: PostgreSQL vs Schema-per-tenant

Thiết kế Database cho SaaS Multi-tenant: PostgreSQL vs Schema-per-tenant
## Multi-tenancy là gì?

Khi xây dựng SaaS, bạn cần quyết định kiến trúc database: mỗi khách hàng một database riêng (multi-database) hay tất cả dùng chung một database nhưng có \`tenant_id\` (shared database)?

## Option 1: Shared Database + Row-level Isolation

### Schema design

\`\`\`sql
CREATE TABLE tenants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  plan TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  email TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(tenant_id, email)
);

CREATE INDEX idx_users_tenant ON users(tenant_id);

CREATE TABLE posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES users(id),
  title TEXT NOT NULL,
  content TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_posts_tenant ON posts(tenant_id);
\`\`\`

### Row-level Security (RLS)

PostgreSQL RLS đảm bảo khách hàng không thể truy cập data của nhau:

\`\`\`sql
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation_users ON users
  USING (tenant_id = current_setting('app.current_tenant_id')::UUID);

CREATE POLICY tenant_isolation_posts ON posts
  USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
\`\`\`

### Application code (Drizzle ORM)

\`\`\`typescript
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

export async function getDbForTenant(tenantId: string) {
  const client = postgres(process.env.DATABASE_URL!);
  await client\`SELECT set_config('app.current_tenant_id', \${tenantId}, false)\`;
  return drizzle(client);
}

// Usage trong API route
export async function GET(req: Request) {
  const tenantId = req.headers.get('X-Tenant-ID');
  const db = await getDbForTenant(tenantId);
  const users = await db.select().from(usersTable); // Tự động filter theo tenant
  return Response.json(users);
}
\`\`\`

### Ưu điểm

- **Dễ quản lý**: Chỉ một database, một connection pool
- **Cost-effective**: Phù hợp với startup nhỏ, nhiều khách hàng nhỏ
- **Backup đơn giản**: Một lần backup cho tất cả

### Hạn chế

- **Noisy neighbor**: Một tenant query nặng có thể ảnh hưởng cả hệ thống
- **Khó scale**: Không thể scale riêng cho tenant lớn
- **Compliance**: Một số khách hàng yêu cầu data isolation vật lý

## Option 2: Schema-per-Tenant

Mỗi tenant một PostgreSQL schema riêng trong cùng một database:

\`\`\`sql
-- Tạo schema cho tenant mới
CREATE SCHEMA tenant_abc123;

CREATE TABLE tenant_abc123.users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL UNIQUE,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE tenant_abc123.posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES tenant_abc123.users(id),
  title TEXT NOT NULL,
  content TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
\`\`\`

### Application code

\`\`\`typescript
export async function getDbForTenant(tenantId: string) {
  const client = postgres(process.env.DATABASE_URL!);
  await client\`SET search_path TO tenant_\${tenantId}, public\`;
  return drizzle(client);
}
\`\`\`

### Ưu điểm

- **Isolation tốt hơn**: Mỗi tenant có schema riêng
- **Dễ migrate**: Có thể migrate từng tenant riêng biệt
- **Performance**: Index không bị phình to bởi data của tenant khác

### Hạn chế

- **Connection pool**: Vẫn share pool, có thể gặp bottleneck
- **Migration phức tạp**: Phải chạy migration cho từng schema

## Option 3: Database-per-Tenant

Mỗi tenant một database riêng biệt:

\`\`\`typescript
const tenantDbConfigs = {
  'tenant-abc': { host: 'db1.myapp.com', database: 'tenant_abc' },
  'tenant-xyz': { host: 'db2.myapp.com', database: 'tenant_xyz' },
};

export function getDbForTenant(tenantId: string) {
  const config = tenantDbConfigs[tenantId];
  const client = postgres(config);
  return drizzle(client);
}
\`\`\`

### Ưu điểm

- **Isolation tối đa**: Mỗi tenant hoàn toàn độc lập
- **Scale linh hoạt**: Tenant lớn có thể dùng RDS riêng với instance lớn hơn
- **Compliance**: Đáp ứng quy định bảo mật khắt khe

### Hạn chế

- **Chi phí cao**: Mỗi database tốn 1 RDS instance
- **Quản lý phức tạp**: Phải monitor nhiều database
- **Migration hell**: Migration 100 tenant = 100 lần chạy migration

## Khuyến nghị cho từng giai đoạn

| Giai đoạn | Số tenant | Kiến trúc |
|---|---|---|
| MVP / Startup | < 100 | Shared DB + RLS |
| Growth | 100 - 1,000 | Schema-per-tenant |
| Enterprise | > 1,000 hoặc có whale customers | Hybrid: Shared cho small, DB-per-tenant cho large |

## Kết luận

Không có giải pháp nào hoàn hảo cho mọi trường hợp. Chọn kiến trúc dựa trên:

- **Bắt đầu nhỏ**: Shared database với RLS
- **Chuẩn bị migrate**: Thiết kế code từ đầu cho phép chuyển đổi sau này
- **Monitor từ sớm**: Track query performance theo tenant để biết khi nào cần chuyển

TechCorp đã giúp 15+ khách hàng SaaS thiết kế kiến trúc multi-tenant tối ưu. Liên hệ để được tư vấn miễn phí!
Hoàng Gia Huy

Hoàng Gia Huy

Security Engineer · TechCorp

Chuyên gia bảo mật và compliance với 7 năm kinh nghiệm penetration testing và security audit. CISSP certified, từng làm việc cho các ngân hàng lớn. Đam mê về application security và zero-trust architecture.

Bạn có dự án cần tư vấn?

Đội ngũ chuyên gia của chúng tôi sẵn sàng hỗ trợ bạn từ ý tưởng đến triển khai.