索引与约束
原创2026/3/18大约 6 分钟
索引和约束是数据库性能优化和数据完整性保障的核心,TypeORM 提供了装饰器和迁移两种方式配置索引/约束,既支持开发环境快速配置,也支持生产环境通过迁移精准管控。
一、约束(Constraints)
约束用于保证数据库数据的完整性和一致性,TypeORM 支持主键、非空、唯一、外键、默认值等核心约束,可通过实体装饰器或迁移配置。
1. 主键约束(Primary Key)
主键是表的唯一标识,每张表只能有一个主键,支持单字段主键和复合主键:
(1)单字段主键(常用)
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User {
// 自增主键(MySQL: AUTO_INCREMENT,PostgreSQL: serial)
@PrimaryGeneratedColumn()
id: number;
// 手动指定主键(非自增,需手动赋值)
// @PrimaryColumn({ type: "uuid" })
// id: string;
}(2)复合主键
多个字段组合作为主键,需通过 @PrimaryColumn 标记多个字段:
import { Entity, PrimaryColumn, Column } from "typeorm";
@Entity()
export class UserRole {
@PrimaryColumn()
userId: number;
@PrimaryColumn()
roleId: number;
@Column()
createTime: Date;
}2. 非空约束(Not Null)
限制字段值不能为空,通过 @Column 的 nullable 配置(默认 false,即非空):
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
// 非空约束(显式配置)
@Column({ nullable: false })
username: string;
// 允许为空
@Column({ nullable: true })
avatar: string;
}3. 唯一约束(Unique)
限制字段值在表中唯一,支持单字段唯一和复合唯一:
(1)单字段唯一
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
// 单字段唯一
@Column({ unique: true })
username: string;
}(2)复合唯一
多个字段组合唯一,通过 @Unique 装饰器配置:
import { Entity, PrimaryGeneratedColumn, Column, Unique } from "typeorm";
// 复合唯一:username + email 组合唯一
@Unique(["username", "email"])
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
email: string;
}4. 默认值约束(Default)
指定字段的默认值,插入数据时未赋值则使用默认值:
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ default: "未知用户" })
nickname: string;
@Column({ type: "datetime", default: () => "CURRENT_TIMESTAMP" })
createTime: Date;
}注意:动态默认值(如当前时间)需用函数形式
() => "CURRENT_TIMESTAMP",静态值直接赋值。
5. 外键约束(Foreign Key)
关联两张表的字段,保证引用完整性,通常配合实体关系(一对多/多对一)配置:
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from "typeorm";
import { User } from "./User";
@Entity()
export class Order {
@PrimaryGeneratedColumn()
id: number;
@Column()
orderNo: string;
// 外键约束:关联 User 表的 id 字段
@ManyToOne(() => User, (user) => user.orders, {
onDelete: "CASCADE", // 主表删除,从表级联删除
onUpdate: "CASCADE", // 主表更新,从表级联更新
})
@JoinColumn({ name: "user_id" }) // 外键字段名
user: User;
}6. 检查约束(Check)
限制字段值的范围(如年龄大于0),TypeORM 暂未提供专属装饰器,需通过迁移或原生 SQL 配置:
// 迁移文件中配置检查约束
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddCheckConstraintToUserAge1710000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// MySQL 示例(PostgreSQL 语法类似)
await queryRunner.query(`
ALTER TABLE user ADD CONSTRAINT CHECK_USER_AGE CHECK (age > 0)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE user DROP CONSTRAINT CHECK_USER_AGE
`);
}
}二、索引(Indexes)
索引用于加速数据库查询,TypeORM 支持普通索引、唯一索引、复合索引、全文索引等,可通过装饰器或迁移配置。
1. 普通索引(Index)
加速字段查询,无唯一性限制,通过 @Index 装饰器配置:
(1)单字段普通索引
import { Entity, PrimaryGeneratedColumn, Column, Index } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
// 单字段普通索引,自定义索引名
@Index("IDX_USER_NICKNAME")
@Column()
nickname: string;
}(2)复合普通索引
多个字段组合索引,加速多字段联合查询:
import { Entity, PrimaryGeneratedColumn, Column, Index } from "typeorm";
// 复合索引:nickname + age
@Index(["nickname", "age"])
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
nickname: string;
@Column()
age: number;
}2. 唯一索引(Unique Index)
兼具索引加速和唯一约束的作用,等价于 @Unique + 索引:
// 方式1:通过 @Column 的 unique 配置(自动创建唯一索引)
@Column({ unique: true })
username: string;
// 方式2:通过 @Index 配置(自定义索引名)
@Index("IDX_USER_EMAIL", { unique: true })
@Column()
email: string;3. 全文索引(Fulltext Index)
加速文本字段的模糊查询(仅 MySQL/MariaDB 支持):
import { Entity, PrimaryGeneratedColumn, Column, Index } from "typeorm";
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id: number;
// 全文索引:title + content
@Index("IDX_ARTICLE_FULLTEXT", ["title", "content"], { fulltext: true })
@Column()
title: string;
@Column({ type: "longtext" })
content: string;
}
// 使用全文索引查询
const articles = await articleRepository
.createQueryBuilder("a")
.where("MATCH(a.title, a.content) AGAINST(:keyword)", { keyword: "TypeORM" })
.getMany();4. 空间索引(Spatial Index)
加速地理空间数据查询(如经纬度),仅支持 MySQL/PostgreSQL:
import { Entity, PrimaryGeneratedColumn, Column, Index } from "typeorm";
@Entity()
export class Location {
@PrimaryGeneratedColumn()
id: number;
// 空间索引(MySQL: point 类型)
@Index({ spatial: true })
@Column({ type: "point" })
coordinate: string; // 存储格式:POINT(经度 纬度)
}5. 迁移中配置索引
生产环境推荐通过迁移配置索引,便于管控和回滚:
import { MigrationInterface, QueryRunner, TableIndex } from "typeorm";
export class CreateUserIndex1710000000001 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// 创建普通索引
await queryRunner.createIndex(
"user",
new TableIndex({
name: "IDX_USER_AGE",
columnNames: ["age"],
isUnique: false,
})
);
// 创建复合唯一索引
await queryRunner.createIndex(
"user",
new TableIndex({
name: "IDX_USER_USERNAME_EMAIL",
columnNames: ["username", "email"],
isUnique: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex("user", "IDX_USER_AGE");
await queryRunner.dropIndex("user", "IDX_USER_USERNAME_EMAIL");
}
}三、索引优化技巧
1. 索引创建原则
- 高频查询字段:仅为查询频率高的字段创建索引(如用户昵称、订单号);
- 区分度高的字段:区分度低的字段(如性别)创建索引效果差;
- 避免过度索引:索引会降低插入/更新/删除性能,需平衡查询和写入;
- 复合索引顺序:复合索引遵循“最左前缀原则”,高频查询字段放左侧。
2. 避免索引失效的场景
- 使用函数操作索引字段(如
DATE(createTime) = '2024-01-01'); - 使用
!=``<>``NOT IN等运算符; - 模糊查询以
%开头(如nickname LIKE '%张三'); - 字段类型不匹配(如字符串字段传入数字)。
3. 索引维护
- 定期分析索引使用情况(如 MySQL 的
SHOW INDEX FROM user); - 删除未使用的索引,减少性能开销;
- 大表添加索引时,选择非高峰期执行,避免锁表。
四、不同数据库的索引/约束差异
1. MySQL/MariaDB
- 支持全文索引、空间索引,检查约束仅在 MySQL 8.0.16+ 生效;
- 复合索引最左前缀原则严格,索引名长度有限制(64字符)。
2. PostgreSQL
- 支持更丰富的索引类型(如 GIN、GiST 索引),适合 JSONB/数组字段;
- 检查约束功能完善,支持复杂条件限制。
3. SQLite
- 索引功能有限,不支持全文索引(需扩展);
- 外键约束需手动开启
PRAGMA foreign_keys = ON。
4. MongoDB
- 索引概念类似,但无约束(NoSQL 无强约束);
- 支持地理空间索引、文本索引,语法与关系型数据库不同。
五、核心注意事项
- 约束优先级:实体装饰器配置的约束,在
synchronize: true时自动创建,生产环境需通过迁移管控; - 索引命名规范:建议统一命名格式(如
IDX_表名_字段名),便于维护; - 复合主键/索引:避免过多字段组合,否则会降低索引效率;
- 外键性能:外键会增加写入开销,高并发场景可考虑应用层维护引用完整性;
- 全文索引替代方案:MySQL 全文索引对中文支持差,可使用 Elasticsearch 替代。
至此,本章节的学习就到此结束了,如有疑惑,可对接技术客服进行相关咨询。