概念定义
派生值——小写化的邮箱、抽取的 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))
}