← 中文资源

简体中文版本

生成列

生成列用同一行内其他列的表达式计算取值(存储型或虚拟型),让派生字段成为一等公民:可索引、可查询、可约束,无需触发器。

阅读英文原文

概念定义

派生值——小写化的邮箱、抽取的 JSON 字段、月份桶——过去靠应用纪律或触发器维护。生成列把推导收进模式:数据库负责计算与一致性,索引和约束可以像对待真实列一样对待它。

工作机制

DDL 声明表达式与 stored/virtual。存储型占行空间、输入变化时更新;虚拟型每次读时计算,但建索引时会在索引中物化条目。PostgreSQL 支持存储型,MySQL 两者都支持。INSERT 与 UPDATE 不能为生成列赋值。

使用场景

行内稳定的推导:规范化编码、可索引的 JSON 路径提升、时间桶键、校验和。推导需要跨行逻辑时应改用视图或物化视图。

常见误区

生成列由数据库在写入时计算,应用无法覆盖;把同一推导在应用里再写一份必然漂移。修改表达式是对真实数据的模式变更,不是小改。宽的存储生成列会膨胀行与建在其上的每个二级索引;各引擎允许的函数集不同,移植 DDL 要按平台重验不可变性与语法。

代码示例

-- 每个邮箱保留 created_at 最新的一行,标记其余为重复
WITH ranked AS (
  SELECT
    id,
    email,
    created_at,
    ROW_NUMBER() OVER (
      PARTITION BY email
      ORDER BY created_at DESC, id DESC
    ) AS rn
  FROM subscribers
)
SELECT id, email, created_at, (rn = 1) AS is_primary
FROM ranked
ORDER BY email, rn;

-- 一次性清扫:删除全部非首行
WITH ranked AS (
  SELECT id,
         ROW_NUMBER() OVER (
           PARTITION BY email
           ORDER BY created_at DESC, id DESC
         ) AS rn
  FROM subscribers
)
DELETE FROM subscribers s
USING ranked r
WHERE s.id = r.id
  AND r.rn > 1;

-- 上游堵漏:唯一索引阻止重复再次写入
CREATE UNIQUE INDEX CONCURRENTLY idx_subscribers_email
  ON subscribers (email);
package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
)

type Profile struct {
	ID       int64          `json:"id"`
	Nickname sql.NullString `json:"-"`
	Bio      *string        `json:"bio"`
}

type profileJSON struct {
	ID       int64   `json:"id"`
	Nickname *string `json:"nickname"`
	Bio      *string `json:"bio"`
}

func nullString(value sql.NullString) *string {
	if !value.Valid {
		return nil
	}
	return &value.String
}

func (p Profile) MarshalJSON() ([]byte, error) {
	return json.Marshal(profileJSON{
		ID:       p.ID,
		Nickname: nullString(p.Nickname),
		Bio:      p.Bio,
	})
}

func scanProfile(row *sql.Row) (Profile, error) {
	var profile Profile
	err := row.Scan(&profile.ID, &profile.Nickname, &profile.Bio)
	return profile, err
}

func main() {
	encoded, _ := json.Marshal(Profile{ID: 7})
	fmt.Println(string(encoded))
	encoded, _ = json.Marshal(Profile{
		ID:       8,
		Nickname: sql.NullString{String: "ada", Valid: true},
	})
	fmt.Println(string(encoded))
}