Column: 행의 속성을 정의하는 타입과 제약 조건
열(Column)은 관계형 데이터베이스 테이블에서 하나의 속성과 저장 규칙을 정의한다. 열의 데이터 타입, 널 허용 여부, 기본값, 제약 조건은 데이터 무결성과 쿼리 성능에 직접 영향을 준다. PostgreSQL에서는 열 정의를 스키마 설계의 핵심 단위로 사용한다.
By 송민성
1. 개념
열(Column)은 테이블에 저장되는 개별 속성의 이름과 규칙을 정의한다. 행(Row)이 하나의 데이터 항목이라면, 열은 해당 항목이 어떤 값을 가질 수 있는지 결정한다.
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이 규칙을 우회할 수 있다. 열의 타입과 제약 조건으로 규칙을 선언하면 데이터베이스가 마지막 방어선 역할을 한다.
적절한 열 설계는 저장 공간과 쿼리 의미도 명확하게 만든다.
price를 text로 저장: '100' < '20'
price를 integer로 저장: 100 > 20문자열 비교는 사전식 순서를 사용하므로 숫자 의미가 필요한 값에는 숫자 타입을 사용해야 한다.
3. 동작 원리
데이터 타입 검사
PostgreSQL은 값을 열에 저장하기 전에 해당 열의 데이터 타입으로 변환할 수 있는지 검사한다.
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에 숫자로 변환할 수 없는 값을 넣으면 문장이 실패한다.
INSERT INTO products (name, price)
VALUES ('마우스', '무료');오류: integer 자료형에 대한 잘못된 입력 구문널과 기본값
널(NULL)은 빈 문자열이나 숫자 0이 아니다. 값이 없거나 알려지지 않았음을 나타낸다.
DEFAULT는 INSERT에서 열을 생략하거나 DEFAULT를 명시했을 때 사용된다. NULL을 직접 넣으면 기본값으로 바뀌지 않는다.
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)은 저장 가능한 값을 제한한다.
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)은 다른 열에서 계산된 값을 데이터베이스가 관리한다.
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은 테이블 생성부터 데이터 입력, 조회, 열 변경까지 실행할 수 있다.
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)이 발생한다.
SELECT *
FROM orders
WHERE status = 'paid';행 수를 n이라고 하면 검사 비용은 O(n)이다.
인덱스 조회
B-트리(B-tree) 인덱스로 값을 찾는 탐색 비용은 일반적으로 O(log n)이다. 결과 행이 k개라면 전체 조회 비용은 대략 O(log n + k)로 볼 수 있다.
CREATE INDEX orders_status_idx
ON orders (status);단, status처럼 값 종류가 적은 열은 조건에 맞는 행이 많을 수 있다. 이 경우 PostgreSQL이 인덱스 스캔보다 순차 스캔을 선택할 수 있다.
행 크기
필요 없는 열까지 조회하면 디스크 입출력, 메모리 사용량, 네트워크 전송량이 증가한다.
SELECT *
FROM orders;필요한 열만 선택하는 편이 유리하다.
SELECT id, status, amount
FROM orders;PostgreSQL은 다중 버전 동시성 제어(MVCC)를 사용한다. 열 하나만 수정해도 기존 행 버전과 구분되는 새 행 버전이 만들어진다. 큰 값이 포함된 행이나 갱신이 잦은 테이블은 불필요한 열 변경을 줄여야 한다.
6. 실무 사용 사례
상태값 제한
주문 상태처럼 허용 값이 정해진 열에는 CHECK 제약 조건을 둘 수 있다.
status text NOT NULL
CHECK (status IN ('pending', 'paid', 'cancelled'))상태 종류와 변경 규칙이 복잡해지면 별도 참조 테이블이나 PostgreSQL 열거형(Enum) 사용을 검토한다.
생성·수정 시각 기록
생성 시각은 기본값으로 처리할 수 있다.
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMPupdated_at은 행이 수정될 때 자동으로 바뀌지 않는다. 애플리케이션의 UPDATE 문이나 트리거(Trigger)로 별도 처리해야 한다.
소프트 삭제
삭제 시각 열로 논리적 삭제를 표현한다.
deleted_at timestamptz삭제되지 않은 행은 deleted_at IS NULL로 조회한다. 자주 사용하는 조건이라면 부분 인덱스(Partial Index)를 적용할 수 있다.
CREATE INDEX users_active_email_idx
ON users (email)
WHERE deleted_at IS NULL;외래 키
다른 테이블의 행을 참조하는 열에는 외래 키(Foreign Key)를 둔다.
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. 주의할 점
이름만 보고 의미를 추측하게 만들지 않기
단위와 의미가 불분명한 이름은 오류를 만든다.
나쁜 예: timeout, price, date
좋은 예: timeout_seconds, price_krw, published_at시간에는 시간대 포함 여부를 결정하기
여러 시간대를 다루는 시스템의 사건 시각에는 timestamptz가 적합하다. 생일처럼 시간대와 무관한 날짜에는 date를 사용한다.
created_at timestamptz NOT NULL,
birth_date date널의 의미를 하나로 유지하기
널이 미입력, 해당 없음, 비공개를 동시에 뜻하면 쿼리와 업무 규칙이 모호해진다. 서로 다른 상태라면 별도 상태 열이나 명시적인 값을 사용한다.
널 비교에는 =를 사용할 수 없다.
SELECT *
FROM orders
WHERE paid_at IS NULL;타입 변경 비용 확인하기
ALTER TABLE로 열 타입을 바꾸면 값 변환, 테이블 재작성, 잠금이 발생할 수 있다. 큰 테이블에서는 실행 계획과 잠금 영향을 확인한 뒤 변경해야 한다.
ALTER TABLE orders
ALTER COLUMN amount TYPE numeric(14, 2);인덱스를 무조건 만들지 않기
인덱스는 조회를 빠르게 만들 수 있지만 저장 공간을 사용하며 INSERT, UPDATE, DELETE 비용을 늘린다. 실제 조회 조건과 실행 계획을 기준으로 추가한다.
EXPLAIN ANALYZE
SELECT id, amount
FROM orders
WHERE status = 'paid';8. 핵심 정리
- 열(Column)은 속성의 이름, 데이터 타입, 기본값, 널 허용 여부, 제약 조건을 정의한다.
- 올바른 열 타입은 비교 의미, 저장 범위, 정밀도, 쿼리 동작을 결정한다.
NOT NULL,UNIQUE,CHECK,REFERENCES로 데이터 무결성을 데이터베이스에서 보장한다.- 인덱스 없는 조건 검색은 일반적으로
O(n), B-트리 탐색은 일반적으로O(log n)이다. - 열 추가와 타입 변경은 큰 테이블에서 잠금, 테이블 재작성, 저장 공간 증가를 일으킬 수 있으므로 실행 전 영향을 확인한다.