Appearance
06-迁移与索引管理
数据库结构随着业务发展不断演进,如何安全、可控地管理这些变更就是数据库迁移要解决的问题。GORM 提供了 AutoMigrate 和 Migrator API 两套机制,但对于生产环境,专业的迁移工具(如 golang-migrate)更为可靠。本篇将系统讲解 GORM 的迁移能力、索引管理以及生产级迁移实践。
AutoMigrate 原理与局限
AutoMigrate 会根据模型结构自动创建表、添加缺失的列和索引。它的设计目标是开发期快速迭代,而非生产环境的版本化管理。
AutoMigrate 的工作原理:
- 检查目标表是否存在,不存在则创建
- 对比模型字段与数据库列,添加缺失的列
- 对比模型索引标签与数据库索引,添加缺失的索引
- 部分数据库支持修改列类型
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null"`
Email string `gorm:"type:varchar(150);uniqueIndex"`
Age int
}
func main() {
db, err := gorm.Open(sqlite.Open("auto.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 第一次迁移:创建表
if err := db.AutoMigrate(&User{}); err != nil {
log.Fatal(err)
}
fmt.Println("首次迁移完成")
// 检查表和列
m := db.Migrator()
fmt.Println("表存在:", m.HasTable(&User{}))
fmt.Println("name 列存在:", m.HasColumn(&User{}, "Name"))
fmt.Println("email 索引存在:", m.HasIndex(&User{}, "Email"))
}AutoMigrate 的局限性:
| 能力 | 是否支持 | 说明 |
|---|---|---|
| 创建表 | 支持 | 表不存在时创建 |
| 添加列 | 支持 | 模型新增字段时自动添加 |
| 添加索引 | 支持 | 根据标签创建索引 |
| 修改列类型 | 部分 | MySQL/PostgreSQL 支持,SQLite 有限 |
| 删除列 | 不支持 | 避免数据丢失 |
| 删除索引 | 不支持 | 需手动处理 |
| 重命名列 | 不支持 | 需手动 SQL |
| 修改约束 | 不支持 | 如 NOT NULL 变更 |
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// 演示 AutoMigrate 不能删除列
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50)"`
Email string `gorm:"type:varchar(150)"`
}
// 后续删除了 Age 字段
// AutoMigrate 不会删除 age 列
func main() {
db, err := gorm.Open(sqlite.Open("limit.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 先创建包含 Age 的表
db.Migrator().CreateTable(&struct {
ID uint `gorm:"primaryKey"`
Name string
Email string
Age int // 这个字段后续从模型中移除
}{})
// 再用不含 Age 的模型 AutoMigrate
db.AutoMigrate(&User{})
// 检查 age 列是否还存在
type ColumnInfo struct {
Name string
}
var columns []ColumnInfo
db.Raw("PRAGMA table_info(users)").Scan(&columns)
fmt.Println("users 表的列:")
for _, c := range columns {
fmt.Printf(" - %s\n", c.Name)
// age 列依然存在,AutoMigrate 不会删除它
}
}手动迁移:Migrator API
对于 AutoMigrate 无法处理的场景,GORM 提供了完整的 Migrator API。
表操作:CreateTable、DropTable、HasTable
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50)"`
Email string `gorm:"type:varchar(150);uniqueIndex"`
}
func main() {
db, err := gorm.Open(sqlite.Open("migrator.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
m := db.Migrator()
// 检查表是否存在
if !m.HasTable(&User{}) {
// 创建表
if err := m.CreateTable(&User{}); err != nil {
log.Fatal(err)
}
fmt.Println("表创建成功")
}
// 重命名表
if m.HasTable("users") {
m.RenameTable(&User{}, "users_backup")
fmt.Println("表已重命名为 users_backup")
m.RenameTable("users_backup", &User{})
}
// 删除表(谨慎使用)
// m.DropTable(&User{})
// m.DropTable("users")
fmt.Println("迁移操作完成")
}列操作:AddColumn、DropColumn、AlterColumn
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50)"`
Email string `gorm:"type:varchar(150)"`
Age int
Status int `gorm:"default:1"`
}
func main() {
db, err := gorm.Open(sqlite.Open("column.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&User{})
m := db.Migrator()
// 添加列:先创建一个临时模型只包含新字段
type AddPhone struct {
Phone string `gorm:"type:varchar(20)"`
}
if !m.HasColumn(&User{}, "Phone") {
if err := m.AddColumn(&User{}, "Phone"); err != nil {
// AddColumn 需要模型字段,使用 Migrator 的 AddColumn 接口
fmt.Println("AddColumn 失败:", err)
}
}
// 直接通过模型字段添加列(推荐方式)
type UserWithBio struct {
ID uint `gorm:"primaryKey"`
Name string
Bio string `gorm:"type:text"`
}
if !m.HasColumn(&UserWithBio{}, "Bio") {
m.AddColumn(&UserWithBio{}, "Bio")
}
// 修改列类型
type UserNewName struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(100)"` // 从 50 改为 100
}
if err := m.AlterColumn(&UserNewName{}, "Name"); err != nil {
fmt.Println("AlterColumn:", err)
}
// 删除列(注意:SQLite 对 DropColumn 支持有限,可能需要重建表)
if m.HasColumn(&User{}, "Age") {
if err := m.DropColumn(&User{}, "Age"); err != nil {
fmt.Println("DropColumn:", err)
}
}
// 检查列
fmt.Println("Name 列存在:", m.HasColumn(&User{}, "Name"))
fmt.Println("Age 列存在:", m.HasColumn(&User{}, "Age"))
// 重命名列
if m.HasColumn(&User{}, "Name") {
m.RenameColumn(&User{}, "Name", "FullName")
fmt.Println("Name 重命名为 FullName")
}
}索引操作:CreateIndex、DropIndex、HasIndex
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);index"`
Email string `gorm:"type:varchar(150);uniqueIndex"`
City string `gorm:"type:varchar(50)"`
Age int
}
func main() {
db, err := gorm.Open(sqlite.Open("idx.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&User{})
m := db.Migrator()
// 检查索引
fmt.Println("Name 索引存在:", m.HasIndex(&User{}, "Name"))
fmt.Println("Email 索引存在:", m.HasIndex(&User{}, "Email"))
// 创建索引(按字段名)
type UserWithCityIndex struct {
City string `gorm:"index:idx_city"`
}
if !m.HasIndex(&User{}, "City") {
m.CreateIndex(&UserWithCityIndex{}, "City")
fmt.Println("City 索引创建")
}
// 创建唯一索引
type UserWithAgeIndex struct {
Age int `gorm:"uniqueIndex:idx_age_unique"`
}
if !m.HasIndex(&User{}, "Age") {
m.CreateIndex(&UserWithAgeIndex{}, "Age")
}
// 删除索引
if m.HasIndex(&User{}, "City") {
m.DropIndex(&User{}, "City")
fmt.Println("City 索引删除")
}
// 重命名索引
if m.HasIndex(&User{}, "Name") {
m.RenameIndex(&User{}, "Name", "idx_user_name")
fmt.Println("Name 索引重命名")
}
}索引管理
普通索引、唯一索引、复合索引
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);index"` // 普通索引
Email string `gorm:"type:varchar(150);uniqueIndex"` // 唯一索引
Phone string `gorm:"type:varchar(20);index:idx_phone"` // 命名索引
// 复合索引:city + age
City string `gorm:"type:varchar(50);index:idx_city_age,priority:1"`
Age int `gorm:"index:idx_city_age,priority:2"`
// 复合唯一索引:first_name + last_name
FirstName string `gorm:"type:varchar(50);uniqueIndex:idx_full_name,priority:1"`
LastName string `gorm:"type:varchar(50);uniqueIndex:idx_full_name,priority:2"`
// 带条件的索引(部分数据库支持)
Status int `gorm:"index:idx_active,priority:1;default:1"`
}
func main() {
db, err := gorm.Open(sqlite.Open("indexes.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&User{})
m := db.Migrator()
// 验证所有索引
fmt.Println("Name 索引:", m.HasIndex(&User{}, "Name"))
fmt.Println("Email 唯一索引:", m.HasIndex(&User{}, "Email"))
fmt.Println("Phone 索引:", m.HasIndex(&User{}, "Phone"))
fmt.Println("City+Age 复合索引:", m.HasIndex(&User{}, "idx_city_age"))
fmt.Println("FirstName+LastName 复合唯一索引:", m.HasIndex(&User{}, "idx_full_name"))
// 插入数据测试唯一索引
user1 := User{Name: "Tom", Email: "tom@x.com", Phone: "13800000001",
City: "Beijing", Age: 25, FirstName: "Tom", LastName: "Smith"}
if err := db.Create(&user1).Error; err != nil {
log.Fatal(err)
}
// 重复 Email 会失败
user2 := User{Name: "Tom2", Email: "tom@x.com", Phone: "13800000002",
City: "Shanghai", Age: 30, FirstName: "Tom2", LastName: "Brown"}
err = db.Create(&user2).Error
fmt.Printf("重复 Email 创建结果: %v\n", err)
// 重复复合唯一索引会失败
user3 := User{Name: "Tom3", Email: "tom3@x.com", Phone: "13800000003",
City: "Beijing", Age: 30, FirstName: "Tom", LastName: "Smith"}
err = db.Create(&user3).Error
fmt.Printf("重复姓名创建结果: %v\n", err)
}全文索引
MySQL 支持全文索引(FULLTEXT),适合文本搜索:
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Article struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"type:varchar(200)"`
Content string `gorm:"type:text;index:idx_content,full_text"` // MySQL 全文索引
}
func main() {
db, err := gorm.Open(sqlite.Open("fulltext.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&Article{})
// 准备数据
articles := []Article{
{Title: "Go 入门", Content: "Go 是一门编译型语言,由 Google 开发"},
{Title: "GORM 教程", Content: "GORM 是 Go 的 ORM 库"},
{Title: "Python 教程", Content: "Python 是一门解释型语言"},
}
db.Create(&articles)
// MySQL 中使用全文搜索:
// db.Where("MATCH(content) AGAINST (?)", "Go").Find(&articles)
// SQLite 中使用 LIKE 模拟
var results []Article
db.Where("content LIKE ?", "%Go%").Find(&results)
fmt.Printf("包含 'Go' 的文章: %d 篇\n", len(results))
for _, a := range results {
fmt.Printf(" - %s\n", a.Title)
}
}使用 tag 定义索引
通过 tag 可以精细控制索引的各种属性:
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Product struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(200);index:idx_name,class:FULLTEXT"` // 全文索引(MySQL)
SKU string `gorm:"type:varchar(50);uniqueIndex:idx_sku"` // 命名唯一索引
Price float64 `gorm:"index:idx_price,sort:desc"` // 降序索引
// 复合索引的多种属性
CategoryID uint `gorm:"index:idx_cat_status,priority:1;not null"`
Status int `gorm:"index:idx_cat_status,priority:2;default:1"`
// 函数索引(PostgreSQL 支持)
// Email string `gorm:"index:idx_lower_email,expression:lower(email)"`
}
func main() {
db, err := gorm.Open(sqlite.Open("tag_idx.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&Product{})
m := db.Migrator()
fmt.Println("idx_name 索引:", m.HasIndex(&Product{}, "idx_name"))
fmt.Println("idx_sku 索引:", m.HasIndex(&Product{}, "idx_sku"))
fmt.Println("idx_price 索引:", m.HasIndex(&Product{}, "idx_price"))
fmt.Println("idx_cat_status 索引:", m.HasIndex(&Product{}, "idx_cat_status"))
}迁移版本管理:使用 golang-migrate
生产环境推荐使用 golang-migrate 进行版本化迁移管理。
安装 golang-migrate
bash
# 安装 CLI 工具
go install -tags 'sqlite mysql postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# 安装 Go 库
go get -u github.com/golang-migrate/migrate/v4
go get -u github.com/golang-migrate/migrate/v4/database/sqlite
go get -u github.com/golang-migrate/migrate/v4/source/file创建迁移文件
bash
# 创建迁移文件(会生成两个文件:up 和 down)
migrate create -ext sql -dir migrations -seq create_users_table
# 生成的文件:
# migrations/000001_create_users_table.up.sql # 升级 SQL
# migrations/000001_create_users_table.down.sql # 回滚 SQL000001_create_users_table.up.sql 内容:
sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
age INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_name ON users(name);000001_create_users_table.down.sql 内容:
sql
DROP INDEX IF EXISTS idx_users_name;
DROP TABLE IF EXISTS users;在 Go 中执行迁移
go
package main
import (
"fmt"
"log"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/sqlite"
_ "github.com/golang-migrate/migrate/v4/source/file"
"database/sql"
_ "modernc.org/sqlite"
)
func main() {
// 使用 golang-migrate 执行迁移
m, err := migrate.New(
"file://migrations",
"sqlite://migrate.db",
)
if err != nil {
log.Fatal("初始化迁移失败:", err)
}
defer m.Close()
// 执行迁移
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatal("迁移失败:", err)
}
// 查看当前版本
version, dirty, err := m.Version()
if err != nil {
log.Fatal("获取版本失败:", err)
}
fmt.Printf("当前迁移版本: %d, dirty: %v\n", version, dirty)
// 回滚上一次迁移
// if err := m.Steps(-1); err != nil {
// log.Fatal("回滚失败:", err)
// }
// 使用 sql.DB 验证
db, err := sql.Open("sqlite", "migrate.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
var name string
err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='users'").Scan(&name)
if err != nil {
fmt.Println("users 表不存在")
} else {
fmt.Printf("users 表已创建: %s\n", name)
}
}数据库迁移最佳实践
- 每个迁移文件保持原子性:一个迁移只做一件事,便于回滚和审查
- 始终提供 down 迁移:确保可以回滚
- 大表变更分批进行:避免长时间锁表
- 先备份再迁移:生产环境迁移前必须备份
- 测试环境验证:迁移先在测试环境跑通
- 避免破坏性变更:删除列先重命名,观察一段时间再删除
- 不要修改已发布的迁移文件:已应用到生产的迁移文件不能修改
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// 演示安全的列删除流程
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50)"`
Email string `gorm:"type:varchar(150)"`
OldField string `gorm:"type:varchar(100)"` // 待删除的字段
}
func main() {
db, err := gorm.Open(sqlite.Open("safe.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&User{})
m := db.Migrator()
// 步骤1:先停止使用 OldField 字段(代码层面)
// 步骤2:重命名为 deprecated_old_field(标记为废弃)
if m.HasColumn(&User{}, "OldField") {
m.RenameColumn(&User{}, "OldField", "deprecated_old_field")
fmt.Println("已重命名为 deprecated_old_field")
}
// 步骤3:观察一段时间,确认无影响
// 步骤4:删除列
if m.HasColumn(&User{}, "deprecated_old_field") {
// m.DropColumn(&User{}, "deprecated_old_field")
fmt.Println("(演示)可安全删除 deprecated_old_field")
}
// 大表加列:使用默认值避免锁表
type UserV2 struct {
ID uint `gorm:"primaryKey"`
Name string
Status int `gorm:"default:1"` // 有默认值,避免 NOT NULL 锁表
}
if !m.HasColumn(&UserV2{}, "Status") {
m.AddColumn(&UserV2{}, "Status")
fmt.Println("添加 Status 列(带默认值)")
}
}完整示例:博客系统迁移脚本
下面用一个完整的博客系统迁移脚本综合演示迁移管理:
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// User 用户模型
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"type:varchar(50);uniqueIndex;not null"`
Email string `gorm:"type:varchar(150);uniqueIndex;not null"`
Password string `gorm:"type:varchar(100);not null"`
Nickname string `gorm:"type:varchar(50)"`
Status int `gorm:"default:1;index"`
CreatedAt string
UpdatedAt string
}
// Post 文章模型
type Post struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"type:varchar(200);not null;index"`
Content string `gorm:"type:text"`
ViewCount int `gorm:"default:0;index"`
UserID uint `gorm:"index;not null"`
CategoryID uint `gorm:"index"`
Status int `gorm:"default:1;index;comment:1发布 0草稿"`
CreatedAt string
UpdatedAt string
}
// Comment 评论模型
type Comment struct {
ID uint `gorm:"primaryKey"`
Content string `gorm:"type:text;not null"`
PostID uint `gorm:"index;not null"`
UserID uint `gorm:"index;not null"`
ParentID *uint `gorm:"index"` // 父评论 ID,可为空(用于回复)
CreatedAt string
}
// Category 分类模型
type Category struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(100);uniqueIndex;not null"`
ParentID *uint `gorm:"index"` // 父分类
Sort int `gorm:"default:0"`
}
// MigrationManager 迁移管理器
type MigrationManager struct {
db *gorm.DB
}
func NewMigrationManager(db *gorm.DB) *MigrationManager {
return &MigrationManager{db: db}
}
// RunMigrations 执行所有迁移
func (mm *MigrationManager) RunMigrations() error {
m := mm.db.Migrator()
// 迁移1:创建分类表
if !m.HasTable(&Category{}) {
fmt.Println("迁移1: 创建 categories 表")
if err := m.CreateTable(&Category{}); err != nil {
return fmt.Errorf("创建 categories 表失败: %w", err)
}
}
// 迁移2:创建用户表
if !m.HasTable(&User{}) {
fmt.Println("迁移2: 创建 users 表")
if err := m.CreateTable(&User{}); err != nil {
return fmt.Errorf("创建 users 表失败: %w", err)
}
}
// 迁移3:创建文章表
if !m.HasTable(&Post{}) {
fmt.Println("迁移3: 创建 posts 表")
if err := m.CreateTable(&Post{}); err != nil {
return fmt.Errorf("创建 posts 表失败: %w", err)
}
}
// 迁移4:创建评论表
if !m.HasTable(&Comment{}) {
fmt.Println("迁移4: 创建 comments 表")
if err := m.CreateTable(&Comment{}); err != nil {
return fmt.Errorf("创建 comments 表失败: %w", err)
}
}
// 迁移5:添加索引(如果不存在)
if !m.HasIndex(&Post{}, "Title") {
fmt.Println("迁移5: 添加 posts.title 索引")
m.CreateIndex(&Post{}, "Title")
}
// 迁移6:添加复合索引
type PostComposite struct {
UserID uint `gorm:"index:idx_user_status,priority:1"`
Status int `gorm:"index:idx_user_status,priority:2"`
}
if !m.HasIndex(&PostComposite{}, "idx_user_status") {
fmt.Println("迁移6: 添加 posts 复合索引 idx_user_status")
m.CreateIndex(&PostComposite{}, "UserID")
}
return nil
}
// RollbackMigration 回滚最后一个迁移
func (mm *MigrationManager) RollbackMigration() error {
m := mm.db.Migrator()
if m.HasTable(&Comment{}) {
fmt.Println("回滚: 删除 comments 表")
return m.DropTable(&Comment{})
}
return nil
}
// Status 查看迁移状态
func (mm *MigrationManager) Status() {
m := mm.db.Migrator()
tables := []string{"categories", "users", "posts", "comments"}
fmt.Println("\n=== 迁移状态 ===")
for _, t := range tables {
fmt.Printf("表 %s: %v\n", t, m.HasTable(t))
}
}
func main() {
db, err := gorm.Open(sqlite.Open("blog_migrate.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
mm := NewMigrationManager(db)
// 执行迁移
if err := mm.RunMigrations(); err != nil {
log.Fatal("迁移失败:", err)
}
// 查看状态
mm.Status()
// 验证:插入测试数据
category := Category{Name: "技术"}
db.Create(&category)
user := User{Username: "admin", Email: "admin@blog.com", Password: "hashed"}
db.Create(&user)
post := Post{
Title: "GORM 迁移指南",
Content: "本篇介绍 GORM 迁移...",
UserID: user.ID,
CategoryID: category.ID,
Status: 1,
}
db.Create(&post)
comment := Comment{
Content: "写得很好!",
PostID: post.ID,
UserID: user.ID,
}
db.Create(&comment)
fmt.Println("\n=== 数据验证 ===")
var postCount, commentCount int64
db.Model(&Post{}).Count(&postCount)
db.Model(&Comment{}).Count(&commentCount)
fmt.Printf("文章数: %d, 评论数: %d\n", postCount, commentCount)
}小结
本篇系统讲解了 GORM 的迁移与索引管理:
- AutoMigrate 原理:自动建表加列加索引,但不会删列、不会重命名
- AutoMigrate 局限:不适合生产环境的版本化管理
- Migrator API:提供表、列、索引的完整 CRUD 操作
- 表操作:CreateTable、DropTable、HasTable、RenameTable
- 列操作:AddColumn、DropColumn、AlterColumn、RenameColumn
- 索引操作:CreateIndex、DropIndex、HasIndex、RenameIndex
- 索引类型:普通索引、唯一索引、复合索引、全文索引
- tag 定义索引:priority、composite、class 等属性
- golang-migrate:生产级迁移工具,支持版本化和回滚
- 最佳实践:原子迁移、提供 down、大表分批、先备份、安全删除列
下一篇我们将学习性能优化与缓存,提升 GORM 应用的执行效率。