인덱스와 제약 조건

제약 조건

SQL 제약 조건은 테이블 컬럼에 적용되는 규칙입니다. 데이터베이스에 잘못된 데이터가 입력되는 것을 방지하기 위해 사용됩니다.

이를 통해 데이터베이스 내 데이터의 정확성과 신뢰성을 보장할 수 있습니다.

기본값(DEFAULT)

DEFAULT 절은 사용자가 INSERT를 할 때 값을 제공하지 않았을 경우 컬럼에 사용할 기본값을 지정합니다.
컬럼 정의에 명시적으로 DEFAULT 절이 없으면, 해당 컬럼의 기본값은 NULL이 됩니다.

명시적인 DEFAULT 절은 기본값을 NULL, 문자열 상수, 블롭 상수, 부호 있는 숫자, 또는 괄호로 둘러싸인 상수 표현식으로 지정할 수 있습니다.

PostgreSQL
MySQL
SQLite
SingleStore
import { sql } from "drizzle-orm";
import { integer, uuid, pgTable } from "drizzle-orm/pg-core";

const table = pgTable('table', {
  integer1: integer('integer1').default(42),
  integer2: integer('integer2').default(sql`'42'::integer`),
  uuid1: uuid('uuid1').defaultRandom(),
  uuid2: uuid('uuid2').default(sql`gen_random_uuid()`),
});
CREATE TABLE IF NOT EXISTS "table" (
  "integer1" integer DEFAULT 42,
  "integer2" integer DEFAULT '42'::integer,
  "uuid1" uuid DEFAULT gen_random_uuid(),
  "uuid2" uuid DEFAULT gen_random_uuid()
);

NOT NULL 제약 조건

기본적으로 컬럼은 NULL 값을 허용합니다. NOT NULL 제약 조건은 컬럼이 NULL 값을 허용하지 않도록 강제합니다.

이 제약 조건은 필드가 항상 값을 포함하도록 강제합니다. 따라서 이 필드에 값을 추가하지 않고는 새로운 레코드를 삽입하거나 기존 레코드를 업데이트할 수 없습니다.

PostgreSQL
MySQL
SQLite
SingleStore
import { integer, pgTable } from "drizzle-orm/pg-core";

const table = pgTable('table', {
  integer: integer('integer').notNull(),
});
CREATE TABLE IF NOT EXISTS "table" (
  "integer" integer NOT NULL,
);

UNIQUE 제약 조건

UNIQUE 제약 조건은 한 컬럼의 모든 값이 서로 다르도록 보장합니다.

UNIQUEPRIMARY KEY 제약 조건 모두 컬럼 또는 컬럼 집합에 대해 고유성을 보장합니다.

PRIMARY KEY 제약 조건은 자동으로 UNIQUE 제약 조건을 포함합니다.

하나의 테이블에 여러 개의 UNIQUE 제약 조건을 설정할 수 있지만, PRIMARY KEY 제약 조건은 테이블당 하나만 설정할 수 있습니다.

PostgreSQL
MySQL
SQLite
SingleStore
import { integer, text, unique, pgTable } from "drizzle-orm/pg-core";

export const user = pgTable('user', {
  id: integer('id').unique(),
});

export const table = pgTable('table', {
  id: integer('id').unique('custom_name'),
});

export const composite = pgTable('composite_example', {
  id: integer('id'),
  name: text('name'),
}, (t) => [{
  unq: unique().on(t.id, t.name),
  unq2: unique('custom_name').on(t.id, t.name)
}]);

// Postgres 15.0+에서는 NULLS NOT DISTINCT를 사용할 수 있습니다.
// 이 예제는 두 가지 사용법을 보여줍니다.
export const userNulls = pgTable('user_nulls_example', {
  id: integer('id').unique("custom_name", { nulls: 'not distinct' }),
}, (t) => [{
  unq: unique().on(t.id).nullsNotDistinct()
}]);
CREATE TABLE IF NOT EXISTS "composite_example" (
  "id" integer,
  "name" text,
  CONSTRAINT "composite_example_id_name_unique" UNIQUE("id","name"),
  CONSTRAINT "custom_name" UNIQUE("id","name")
);

CREATE TABLE IF NOT EXISTS "table" (
	"id" integer,
	CONSTRAINT "custom_name" UNIQUE("id")
);

CREATE TABLE IF NOT EXISTS "user" (
	"id" integer,
	CONSTRAINT "user_id_unique" UNIQUE("id")
);

CREATE TABLE IF NOT EXISTS "user_nulls_example" (
  "id" integer,
  CONSTRAINT "custom_name" UNIQUE NULLS NOT DISTINCT("id"),
  CONSTRAINT "user_nulls_example_id_unique" UNIQUE NULLS NOT DISTINCT("id")
);

CHECK 제약 조건

CHECK 제약 조건은 컬럼에 입력될 수 있는 값의 범위를 제한하는 데 사용됩니다.

컬럼에 CHECK 제약 조건을 정의하면 해당 컬럼에 특정 값만 허용됩니다.
테이블에 CHECK 제약 조건을 정의하면 행의 다른 컬럼 값을 기반으로 특정 컬럼의 값을 제한할 수 있습니다.

PostgreSQL
MySQL
SQLite
SingleStore
import { sql } from "drizzle-orm";
import { check, integer, pgTable, text, uuid } from "drizzle-orm/pg-core";

export const users = pgTable(
  "users",
  {
    id: uuid().defaultRandom().primaryKey(),
    username: text().notNull(),
    age: integer(),
  },
  (table) => [{
    checkConstraint: check("age_check1", sql`${table.age} > 21`),
  }]
);
CREATE TABLE IF NOT EXISTS "users" (
    "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
    "username" text NOT NULL,
    "age" integer,
    CONSTRAINT "age_check1" CHECK ("users"."age" > 21)
);

기본 키(Primary Key)

PRIMARY KEY 제약 조건은 테이블의 각 레코드를 고유하게 식별합니다.
기본 키는 UNIQUE 값을 포함해야 하며, NULL 값을 포함할 수 없습니다.

하나의 테이블에는 하나의 기본 키만 존재할 수 있으며, 이 기본 키는 단일 컬럼 또는 여러 컬럼(필드)으로 구성될 수 있습니다.

PostgreSQL
MySQL
SQLite
SingleStore
import { serial, text, pgTable } from "drizzle-orm/pg-core";

const user = pgTable('user', {
  id: serial('id').primaryKey(),
});

const table = pgTable('table', {
  id: text('cuid').primaryKey(),
});
CREATE TABLE IF NOT EXISTS "user" (
  "id" serial PRIMARY KEY,
);

CREATE TABLE IF NOT EXISTS "table" (
  "cuid" text PRIMARY KEY,
);

복합 기본 키(Composite Primary Key)

PRIMARY KEY와 마찬가지로, 복합 기본 키는 여러 필드를 사용해 테이블의 각 레코드를 고유하게 식별합니다.

Drizzle ORM은 이를 위해 primaryKey 연산자를 제공합니다:

PostgreSQL
MySQL
SQLite
SingleStore
import { serial, text, integer, primaryKey, pgTable } from "drizzle-orm/pg-core";

export const user = pgTable("user", {
  id: serial("id").primaryKey(),
  name: text("name"),
});

export const book = pgTable("book", {
  id: serial("id").primaryKey(),
  name: text("name"),
});

export const booksToAuthors = pgTable("books_to_authors", {
  authorId: integer("author_id"),
  bookId: integer("book_id"),
}, (table) => {
  return [{
    pk: primaryKey({ columns: [table.bookId, table.authorId] }),
    pkWithCustomName: primaryKey({ name: 'custom_name', columns: [table.bookId, table.authorId] }),
  }];
});
...

CREATE TABLE IF NOT EXISTS "books_to_authors" (
  "author_id" integer,
  "book_id" integer,
  PRIMARY KEY("book_id","author_id"),
);

ALTER TABLE "books_to_authors" ADD CONSTRAINT "custom_name" PRIMARY KEY("book_id","author_id");

외래 키(ForeIGN KEY)

FOREIGN KEY 제약 조건은 테이블 간의 연결을 파괴할 수 있는 동작을 방지하기 위해 사용됩니다.
FOREIGN KEY는 한 테이블의 필드(또는 필드 집합)로, 다른 테이블의 PRIMARY KEY를 참조합니다.
외래 키가 있는 테이블을 자식 테이블이라고 하며, 기본 키가 있는 테이블을 참조 테이블 또는 부모 테이블이라고 합니다.

Drizzle ORM은 외래 키를 선언하는 여러 방법을 제공합니다.
컬럼 선언문에서 외래 키를 선언할 수 있습니다:

PostgreSQL
MySQL
SQLite
SingleStore
import { serial, text, integer, pgTable } from "drizzle-orm/pg-core";

export const user = pgTable("user", {
  id: serial("id"),
  name: text("name"),
});

export const book = pgTable("book", {
  id: serial("id"),
  name: text("name"),
  authorId: integer("author_id").references(() => user.id)
});

자기 참조를 하고 싶다면, TypeScript의 제한으로 인해 참조 콜백의 반환 타입을 명시적으로 설정하거나 foreignKey 연산자를 별도로 사용해야 합니다.

PostgreSQL
MySQL
SQLite
SingleStore
import { serial, text, integer, foreignKey, pgTable, AnyPgColumn } from "drizzle-orm/pg-core";

export const user = pgTable("user", {
  id: serial("id"),
  name: text("name"),
  parentId: integer("parent_id").references((): AnyPgColumn => user.id)
});

// 또는
export const user = pgTable("user", {
  id: serial("id"),
  name: text("name"),
  parentId: integer("parent_id"),
}, (table) => {
  return [{
    parentReference: foreignKey({
      columns: [table.parentId],
      foreignColumns: [table.id],
      name: "custom_fk"
    }),
  }];
});

여러 컬럼으로 구성된 외래 키를 선언하려면 전용 foreignKey 연산자를 사용할 수 있습니다:

PostgreSQL
MySQL
SQLite
SingleStore
import { serial, text, foreignKey, pgTable, AnyPgColumn } from "drizzle-orm/pg-core";

export const user = pgTable("user", {
  firstName: text("firstName"),
  lastName: text("lastName"),
}, (table) => {
  return {
    pk: primaryKey({ columns: [table.firstName, table.lastName]}),
  };
});

export const profile = pgTable("profile", {
  id: serial("id").primaryKey(),
  userFirstName: text("user_first_name"),
  userLastName: text("user_last_name"),
}, (table) => {
  return [{
    userReference: foreignKey({
      columns: [table.userFirstName, table.userLastName],
      foreignColumns: [user.firstName, user.lastName]
      name: "custom_fk"
    })
  }]
})

인덱스

Drizzle ORM은 indexunique index 선언을 위한 API를 제공합니다.

PostgreSQL
MySQL
SQLite
SingleStore
import { serial, text, index, uniqueIndex, pgTable } from "drizzle-orm/pg-core";

export const user = pgTable("user", {
  id: serial("id").primaryKey(),
  name: text("name"),
  email: text("email"),
}, (table) => {
  return {
    nameIdx: index("name_idx").on(table.name),
    emailIdx: uniqueIndex("email_idx").on(table.email),
  };
});
CREATE TABLE "user" (
  ...
);

CREATE INDEX "name_idx" ON "user" ("name");
CREATE UNIQUE INDEX "email_idx" ON "user" ("email");
IMPORTANT

drizzle-kit@0.22.0drizzle-orm@0.31.0 이전 버전에서는 drizzle-kit가 인덱스 nameon() 파라미터만 지원합니다.

drizzle-kit@0.22.0drizzle-orm@0.31.0 이후 버전부터는 drizzle-kit가 모든 필드를 지원합니다!

0.31.0 버전부터 Drizzle ORM은 새로운 인덱스 API를 제공하며, 인덱스 생성에 필요한 모든 파라미터를 설정할 수 있습니다:

// 첫 번째 예제, `.on()` 사용
index('name')
  .on(table.column1.asc(), table.column2.nullsFirst(), ...) 또는 .onOnly(table.column1.desc().nullsLast(), table.column2, ...)
  .concurrently()
  .where(sql``)
  .with({ fillfactor: '70' })

// 두 번째 예제, `.using()` 사용
index('name')
  .using('btree', table.column1.asc(), sql`lower(${table.column2})`, table.column1.op('text_ops'))
  .where(sql``) // SQL 표현식
  .with({ fillfactor: '70' })