본문 바로가기
TYLER SONGBlog
← Blog index
CS7 min read

Column: 행의 속성을 정의하는 타입과 제약 조건

열(Column)은 관계형 데이터베이스 테이블에서 하나의 속성과 저장 규칙을 정의한다. 열의 데이터 타입, 널 허용 여부, 기본값, 제약 조건은 데이터 무결성과 쿼리 성능에 직접 영향을 준다. PostgreSQL에서는 열 정의를 스키마 설계의 핵심 단위로 사용한다.

By 송민성

1. 개념

열(Column)은 테이블에 저장되는 개별 속성의 이름과 규칙을 정의한다. 행(Row)이 하나의 데이터 항목이라면, 열은 해당 항목이 어떤 값을 가질 수 있는지 결정한다.

text
users 테이블

id | email              | nickname | created_at
---|--------------------|----------|------------------------
1  | min@example.com    | 민수     | 2026-09-30 09:00:00+09
2  | jin@example.com    | 진       | 2026-09-30 10:00:00+09

각 열은 다음 정보를 가질 수 있다.

  • 열 이름: email
  • 데이터 타입(Data Type): text
  • 널 허용 여부(Nullability): NOT NULL
  • 기본값(Default Value): CURRENT_TIMESTAMP
  • 제약 조건(Constraint): UNIQUE, CHECK, REFERENCES
  • 생성 열(Generated Column) 정의

열은 화면의 입력 필드가 아니다. 애플리케이션에서 숫자처럼 사용하는 값이라도 열 타입이 text라면 데이터베이스는 문자열로 비교하고 정렬한다.

2. 왜 사용하는가

열은 데이터의 구조와 유효 범위를 데이터베이스에 선언한다.

예를 들어 사용자 이메일을 NOT NULL UNIQUE로 정의하면 다음 규칙이 모든 쓰기 경로에 적용된다.

  • 이메일 없는 사용자 저장 금지
  • 같은 이메일 중복 저장 금지
  • 웹 서버 외의 배치 작업이나 관리 도구에도 같은 규칙 적용

애플리케이션 코드만으로 검증하면 다른 서비스나 직접 실행한 SQL이 규칙을 우회할 수 있다. 열의 타입과 제약 조건으로 규칙을 선언하면 데이터베이스가 마지막 방어선 역할을 한다.

적절한 열 설계는 저장 공간과 쿼리 의미도 명확하게 만든다.

text
price를 text로 저장: '100' < '20'
price를 integer로 저장: 100 > 20

문자열 비교는 사전식 순서를 사용하므로 숫자 의미가 필요한 값에는 숫자 타입을 사용해야 한다.

3. 동작 원리

데이터 타입 검사

PostgreSQL은 값을 열에 저장하기 전에 해당 열의 데이터 타입으로 변환할 수 있는지 검사한다.

sql
CREATE TABLE products ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, price integer NOT NULL ); INSERT INTO products (name, price) VALUES ('키보드', 50000);

price에 숫자로 변환할 수 없는 값을 넣으면 문장이 실패한다.

sql
INSERT INTO products (name, price) VALUES ('마우스', '무료');
text
오류: integer 자료형에 대한 잘못된 입력 구문

널과 기본값

널(NULL)은 빈 문자열이나 숫자 0이 아니다. 값이 없거나 알려지지 않았음을 나타낸다.

DEFAULT는 INSERT에서 열을 생략하거나 DEFAULT를 명시했을 때 사용된다. NULL을 직접 넣으면 기본값으로 바뀌지 않는다.

sql
CREATE TABLE posts ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL, view_count integer NOT NULL DEFAULT 0, published_at timestamptz ); INSERT INTO posts (title) VALUES ('첫 글'); INSERT INTO posts (title, view_count) VALUES ('둘째 글', DEFAULT);

두 행의 view_count는 모두 0이다.

제약 조건 검사

제약 조건(Constraint)은 저장 가능한 값을 제한한다.

sql
CREATE TABLE accounts ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, email text NOT NULL UNIQUE, status text NOT NULL DEFAULT 'active', age integer, CONSTRAINT accounts_status_check CHECK (status IN ('active', 'suspended', 'deleted')), CONSTRAINT accounts_age_check CHECK (age IS NULL OR age >= 0) );

각 제약 조건의 역할은 다음과 같다.

  • NOT NULL: 널 저장 금지
  • UNIQUE: 중복 값 저장 금지
  • CHECK: 조건을 만족하지 않는 값 저장 금지
  • PRIMARY KEY: 행 식별자 보장
  • REFERENCES: 참조 무결성(Referential Integrity) 보장

생성 열

생성 열(Generated Column)은 다른 열에서 계산된 값을 데이터베이스가 관리한다.

sql
CREATE TABLE order_items ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, unit_price numeric(12, 2) NOT NULL, quantity integer NOT NULL CHECK (quantity > 0), total_price numeric(14, 2) GENERATED ALWAYS AS (unit_price * quantity) STORED );

total_price를 직접 입력하지 않아도 PostgreSQL이 계산한다. 원본 열이 변경되면 생성 값도 함께 갱신된다.

4. 코드 예제

다음 SQL은 테이블 생성부터 데이터 입력, 조회, 열 변경까지 실행할 수 있다.

sql
DROP TABLE IF EXISTS orders; CREATE TABLE orders ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, customer_email text NOT NULL, status text NOT NULL DEFAULT 'pending', amount numeric(12, 2) NOT NULL, created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT orders_status_check CHECK (status IN ('pending', 'paid', 'cancelled')), CONSTRAINT orders_amount_check CHECK (amount >= 0) ); INSERT INTO orders (customer_email, amount) VALUES ('min@example.com', 25000.00), ('jin@example.com', 49000.00); INSERT INTO orders (customer_email, status, amount) VALUES ('seo@example.com', 'paid', 18000.00); SELECT id, customer_email, status, amount, created_at FROM orders ORDER BY id; ALTER TABLE orders ADD COLUMN paid_at timestamptz; UPDATE orders SET paid_at = CURRENT_TIMESTAMP WHERE status = 'paid'; CREATE INDEX orders_status_created_at_idx ON orders (status, created_at); SELECT id, customer_email, amount FROM orders WHERE status = 'paid' ORDER BY created_at DESC;

금액에는 부동소수점 타입보다 정확한 십진 연산을 제공하는 numeric이 적합하다. numeric(12, 2)는 전체 유효 숫자를 최대 12자리, 소수점 이하를 2자리로 제한한다.

5. 시간 복잡도 또는 성능 특성

열 접근 자체를 하나의 고정된 시간복잡도로 표현하기는 어렵다. 실제 비용은 행 수, 행 크기, 저장 방식, 인덱스, 조건식에 따라 달라진다.

전체 테이블 조회

인덱스 없이 특정 열의 조건으로 행을 찾으면 일반적으로 순차 스캔(Sequential Scan)이 발생한다.

sql
SELECT * FROM orders WHERE status = 'paid';

행 수를 n이라고 하면 검사 비용은 O(n)이다.

인덱스 조회

B-트리(B-tree) 인덱스로 값을 찾는 탐색 비용은 일반적으로 O(log n)이다. 결과 행이 k개라면 전체 조회 비용은 대략 O(log n + k)로 볼 수 있다.

sql
CREATE INDEX orders_status_idx ON orders (status);

단, status처럼 값 종류가 적은 열은 조건에 맞는 행이 많을 수 있다. 이 경우 PostgreSQL이 인덱스 스캔보다 순차 스캔을 선택할 수 있다.

행 크기

필요 없는 열까지 조회하면 디스크 입출력, 메모리 사용량, 네트워크 전송량이 증가한다.

sql
SELECT * FROM orders;

필요한 열만 선택하는 편이 유리하다.

sql
SELECT id, status, amount FROM orders;

PostgreSQL은 다중 버전 동시성 제어(MVCC)를 사용한다. 열 하나만 수정해도 기존 행 버전과 구분되는 새 행 버전이 만들어진다. 큰 값이 포함된 행이나 갱신이 잦은 테이블은 불필요한 열 변경을 줄여야 한다.

6. 실무 사용 사례

상태값 제한

주문 상태처럼 허용 값이 정해진 열에는 CHECK 제약 조건을 둘 수 있다.

sql
status text NOT NULL CHECK (status IN ('pending', 'paid', 'cancelled'))

상태 종류와 변경 규칙이 복잡해지면 별도 참조 테이블이나 PostgreSQL 열거형(Enum) 사용을 검토한다.

생성·수정 시각 기록

생성 시각은 기본값으로 처리할 수 있다.

sql
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP

updated_at은 행이 수정될 때 자동으로 바뀌지 않는다. 애플리케이션의 UPDATE 문이나 트리거(Trigger)로 별도 처리해야 한다.

소프트 삭제

삭제 시각 열로 논리적 삭제를 표현한다.

sql
deleted_at timestamptz

삭제되지 않은 행은 deleted_at IS NULL로 조회한다. 자주 사용하는 조건이라면 부분 인덱스(Partial Index)를 적용할 수 있다.

sql
CREATE INDEX users_active_email_idx ON users (email) WHERE deleted_at IS NULL;

외래 키

다른 테이블의 행을 참조하는 열에는 외래 키(Foreign Key)를 둔다.

sql
CREATE TABLE order_payments ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, order_id bigint NOT NULL REFERENCES orders(id), paid_amount numeric(12, 2) NOT NULL CHECK (paid_amount >= 0) );

존재하지 않는 주문 번호를 저장할 수 없으므로 참조 무결성이 유지된다.

7. 주의할 점

이름만 보고 의미를 추측하게 만들지 않기

단위와 의미가 불분명한 이름은 오류를 만든다.

text
나쁜 예: timeout, price, date
좋은 예: timeout_seconds, price_krw, published_at

시간에는 시간대 포함 여부를 결정하기

여러 시간대를 다루는 시스템의 사건 시각에는 timestamptz가 적합하다. 생일처럼 시간대와 무관한 날짜에는 date를 사용한다.

sql
created_at timestamptz NOT NULL, birth_date date

널의 의미를 하나로 유지하기

널이 미입력, 해당 없음, 비공개를 동시에 뜻하면 쿼리와 업무 규칙이 모호해진다. 서로 다른 상태라면 별도 상태 열이나 명시적인 값을 사용한다.

널 비교에는 =를 사용할 수 없다.

sql
SELECT * FROM orders WHERE paid_at IS NULL;

타입 변경 비용 확인하기

ALTER TABLE로 열 타입을 바꾸면 값 변환, 테이블 재작성, 잠금이 발생할 수 있다. 큰 테이블에서는 실행 계획과 잠금 영향을 확인한 뒤 변경해야 한다.

sql
ALTER TABLE orders ALTER COLUMN amount TYPE numeric(14, 2);

인덱스를 무조건 만들지 않기

인덱스는 조회를 빠르게 만들 수 있지만 저장 공간을 사용하며 INSERT, UPDATE, DELETE 비용을 늘린다. 실제 조회 조건과 실행 계획을 기준으로 추가한다.

sql
EXPLAIN ANALYZE SELECT id, amount FROM orders WHERE status = 'paid';

8. 핵심 정리

  • 열(Column)은 속성의 이름, 데이터 타입, 기본값, 널 허용 여부, 제약 조건을 정의한다.
  • 올바른 열 타입은 비교 의미, 저장 범위, 정밀도, 쿼리 동작을 결정한다.
  • NOT NULL, UNIQUE, CHECK, REFERENCES로 데이터 무결성을 데이터베이스에서 보장한다.
  • 인덱스 없는 조건 검색은 일반적으로 O(n), B-트리 탐색은 일반적으로 O(log n)이다.
  • 열 추가와 타입 변경은 큰 테이블에서 잠금, 테이블 재작성, 저장 공간 증가를 일으킬 수 있으므로 실행 전 영향을 확인한다.