개요
이번 게시글에서는 PostgreSQL 데이터베이스 시스템 내에서의 테이블 정의, 제약 조건 및 수정에 대한 포괄적인 기술적 검토를 제공합니다. 테이블의 기본 구조, 기본값 및 생성된 값과 같은 특수 열 속성, 데이터 무결성 유지에 있어 제약 조건의 중요한 역할(표준 및 PostgreSQL 특정 유형 포함), 시스템 열의 특성, 그리고 ALTER TABLE 명령을 사용하여 기존 테이블 구조를 변경하는 다양한 작업에 대해 다룹니다.
섹션 1: 데이터베이스 테이블 기본 사항
모든 관계형 데이터베이스의 핵심에는 데이터를 구성하고 저장하는 기본 구조인 테이블이 있습니다. 고급 기능을 살펴보기 전에 테이블의 기본 구성과 목적을 이해하는 것이 필수적입니다.
1.1 테이블 구조: 행과 열
PostgreSQL과 같은 관계형 데이터베이스 시스템 내의 테이블은 종이나 스프레드시트의 익숙한 테이블 구조를 반영합니다. 즉, 행과 열로 구성됩니다. 각 열은 특정 속성 또는 정보 조각을 나타내며, 각 행은 해당 속성에 대한 값으로 구성된 단일 레코드 또는 엔티티를 나타냅니다.
생성 시 열의 수와 순서는 고정되며 각 열에는 해당 테이블 내에서 고유한 이름이 할당됩니다. 이 고정된 구조는 데이터 저장 및 검색의 일관성과 예측 가능성을 보장합니다. 반대로 행의 수는 가변적이며, 특정 시점에 저장된 데이터 양을 반영하고 레코드가 추가되거나 제거됨에 따라 테이블이 동적으로 증가하거나 축소될 수 있도록 합니다. 열(스키마 정의)의 고정된 특성과 행(데이터 인스턴스 보유)의 가변적 특성 사이의 이러한 본질적인 대조는 안정적인 구조적 프레임워크 내에서 진화하는 데이터 세트를 관리하는 관계형 모델의 능력에 기본이 됩니다.
표준 SQL에 의해 정의된 기본 테이블 구조는 행이 저장되거나 검색되는 순서를 본질적으로 보장하지 않는다는 점을 인식하는 것이 중요합니다. 행은 삽입 시간이나 물리적 저장 위치에 따라 특정 순서로 나타날 수 있지만, 쿼리에서 ORDER BY 절을 사용하여 명시적으로 요청하지 않는 한 이 순서는 보장되지 않습니다. 또한 특정 제약 조건이 적용되지 않는 한 테이블에는 여러 개의 동일한 행이 포함될 수 있습니다. 이는 테이블의 주요 역할이 구조적 조직이며, 순서 지정 및 고유성과 같은 속성은 특정 SQL 명령이나 제약 조건을 통해 계층화된다는 점을 강조합니다.
1.2 데이터 구성에서 테이블의 목적
데이터베이스 테이블의 근본적인 목적은 관련 데이터를 저장하기 위한 구조화된 메커니즘을 제공하는 것입니다. 일반적인 관계형 데이터베이스 설계에서는 정보가 여러 테이블로 분리되며, 각 테이블은 고유한 엔티티 유형(예: customers, products, orders)을 나타냅니다. 이러한 구성은 효율적인 데이터 관리를 용이하게 하여 관계형 모델의 원칙에 따라 정보의 대상 지정 저장, 검색, 수정 및 삭제를 가능하게 합니다. 데이터를 논리적으로 구조화함으로써 테이블은 복잡한 쿼리와 관계를 효과적으로 정의하고 관리할 수 있게 합니다.
1.3 데이터 유형의 역할
테이블 정의의 초석은 각 열에 데이터 유형을 할당하는 것입니다. 데이터 유형은 두 가지 주요 기능을 수행합니다. 열에 저장될 수 있는 가능한 값 집합을 제한하고, 저장된 데이터에 의미를 할당하여 조작 방법을 결정합니다. 예를 들어, integer로 정의된 열은 정수만 허용하고 수학적 연산을 허용하는 반면, text 열은 연결과 같은 텍스트 조작에 적합한 문자열을 허용합니다.
PostgreSQL은 숫자 유형(smallint, integer, bigint, numeric, real, double precision), 문자 유형(char(n), varchar(n), text), 날짜/시간 유형(date, time, timestamp, interval), 부울(boolean), 이진 데이터(bytea), 네트워크 주소(inet, cidr), 기하학적 유형(point, line, box, polygon), 고유 식별자(uuid), 문서 유형(json, jsonb, xml) 등 다양한 요구 사항을 충족하는 풍부한 내장 데이터 유형 세트를 자랑합니다. 또한 사용자는 CREATE TYPE 명령을 사용하여 사용자 정의 데이터 유형을 생성하여 유형 시스템을 확장할 수 있습니다.
다음은 PostgreSQL에서 자주 사용되는 몇 가지 데이터 유형입니다 :
데이터 유형 범주 | 예시 유형 | 설명 |
숫자 | integer, bigint, numeric(p, s), real, double precision | 정수, 임의 정밀도 숫자, 부동 소수점 숫자 |
문자 | char(n), varchar(n), text | 고정 길이 문자열, 가변 길이 문자열 |
날짜/시간 | date, time, timestamp, timestamptz, interval | 날짜, 시간, 날짜 및 시간 (타임존 포함/미포함), 시간 간격 |
부울 | boolean | 논리적 참/거짓 값 |
이진 | bytea | 이진 데이터 ("바이트 배열") |
기하 | point, line, box, polygon, circle | 2차원 평면상의 기하학적 객체 |
네트워크 주소 | inet, cidr, macaddr | IP 주소, 네트워크 주소, MAC 주소 |
JSON | json, jsonb | 텍스트 JSON 데이터, 이진 JSON 데이터 |
UUID | uuid | 범용 고유 식별자 |
데이터 유형 선택은 데이터 무결성을 보장하는 첫 번째 방어선으로, 근본적으로 잘못된 데이터(예: 숫자 필드의 텍스트)가 저장되는 것을 방지합니다. 또한 저장 효율성과 쿼리 성능에도 상당한 영향을 미칩니다.
섹션 2: 특수 열 속성 및 정의
기본 이름과 데이터 유형 외에도 열은 값 할당 또는 계산을 자동화하는 특수 속성을 가질 수 있어 애플리케이션 로직을 단순화하고 데이터 관리를 향상시킵니다.
2.1 기본값 할당 (DEFAULT)
DEFAULT 절은 INSERT 작업 중에 해당 열에 대한 명시적 값이 제공되지 않은 경우 열에 기본값을 자동으로 할당하는 메커니즘을 제공합니다. 이는 사용자가 지정하지 않은 경우에도 열에 합리적인 값이 있도록 보장하여 삽입 중에 누락된 값을 명시적으로 처리할 필요성을 줄이는 데 유용합니다.
구문은 열 정의 내에서 DEFAULT 키워드 다음에 기본 표현식(default_expr)을 지정하는 것을 포함합니다. 이 표현식은 열의 데이터 유형과 호환되는 값을 생성해야 합니다. 중요한 것은 표현식이 변수가 없어야 한다는 것입니다. 즉, 동일한 테이블 내의 다른 열을 참조할 수 없으며 하위 쿼리, 쿼리 매개변수, 집계 또는 창/분석 함수를 포함할 수 없습니다. 그러나 비결정적일 수는 있습니다. 일반적인 예로는 리터럴 값(예: DEFAULT 0, DEFAULT 'active'), 생성 시간을 기록하기 위한 current_timestamp와 같은 함수 또는 시퀀스 생성기에서 값을 가져오기 위한 nextval('sequence_name')이 있습니다. 열 정의에 DEFAULT 절이 생략되면 기본값은 암시적으로 NULL입니다.
예시:
CREATE TABLE distributors (
name varchar(40) DEFAULT 'Luso Films', -- 문자열 리터럴 기본값
did integer DEFAULT nextval('distributors_serial'), -- 시퀀스 기본값
modtime timestamp DEFAULT current_timestamp -- 함수 기본값
);
이 예시에서 distributors 테이블에 새 행을 삽입할 때 name을 지정하지 않으면 'Luso Films'가 기본값으로 사용됩니다. did 열은 distributors_serial 시퀀스에서 다음 값을 가져오고, modtime 열은 현재 타임스탬프를 기본값으로 갖습니다.
2.2 ID 열: 자동 증가 키 (GENERATED AS IDENTITY, SERIAL)
특히 기본 키 열에 대한 일반적인 요구 사항은 각 새 행에 대해 고유하고 순차적인 숫자 식별자를 자동으로 생성하는 것입니다. PostgreSQL은 이를 위해 두 가지 주요 접근 방식을 제공합니다. 전통적인 PostgreSQL 특정 SERIAL 의사 유형과 최신 SQL 표준 GENERATED AS IDENTITY 절입니다.
SERIAL 유형(smallserial, serial, bigserial, 각각 smallint, integer, bigint에 해당)은 약식으로 작동합니다. 열을 SERIAL로 정의하면 자동으로 세 가지 작업이 수행됩니다.
1. 기본 시퀀스 생성기를 생성
2. 열의 기본값을 nextval()을 사용하여 해당 시퀀스에서 다음 값을 검색하도록 설정
3. 열에 NOT NULL 제약 조건을 추가합니다.
편리하지만 SERIAL은 PostgreSQL 확장이며 SQL 표준의 일부가 아닙니다. 기본 시퀀스 구성(예: 시작 값, 증분)은 테이블 생성 후 별도의 ALTER SEQUENCE 명령이 필요합니다.
SERIAL 예시:
CREATE TABLE products (
product_id SERIAL PRIMARY KEY, -- SERIAL을 사용한 자동 증가 기본 키
product_name VARCHAR(100) NOT NULL
);
이 예시에서 product_id는 SERIAL로 정의되어, 새 제품이 삽입될 때마다 자동으로 증가하는 정수 값이 할당됩니다.
PostgreSQL 버전 10에서 도입된 GENERATED AS IDENTITY 절은 ID 열을 정의하는 표준 준수 방법을 제공합니다. 암시적으로 생성된 시퀀스를 열과 명시적으로 연결합니다. SERIAL과 마찬가지로 ID 열은 암시적으로 NOT NULL입니다. 구문은 두 가지 주요 동작을 제공합니다.
- GENERATED ALWAYS AS IDENTITY: PostgreSQL에 시퀀스를 사용하여 열에 대한 값을 항상 생성하도록 지시합니다. INSERT 문에 OVERRIDING SYSTEM VALUE 절이 포함되지 않는 한 사용자가 INSERT 중에 값을 제공하려고 시도하면 오류가 발생합니다. 마찬가지로 열 값을 변경하려는 UPDATE 명령도 거부됩니다. 이는 엄격한 적용을 제공하여 데이터베이스가 ID 값을 제어하도록 보장합니다.
- GENERATED BY DEFAULT AS IDENTITY: PostgreSQL이 사용자가 INSERT 중에 값을 제공하지 않는 경우에만 값을 생성해야 함을 지정합니다. 값이 제공되면 시퀀스 생성 값보다 우선합니다. 이 동작은 SERIAL 열의 동작과 유사하며 때때로 수동 ID 할당이 필요할 수 있는 경우에 더 많은 유연성을 제공합니다.
GENERATED AS IDENTITY 예시:
-- GENERATED ALWAYS 예시
CREATE TABLE color (
color_id INT GENERATED ALWAYS AS IDENTITY, -- 항상 자동 생성
color_name VARCHAR NOT NULL
);
-- GENERATED BY DEFAULT 예시
CREATE TABLE distributors (
did integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, -- 기본적으로 자동 생성, 사용자 값 허용
name varchar(40) NOT NULL CHECK (name <> '')
);
첫 번째 예시(color 테이블)에서 color_id는 항상 데이터베이스에 의해 생성되며, 사용자가 값을 직접 삽입하려고 하면 오류가 발생합니다 (단, OVERRIDING SYSTEM VALUE 사용 시 제외). 두 번째 예시(distributors 테이블)에서는 사용자가 did 값을 제공하면 해당 값이 사용되고, 제공하지 않으면 데이터베이스가 값을 생성합니다.
GENERATED AS IDENTITY 절은 CREATE SEQUENCE에서 사용 가능한 매개변수(예: INCREMENT BY, START WITH, MINVALUE, MAXVALUE, CYCLE)를 미러링하는 sequence_options를 사용하여 기본 시퀀스의 인라인 구성도 허용합니다. 이는 SERIAL 접근 방식에 비해 시퀀스 관리를 테이블 정의와 더 긴밀하게 통합합니다.
SERIAL과 IDENTITY가 모두 존재하는 것은 이전 버전과의 호환성/편의성과 SQL 표준 준수 증가에 대한 PostgreSQL의 약속을 반영합니다. 개발자는 PostgreSQL 관용적 관행과 IDENTITY 열이 제공하는 표준 준수 및 명시적 제어의 이점 사이에서 균형을 맞추면서 자신의 요구에 가장 적합한 메커니즘을 선택할 수 있습니다.
다음 표는 주요 차이점을 요약합니다.
기능 | SERIAL / BIGSERIAL | GENERATED BY DEFAULT AS IDENTITY | GENERATED ALWAYS AS IDENTITY |
SQL 표준 준수 | 아니요 (PostgreSQL 특정) | 예 | 예 |
명시적 구문 | 의사 유형 약식 | 명시적 GENERATED...IDENTITY 절 | 명시적 GENERATED...IDENTITY 절 |
사용자 입력 제어 | 사용자 제공 값 허용 | 사용자 제공 값 허용 | 사용자 제공 값 거부 (OVERRIDING SYSTEM VALUE 제외) |
시퀀스 구성 | 별도의 ALTER SEQUENCE | CREATE TABLE의 통합된 sequence_options | CREATE TABLE의 통합된 sequence_options |
Null 허용 여부 | 암시적으로 NOT NULL | 암시적으로 NOT NULL | 암시적으로 NOT NULL |
기본 메커니즘 | 시퀀스 생성, DEFAULT nextval() 설정 | 시퀀스 생성, 직접 연결 | 시퀀스 생성, 직접 연결 |
2.3 생성된 열: 계산된 값 (GENERATED ALWAYS AS... STORED)
생성된 열은 열의 값이 직접 삽입되거나 업데이트되지 않고 동일한 행 내의 다른 열을 포함하는 표현식을 기반으로 자동으로 계산되는 특수 범주입니다. 개념적으로 뷰처럼 작동하지만 열 수준에서 작동합니다.
PostgreSQL은 현재 저장된(stored) 생성된 열 한 가지만 구현합니다. 이는 행이 쓰여질 때(INSERT 또는 UPDATE를 통해) 값이 계산되고 결과가 다른 열 데이터와 함께 디스크에 물리적으로 저장됨을 의미합니다. 이는 (현재 PostgreSQL에서 지원되지 않는) 가상 생성된 열과 대조됩니다. 가상 생성된 열은 열을 읽을 때만 값을 계산하고 추가 저장 공간을 차지하지 않습니다.
저장된 생성된 열을 정의하는 구문은 다음과 같습니다. column_name data_type GENERATED ALWAYS AS (expression) STORED .
사용 사례:
저장된 생성된 열은 파생된 값이 자주 쿼리되거나 인덱싱에 필요할 때 유용합니다. 결과를 미리 계산하고 저장함으로써 모든 쿼리에서 즉석에서 값을 계산하는 것과 비교하여 읽기 성능을 잠재적으로 향상시킬 수 있습니다. 일반적인 예는 다음과 같습니다.
- first_name 및 last_name 열에서 full_name 열 생성.
- list_price, tax, discount 열을 기반으로 net_price 계산.
- 파생된 기하학적 속성 또는 데이터의 표준화된 표현 저장.
예시 1: 이름 연결
CREATE TABLE contacts(
id SERIAL PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
-- full_name은 first_name과 last_name을 연결하여 생성됨
full_name VARCHAR(101) GENERATED ALWAYS AS (first_name |
| ' ' |
| last_name) STORED,
email VARCHAR(300) UNIQUE
);
INSERT INTO contacts(first_name, last_name, email)
VALUES ('John', 'Doe', 'john.doe@example.com');
SELECT * FROM contacts;
-- 결과: id=1, first_name='John', last_name='Doe', full_name='John Doe', email='...'
이 예시에서 full_name 열은 first_name 과 last_name 열의 값을 공백으로 연결하여 자동으로 계산되고 저장됩니다.
예시 2: 순 가격 계산
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
list_price DECIMAL(10, 2) NOT NULL,
tax DECIMAL(5, 2) DEFAULT 0,
discount DECIMAL(5, 2) DEFAULT 0,
-- net_price는 list_price, tax, discount를 사용하여 계산됨
net_price DECIMAL(10, 2) GENERATED ALWAYS AS
((list_price + (list_price * tax / 100)) - (list_price * discount / 100)) STORED
);
INSERT INTO products (name, list_price, tax, discount)
VALUES ('Laptop', 1200.00, 10.00, 5.00);
SELECT name, net_price FROM products WHERE id = 1;
-- 결과: name='Laptop', net_price=1260.00
여기서 net_price 는 정가, 세금, 할인을 고려하여 계산된 최종 가격을 저장합니다.
제한 사항:
무결성과 효율적인 계산을 보장하기 위해 생성된 열에는 몇 가지 제한 사항이 적용됩니다.
- 쓰기 제한: 생성된 열에는 직접 쓸 수 없습니다. INSERT 또는 UPDATE 문은 해당 열에 대한 값을 지정할 수 없지만, INSERT 중에 DEFAULT 키워드를 사용할 수 있습니다.
- 표현식 제약 조건: 생성 표현식에는 제한 사항이 있습니다.
- 불변(immutable) 함수(동일한 입력에 대해 항상 동일한 결과를 반환하고 부작용이 없는 함수)만 사용할 수 있습니다. random() 또는 now()와 같은 휘발성 함수는 허용되지 않습니다.
- 하위 쿼리를 포함할 수 없습니다.
- 현재 행 외부의 어떤 것도 참조할 수 없습니다(예: 다른 테이블, 이전 행).
- 동일한 테이블 내의 다른 생성된 열을 참조할 수 없습니다.
- tableoid를 제외한 시스템 열을 참조할 수 없습니다.
- 불변(immutable) 함수(동일한 입력에 대해 항상 동일한 결과를 반환하고 부작용이 없는 함수)만 사용할 수 있습니다. random() 또는 now()와 같은 휘발성 함수는 허용되지 않습니다.
- 기타 정의: 생성된 열은 동시에 DEFAULT 절이나 IDENTITY 정의를 가질 수 없습니다.
- 파티셔닝: 생성된 열은 테이블의 파티션 키의 일부가 될 수 없습니다.
- 상속/트리거/복제: 상속 계층 및 파티션된 테이블에서의 동작에 대한 특정 규칙이 적용됩니다. BEFORE 트리거에서는 액세스할 수 없지만, BEFORE 트리거에서 기본 열에 대한 변경 사항은 생성된 열의 계산에 반영됩니다. 논리적 복제 중에도 건너뜁니다.
저장된 생성된 열의 사용에는 장단점이 있습니다. 추가 디스크 공간을 소비하고 쓰기 작업(INSERT, UPDATE)에 계산 오버헤드를 추가합니다. 왜냐하면 표현식을 평가하고 결과를 저장해야 하기 때문입니다. 그러나 자주 액세스되는 파생 데이터의 경우, 이 쓰기 시간 비용은 읽기 성능 향상으로 상쇄될 수 있습니다. SELECT 쿼리 중에 재계산할 필요 없이 값을 즉시 사용할 수 있기 때문입니다. 생성 표현식에 대한 제한 사항은 결정론적 동작을 유지하고 쓰기 중 계산이 효율적이고 영향을 받는 행에 국한되도록 보장하여 복잡한 종속성이나 성능 문제를 방지하는 데 필수적입니다.
섹션 3: 제약 조건을 통한 데이터 무결성 보장
데이터 유형은 기본적인 수준의 유효성 검사를 제공하지만, 제약 조건은 테이블에 저장된 데이터에 대한 규칙을 정의하고 시행하는 더 강력하고 세분화된 메커니즘을 제공하여 데이터의 정확성, 일관성 및 전반적인 무결성을 보장합니다.
3.1 제약 조건의 역할
제약 조건은 SQL을 사용하여 테이블 스키마의 일부로 정의된 공식적인 규칙으로, 삽입 또는 업데이트 작업이 성공하려면 새 데이터나 업데이트된 데이터가 만족해야 합니다. 제약 조건은 데이터 품질의 수호자 역할을 하여 데이터베이스 내에서 일관성이 없거나 논리적으로 잘못된 상태로 이어질 수 있는 작업을 방지합니다. 사용자가 정의된 제약 조건을 위반하는 INSERT 또는 UPDATE 작업을 시도하면 PostgreSQL은 오류를 발생시키고 작업이 중단되어 기존 데이터의 무결성을 보존합니다. 제약 조건을 통해 데이터베이스 설계자는 비즈니스 규칙과 구조적 요구 사항을 데이터베이스 스키마 자체에 직접 인코딩할 수 있습니다.
3.2 열 제약 조건 대 테이블 제약 조건
PostgreSQL은 구문적으로 두 가지 방식으로 제약 조건을 정의할 수 있습니다:
- 열 제약 조건: CREATE TABLE 문 내에서 개별 열 정의의 일부로 정의됩니다. 일반적으로 해당 특정 열에만 적용됩니다.
- 테이블 제약 조건: CREATE TABLE 문 내에서 별도로 정의되며, 단일 열 정의에 직접 연결되지 않습니다. 여러 열에 걸친 제약 조건을 정의할 수 있습니다.
본질적으로 단일 열에만 영향을 미치는 제약 조건(예: NOT NULL 또는 단일 열에 적용되는 CHECK 또는 UNIQUE 제약 조건)의 경우, 열 제약 조건으로 정의하는 것은 종종 표기상의 편의 문제입니다. 그러나 제약 조건이 여러 열을 포함하거나 참조해야 하는 경우(예: 다중 열 UNIQUE 키, 두 열을 비교하는 CHECK 제약 조건, 다중 열 FOREIGN KEY) 테이블 제약 조건이 필요합니다.
NOT NULL 제약 조건은 항상 열 제약 조건으로 작성해야 한다는 예외가 있습니다. 다른 일반적인 제약 조건(CHECK, UNIQUE, PRIMARY KEY, FOREIGN KEY)은 단일 열에 적용될 때 두 구문을 모두 사용하여 표현할 수 있지만, 다중 열 적용에는 테이블 제약 조건 구문이 필요합니다. 이러한 구분은 CREATE TABLE 문이 구성되는 방식을 안내하며, 지역화된 규칙에는 열 제약 조건을 사용하고 더 광범위한 규칙이나 스타일 일관성을 위해서는 테이블 제약 조건을 사용합니다.
다음 표는 일반적인 제약 조건 유형에 대한 정의 범위를 요약합니다 :
제약 조건 유형 | 열 제약 조건 허용 여부 | 테이블 제약 조건 허용 여부 | 다중 열 가능 여부 |
CHECK | 예 | 예 | 예 (테이블 제약 조건으로) |
NOT NULL | 예 | 아니요* | 아니요 |
UNIQUE | 예 | 예 | 예 (테이블 제약 조건으로) |
PRIMARY KEY | 예 | 예 | 예 (테이블 제약 조건으로) |
FOREIGN KEY | 예 | 예 | 예 (테이블 제약 조건으로) |
EXCLUDE | 아니요 | 예 | 예 (테이블 제약 조건으로) |
참고: NOT NULL 기능은 테이블 CHECK 제약 조건(예: CHECK (column IS NOT NULL))으로 모방할 수 있지만 , 직접적인 NOT NULL 구문은 열 전용입니다.
3.3 CHECK 제약 조건 (CHECK)
CHECK 제약 조건은 행 내의 데이터 값에 대한 임의의 조건을 적용하는 다목적 방법을 제공합니다. INSERT 또는 UPDATE 작업이 성공하려면 TRUE 또는 UNKNOWN(null)으로 평가되어야 하는 부울 표현식을 지정합니다. 표현식이 FALSE로 평가되면 작업이 실패합니다.
예를 들어 price 열이 양수인지 확인(CHECK (price > 0)), end_date가 start_date 이후에 발생하는지 확인(CHECK (end_date > start_date)), 특정 형식 또는 값 범위 적용 등이 있습니다. 테이블 제약 조건으로 정의된 경우 표현식은 평가되는 행의 여러 열을 참조할 수 있습니다.
예시:
CREATE TABLE products (
product_no integer,
name text,
price numeric CHECK (price > 0), -- 열 CHECK 제약 조건
discounted_price numeric,
CHECK (price > discounted_price) -- 테이블 CHECK 제약 조건 (여러 열 참조)
);
-- 제약 조건 위반 시도
INSERT INTO products (product_no, name, price, discounted_price) VALUES (1, 'Test', 10, 15);
-- ERROR: new row for relation "products" violates check constraint "products_check"
-- DETAIL: Failing row contains (1, Test, 10, 15).
이 예시에서 price 열에는 양수만 허용하는 열 제약 조건이 있고, 테이블 수준에서는 price가 discounted_price보다 커야 한다는 제약 조건이 있습니다. 마지막 INSERT 문은 두 번째 제약 조건을 위반하여 실패합니다.
그러나 PostgreSQL의 CHECK 제약 조건에는 제한 사항이 있습니다. 표현식에는 하위 쿼리가 포함될 수 없으며, 삽입되거나 업데이트되는 현재 행의 열 이외의 변수를 참조할 수도 없습니다. 검사는 다른 테이블의 데이터를 참조할 수도 없습니다. 또한 표현식 내에서 사용되는 함수는 일관된 결과를 보장하기 위해 이상적으로는 불변(immutable)이어야 합니다. 이러한 제한 사항은 CHECK 제약 조건을 수정되는 단일 행 내의 데이터를 기반으로 한 조건 유효성 검사로 제한합니다. 이 설계는 DML 작업 중 제약 조건 평가의 성능과 단순성을 우선시합니다. 다른 행, 다른 테이블 또는 집계 상태에 액세스해야 하는 더 복잡한 유효성 검사 논리는 일반적으로 트리거를 사용하거나 애플리케이션 수준에서 처리해야 합니다.
CHECK 제약 조건은 표현식이 TRUE 또는 NULL(알 수 없음)으로 평가되면 만족된다는 점에 유의하는 것도 중요합니다. 따라서 간단한 CHECK (price > 0) 제약 조건은 price 열의 NULL 값을 방지하지 않습니다. null을 허용하지 않으려면 NOT NULL 제약 조건을 사용하거나 검사 표현식에서 명시적으로 null을 처리해야 합니다(예: CHECK (price IS NOT NULL AND price > 0)).
3.4 NOT NULL 제약 조건 (NOT NULL)
NOT NULL 제약 조건은 열이 NULL 값을 저장할 수 없도록 보장하는 가장 기본적인 데이터 무결성 규칙 중 하나입니다. 필수 정보를 나타내는 열에 필수적입니다.
구문적으로 NOT NULL은 항상 데이터 유형 바로 뒤에 열 제약 조건으로 지정됩니다. CHECK (column_name IS NOT NULL)을 사용하여 동일한 논리적 규칙을 표현할 수 있지만, PostgreSQL은 전용 NOT NULL 제약 조건을 더 효율적으로 구현합니다. 다른 제약 조건 유형과 달리 이 방식으로 정의된 NOT NULL 제약 조건에는 명시적인 이름을 지정할 수 없습니다.
예시:
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL, -- NOT NULL 제약 조건
email VARCHAR(255) UNIQUE NOT NULL, -- NOT NULL 제약 조건
password VARCHAR(50) NOT NULL -- NOT NULL 제약 조건
);
-- 제약 조건 위반 시도
INSERT INTO users (username, email) VALUES ('testuser', 'test@example.com');
-- ERROR: null value in column "password" violates not-null constraint
이 예시에서 username, email, password 열은 모두 NOT NULL로 정의되어 있어 해당 열에 값을 제공하지 않고 행을 삽입하려고 하면 오류가 발생합니다.
3.5 UNIQUE 제약 조건 (UNIQUE)
UNIQUE 제약 조건은 지정된 열(또는 열 그룹)의 값(또는 값 조합)이 테이블 내의 모든 행에서 고유함을 보장합니다. 이는 식별자, 이메일 주소 또는 고유해야 하는 기타 속성에 대한 중복 항목을 방지하는 데 중요합니다.
UNIQUE 제약 조건은 단일 열에 적용되는 경우 열 제약 조건으로 정의하거나, 하나 또는 여러 열에 적용되는 경우 테이블 제약 조건으로 정의할 수 있습니다. 테이블 제약 조건 구문은 열 조합(예: UNIQUE (order_id, product_id))에 대한 고유성을 적용하는 데 필요합니다.
예시:
-- 단일 열 UNIQUE (열 제약 조건)
CREATE TABLE products (
product_no integer UNIQUE, -- product_no는 고유해야 함
name text,
price numeric
);
-- 다중 열 UNIQUE (테이블 제약 조건)
CREATE TABLE example (
a integer,
b integer,
c integer,
UNIQUE (a, c) -- (a, c) 조합은 고유해야 함
);
-- 제약 조건 위반 시도 (products 테이블)
INSERT INTO products (product_no, name, price) VALUES (1, 'Cheese', 9.99);
INSERT INTO products (product_no, name, price) VALUES (1, 'Bread', 1.99);
-- ERROR: duplicate key value violates unique constraint "products_product_no_key"
-- DETAIL: Key (product_no)=(1) already exists.
첫 번째 예시에서는 product_no 열에 UNIQUE 제약 조건이 적용됩니다. 두 번째 예시에서는 a와 c 열의 조합이 고유해야 합니다. 마지막 INSERT 문은 products 테이블의 UNIQUE 제약 조건을 위반하여 실패합니다.
UNIQUE 제약 조건의 중요한 측면은 NULL 값 처리입니다. 기본적으로 (그리고 SQL 표준에 따라) NULL 값은 서로 같다고 간주되지 않습니다. 즉, UNIQUE 제약 조건이 있는 단일 열은 여러 행이 해당 열에 NULL을 포함하도록 허용합니다. PostgreSQL은 NULLS NOT DISTINCT를 지정하여 이 동작을 변경할 수 있도록 허용합니다. 이는 제약 조건의 목적상 NULL 값을 동일하게 처리하여 제약된 열(들)에 하나의 NULL 값만 허용합니다. 기본 동작은 NULLS DISTINCT를 사용하여 명시적으로 지정할 수 있습니다.
중요하게도, UNIQUE 제약 조건을 추가하면 백그라운드에서 제약된 열(들)에 대한 고유 B-트리 인덱스가 자동으로 생성됩니다. 이 인덱스는 데이터베이스가 INSERT 및 UPDATE 작업 중에 위반 사항을 효율적으로 확인하는 데 사용하는 메커니즘입니다. 따라서 동일한 열에 대해 별도의 고유 인덱스를 수동으로 생성하는 것은 중복됩니다. 이 자동 인덱스 생성은 논리적 데이터 무결성 규칙과 성능적인 적용에 필요한 물리적 인덱싱 구조 간의 긴밀한 통합을 강조합니다. 선택적 INCLUDE 절을 사용하면 인덱스에 저장할 추가 열을 지정하여 특정 쿼리에 대한 인덱스 전용 스캔을 가능하게 할 수 있습니다.
섹션 4: 관계 및 고유성 설정
다양한 제약 조건 유형 중에서 기본 키와 외래 키는 관계형 데이터베이스 설계의 기초를 형성하므로 레코드의 고유 식별과 테이블 간의 논리적 연결 적용을 가능하게 하는 특별한 지위를 갖습니다.
4.1 기본 키 제약 조건 (PRIMARY KEY)
PRIMARY KEY 제약 조건은 값이 테이블 내의 각 행을 고유하게 식별하는 열 또는 열 집합을 지정합니다. 이는 레코드에 대한 최종 식별자를 제공하는 가장 중요한 제약 조건 유형이라고 할 수 있습니다.
기능적으로 PRIMARY KEY 제약 조건은 지정된 열(들)에 대한 UNIQUE 제약 조건과 NOT NULL 제약 조건의 속성을 모두 결합합니다. 즉, 기본 키 값(들)은 모든 행에서 고유해야 하며 관련된 열 중 어느 것도 NULL 값을 포함할 수 없습니다. 테이블은 최대 하나의 기본 키만 정의하도록 제한되지만, 이 키는 여러 열에 걸쳐 있을 수 있습니다(복합 기본 키라고 함).
예시:
-- 단일 열 PRIMARY KEY (열 제약 조건)
CREATE TABLE products (
product_no integer PRIMARY KEY, -- product_no는 기본 키 (UNIQUE + NOT NULL)
name text,
price numeric
);
-- 복합 PRIMARY KEY (테이블 제약 조건)
CREATE TABLE example (
a integer,
b integer,
c integer,
PRIMARY KEY (a, c) -- (a, c) 조합이 기본 키
);
-- 제약 조건 위반 시도 (products 테이블)
INSERT INTO products (product_no, name, price) VALUES (NULL, 'Test', 1.00);
-- ERROR: null value in column "product_no" violates not-null constraint
INSERT INTO products (product_no, name, price) VALUES (1, 'Cheese', 9.99);
INSERT INTO products (product_no, name, price) VALUES (1, 'Bread', 1.99);
-- ERROR: duplicate key value violates unique constraint "products_pkey"
첫 번째 예시에서 product_no는 기본 키입니다. 두 번째 예시에서는 a와 c의 조합이 기본 키입니다. 마지막 두 INSERT 문은 각각 NOT NULL 및 UNIQUE 속성을 위반하여 실패합니다.
UNIQUE 제약 조건과 유사하게, PRIMARY KEY를 정의하면 고유성 요구 사항을 효율적으로 적용하기 위해 키 열(들)에 대한 기본 고유 B-트리 인덱스가 자동으로 생성됩니다. INCLUDE 절은 기본 키와 함께 사용하여 이 인덱스에 키가 아닌 열을 추가할 수도 있습니다.
기본 키는 여러 가지 중요한 목적을 수행합니다. 애플리케이션과 사용자가 특정 행을 참조하는 신뢰할 수 있는 방법을 제공하고, 테이블 구조를 문서화하는 데 기본이 되며, 다른 테이블이 이 테이블을 참조하는 외래 키 관계를 설정할 때 기본 대상 열 역할을 합니다.
4.2 외래 키 제약 조건 (FOREIGN KEY)
외래 키 제약 조건은 테이블 간의 관계를 설정하고 적용하여 참조 무결성을 보장하는 메커니즘입니다. 한 테이블(참조하는 테이블)의 열(또는 열 집합)에 정의된 외래 키는 해당 열의 값이 다른 테이블(참조되는 테이블)의 해당 기본 키 또는 고유 제약 조건 열(들)에 있는 값과 일치해야 함을 요구합니다.
이 제약 조건은 "고아" 레코드 생성을 방지합니다. 예를 들어, order 레코드는 관련 customer_id가 customers 테이블의 실제 레코드에 해당하지 않는 한 존재할 수 없도록 보장할 수 있습니다. 외래 키를 정의하는 사용자는 참조되는 테이블에 대한 REFERENCES 권한을 보유해야 합니다. REFERENCES 절에 참조되는 열이 명시적으로 지정되지 않은 경우 PostgreSQL은 참조가 참조되는 테이블의 기본 키에 대한 것이라고 가정합니다. 참조되는 열은 기본 키를 구성하거나 고유 제약 조건 또는 인덱스의 대상이어야 합니다. 필수는 아니지만, 참조하는 열을 인덱싱하는 것은 외래 키 관계를 기반으로 한 조인을 포함하는 쿼리의 성능에 종종 유익합니다.
예시:
-- 먼저 참조될 테이블 생성
CREATE TABLE products (
product_no integer PRIMARY KEY,
name text,
price numeric
);
-- 외래 키를 포함하는 테이블 생성
CREATE TABLE orders (
order_id integer PRIMARY KEY,
shipping_address text,
product_no integer REFERENCES products (product_no) -- 외래 키 (products.product_no 참조)
);
-- 다른 테이블의 UNIQUE 열 참조 예시
CREATE TABLE product_codes (
code char(5) UNIQUE,
description text
);
CREATE TABLE order_details (
order_id integer REFERENCES orders(order_id),
product_code char(5) REFERENCES product_codes (code), -- 외래 키 (product_codes.code 참조)
quantity integer
);
-- 제약 조건 위반 시도 (orders 테이블)
INSERT INTO orders (order_id, shipping_address, product_no) VALUES (1, '123 Main St', 999);
-- ERROR: insert or update on table "orders" violates foreign key constraint "orders_product_no_fkey"
-- DETAIL: Key (product_no)=(999) is not present in table "products".
orders 테이블의 product_no 열은 products 테이블의 product_no (기본 키)를 참조합니다. order_details 테이블의 product_code 열은 product_codes 테이블의 code (UNIQUE 열)를 참조합니다. 마지막 INSERT 문은 products 테이블에 product_no가 999인 행이 없기 때문에 외래 키 제약 조건을 위반하여 실패합니다.
외래 키는 지정된 MATCH 유형에 따라 참조하는 열의 NULL 값을 처리할 수 있습니다. 기본값인 MATCH SIMPLE은 참조하는 열 중 하나라도 NULL이면 외래 키 제약 조건이 만족되도록 허용합니다. MATCH FULL은 참조하는 열 중 하나라도 NULL이 아닌 경우 모두 null이 아니어야 하며 조합이 참조된 행과 일치해야 합니다. 모든 참조하는 열이 NULL이면 제약 조건이 만족됩니다. MATCH PARTIAL은 SQL 표준에 정의되어 있지만 현재 PostgreSQL에서는 구현되지 않았습니다. 참조하는 열을 NOT NULL로 선언하면 null이 완전히 허용되지 않으므로 이러한 MATCH 규칙을 효과적으로 우회합니다.
기본 키와 외래 키는 함께 독립적인 테이블 모음을 응집력 있고 상호 연결된 데이터베이스로 변환합니다. 서로 다른 데이터 엔티티 간의 의도된 관계를 공식적으로 정의하고 데이터베이스 엔진이 이러한 관계의 일관성을 자동으로 적용하도록 하여 관계형 모델의 기반을 형성합니다.
4.2.1 참조 작업 (ON DELETE, ON UPDATE)
외래 키 제약 조건은 참조되는 테이블의 행이 삭제되거나(ON DELETE) 참조된 키 값이 업데이트될 때(ON UPDATE) 데이터베이스가 어떻게 반응해야 하는지를 지시하는 참조 작업을 지정할 수도 있습니다. 이러한 작업은 관련 테이블 간의 일관성을 유지하기 위한 선언적 규칙을 제공합니다. 사용 가능한 작업은 다음과 같습니다.
- NO ACTION (기본값): 제약 조건 검사가 발생할 때(제약 조건이 지연 가능한 경우 문 또는 트랜잭션 끝에 발생할 수 있음) 참조하는 행이 여전히 존재하는 경우, (참조되는 테이블에서의 삭제/업데이트) 작업은 오류를 발생시킵니다.
- RESTRICT: NO ACTION과 유사하지만, 검사는 항상 명령이 실행될 때 즉시 수행됩니다(지연될 수 없음). 참조하는 행이 존재하면 작업이 실패합니다.
- CASCADE: 참조된 행이 삭제되면 해당 행을 참조하는 모든 행도 자동으로 삭제됩니다. 참조된 키가 업데이트되면 참조하는 열의 해당 값이 새 값으로 자동으로 업데이트됩니다.
- SET NULL: 참조된 행이 삭제되거나 참조된 키가 업데이트되면 참조하는 행의 참조하는 열(들)이 NULL로 설정됩니다. 이를 위해서는 참조하는 열이 null을 허용해야 합니다.
- SET DEFAULT: 참조된 행이 삭제되거나 참조된 키가 업데이트되면 참조하는 행의 참조하는 열(들)이 정의된 기본값으로 설정됩니다. 이를 위해서는 참조하는 열에 적절한 기본값이 정의되어 있어야 합니다.
예시 (ON DELETE CASCADE):
CREATE TABLE products (
product_no integer PRIMARY KEY,
name text,
price numeric
);
CREATE TABLE orders (
order_id integer PRIMARY KEY,
shipping_address text,
product_no integer REFERENCES products (product_no) ON DELETE CASCADE -- CASCADE 옵션 추가
);
INSERT INTO products VALUES (1, 'Toy Car', 5.00);
INSERT INTO orders VALUES (101, '456 Oak Ave', 1);
INSERT INTO orders VALUES (102, '789 Pine St', 1);
SELECT * FROM orders; -- 2개의 주문이 표시됨
DELETE FROM products WHERE product_no = 1; -- product_no=1인 제품 삭제
SELECT * FROM orders; -- 주문이 없음 (CASCADE로 인해 삭제됨)
이 예시에서는 orders 테이블의 외래 키에 ON DELETE CASCADE가 지정되었습니다. products 테이블에서 product_no가 1인 행이 삭제되자, 해당 제품을 참조하던 orders 테이블의 모든 행도 자동으로 삭제되었습니다.
이러한 참조 작업은 계단식 삭제 또는 업데이트와 같은 복잡한 일관성 논리를 스키마 정의에 따라 데이터베이스 엔진이 자동으로 처리하도록 허용합니다. 이는 책임을 애플리케이션 계층에서 이동시켜 잠재적으로 안정성을 높이고 애플리케이션 코드를 단순화합니다. 그러나 작업 선택, 특히 CASCADE는 스키마 설계 중에 신중하게 고려해야 합니다. 자동 수정은 제대로 계획되지 않으면 광범위하고 때로는 의도하지 않은 결과를 초래할 수 있기 때문입니다.
섹션 5: 고급 제약 조건 메커니즘
표준 SQL 제약 조건 외에도 PostgreSQL은 더 복잡한 데이터 무결성 규칙을 적용하기 위한 고유한 메커니즘을 제공합니다.
5.1 제외 제약 조건 (EXCLUDE)
제외 제약 조건은 고유성 개념을 크게 일반화하는 강력한 PostgreSQL 특정 기능입니다. UNIQUE 제약 조건은 등호(=) 연산자를 기반으로 두 행이 동일하지 않도록 보장하는 반면, EXCLUDE 제약 조건은 지정된 열 또는 표현식에 대해 지정된 연산자를 사용하여 임의의 두 행을 비교할 때 모든 비교 결과가 TRUE가 되지 않도록 보장합니다. 본질적으로, 단순한 등호를 훨씬 넘어서는 정의된 기준 집합을 기반으로 행이 "충돌"하는 것을 방지합니다.
5.1.1 연산자 및 사용 사례
제외 제약 조건은 EXCLUDE USING index_method ( element WITH operator [,...] ) 구문을 사용하여 정의되며, 선택적으로 인덱스 매개변수와 부분 제약 조건을 위한 WHERE 절이 뒤따릅니다. index_method는 일반적으로 gist 또는 spgist입니다. 제약 조건의 핵심은 element WITH operator 쌍에 있습니다.
제외 제약 조건의 강력함은 선택한 index_method 및 관련 연산자 클래스와 호환되는 한 사용할 수 있는 다양한 operator에서 비롯됩니다:
- = (같음): 표준 UNIQUE 제약 조건을 모방하여 두 행이 열에서 동일한 값을 갖지 않도록 보장하는 데 사용할 수 있습니다.
- && (겹침): 이것은 아마도 가장 일반적인 사용 사례일 것입니다. 범위 유형(예: tsrange, daterange, numrange) 또는 기하학적 유형(box, circle)에 적용되어 행이 겹치는 범위나 교차하는 기하학적 구조를 갖는 것을 방지합니다.
- <> (같지 않음): 등록 값이 동일한 버스 ID에 속하지 않는 한 고유하도록 보장하는 것과 같은 더 복잡한 논리에 사용할 수 있습니다.
- 기타 범위/기하학적 연산자: @> (포함), <@ (포함됨), ~= (점/요소 포함)와 같은 연산자는 데이터 유형 및 원하는 논리에 따라 사용할 수 있습니다.
- 사용자 정의 연산자: 매우 특정한 제외 논리를 위해 사용자 정의 연산자를 정의하는 것이 이론적으로 가능하지만, 이는 PostgreSQL의 연산자 클래스 시스템과의 통합을 요구하는 상당한 복잡성을 수반합니다.
제외 제약 조건이 뛰어난 일반적인 사용 사례는 다음과 같습니다.
- 자원 스케줄링/예약: 동일한 자원에 대한 시간 슬롯(tsrange 또는 daterange)이 겹치지 않도록(&&) 하여 회의실, 장비 또는 인력의 이중 예약을 방지합니다.
- 가격 유효성: 겹치는 유효 기간을 방지하여 특정 시점에 주어진 제품에 대해 하나의 가격 정의만 활성화되도록 보장합니다.
- 지리 공간 제약 조건: 테이블에 저장된 지리적 영역(polygon 또는 box)이 교차하는 것을 방지합니다.
- 복잡한 비즈니스 규칙: 고유성이 다른 열의 값에 따라 달라지는 버스 등록 예와 같은 규칙을 적용합니다.
예시 1: 회의실 예약 (겹침 방지)
-- GiST 인덱스를 사용하기 위해 btree_gist 확장 활성화 (필요한 경우)
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE room_reservations (
room_name text,
reservation_period tsrange, -- 타임스탬프 범위 타입
EXCLUDE USING gist (
room_name WITH =, -- 방 이름은 같아야 하고
reservation_period WITH && -- 예약 기간은 겹치면 안 됨 (&& 연산자)
)
);
-- 유효한 예약 삽입
INSERT INTO room_reservations VALUES ('Room A', '
**예시 2: 버스 등록 (조건부 고유성)**
```sql
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE bus_register_ledger (
bus_id text,
registration text,
driver text,
-- registration은 같고 bus_id는 다른 경우를 제외함
EXCLUDE USING gist (
registration WITH =,
bus_id WITH <>
)
);
-- 초기 데이터 삽입
INSERT INTO bus_register_ledger(bus_id, registration, driver) VALUES ('bus-id-1', 'bussy1', 'jessica');
INSERT INTO bus_register_ledger(bus_id, registration, driver) VALUES ('bus-id-2', 'bussy2', 'john');
-- 같은 버스, 같은 등록번호 (운전자 변경 등) -> 허용됨
INSERT INTO bus_register_ledger(bus_id, registration, driver) VALUES ('bus-id-1', 'bussy1', 'adam');
-- 다른 버스, 이미 사용 중인 등록번호 -> 제약 조건 위반
INSERT INTO bus_register_ledger(bus_id, registration, driver) VALUES ('bus-id-3', 'bussy2', 'brendan');
-- ERROR: conflicting key value violates exclusion constraint "unique_bus_id_registration_pair"
-- DETAIL: Key (registration, bus_id)=(bussy2, bus-id-3) conflicts with existing key (registration, bus_id)=(bussy2, bus-id-2).
이 제약 조건은 registration 값이 같고(=) 동시에 bus_id 값이 다른(<>) 두 행이 존재하는 것을 방지합니다. 즉, 특정 등록 번호는 여러 버스에 할당될 수 없습니다.
5.1.2 GiST/SP-GiST 인덱스의 역할
제외 제약 조건은 효율적인 적용을 위해 기본적으로 기본 인덱스에 의존합니다. 일반적으로 등호 및 스칼라 유형의 범위 비교에 적합한 B-트리 인덱스를 사용하는 UNIQUE 및 PRIMARY KEY 제약 조건과 달리, 제외 제약 조건은 일반적으로 더 고급 인덱스 유형이 필요합니다: GiST (일반화된 검색 트리) 또는 SP-GiST (공간 분할 GiST).
이러한 인덱스 구조는 다차원 데이터, 비 스칼라 유형(범위, 기하학적 모양, 전체 텍스트 검색 벡터 등) 및 제외 제약 조건에서 자주 사용되는 복잡한 비교 연산자(예: &&, @>, <>)를 처리하도록 설계되었습니다. 표준 B-트리 인덱스는 일반적으로 = 연산자만 사용하는 단순한 제외를 제외하고는 이러한 작업에 충분하지 않습니다.
이러한 특수 인덱스 유형에 대한 요구 사항은 고급 제약 조건의 논리적 정의와 이를 효율적으로 지원하는 데 필요한 물리적 저장 및 인덱싱 메커니즘 간의 깊은 연관성을 강조합니다. EXCLUDE의 선언적 능력은 GiST 및 SP-GiST의 기능에 의해 직접적으로 가능해집니다.
또한, 제외 제약 조건이 동일한 GiST 인덱스 내에서 범위 또는 기하학적 유형과 함께 = 또는 <>와 같은 연산자를 사용하여 표준 데이터 유형(예: integer 또는 text)을 결합해야 하는 경우, 종종 btree_gist 확장을 설치해야 합니다(CREATE EXTENSION btree_gist;). 이 확장은 표준 B-트리 비교 가능 유형에 대한 GiST 연산자 클래스를 제공하여 GiST 구조 내에서 효과적으로 인덱싱할 수 있도록 합니다.
매우 강력하지만, 고도로 사용자 정의되거나 조건부 제외 논리를 정의하는 것은 선언적 프레임워크 내에서 제한에 부딪힐 수 있습니다. 예를 들어, 다른 열의 값을 기반으로 제외 제약 조건을 조건부로 만들려고 시도하는 경우(예: status = 'SCHEDULED'인 경우에만 겹침 제외) 복잡한 사용자 정의 연산자를 사용하고 잠재적으로 내부 연산자 클래스를 수정하지 않고는 매우 어렵거나 불가능할 수 있습니다. 이러한 경우 개발자는 부분 제외 제약 조건( WHERE 절 사용)과 애플리케이션 수준 검사를 결합하거나 데이터베이스 트리거를 사용하여 절차적 유효성 검사 논리를 구현하는 등 대체 전략을 채택해야 할 수 있습니다.
섹션 6: 시스템 열 이해
사용자가 CREATE TABLE에서 명시적으로 정의한 열 외에도 PostgreSQL은 모든 테이블에 여러 숨겨진 시스템 열을 자동으로 포함합니다. 이러한 열은 데이터베이스의 내부 작동, 특히 저장 및 동시성 제어와 관련된 메타데이터를 제공합니다.
6.1 암시적으로 정의된 열
모든 사용자 정의 테이블에는 PostgreSQL에서 관리하는 시스템 열이 암시적으로 포함됩니다. 이러한 열의 이름(예: ctid, xmin, xmax)은 예약되어 있으며 사용자 정의 열의 식별자로 사용할 수 없습니다. 일반적으로 표준 SELECT * 쿼리에서는 숨겨져 있지만, 필요한 경우 명시적으로 선택할 수 있으며, 종종 진단 목적, 관리 작업 또는 낮은 수준의 동작을 이해하는 데 사용됩니다.
6.2 주요 시스템 열 설명
여러 시스템 열은 유용한 내부 정보를 제공합니다:
시스템 열 | 설명 | 목적 |
oid | 객체 식별자 | 역사적으로 행의 고유 식별자로 사용되었으나 현재는 권장되지 않음. |
tableoid | 행을 포함하는 테이블의 OID | 상속/파티션된 테이블에서 행의 실제 출처 테이블 식별. pg_class.oid와 조인하여 테이블 이름 확인 가능. |
xmin | 삽입 트랜잭션 ID (XID) | 이 행 버전을 삽입한 트랜잭션 식별 (MVCC 핵심). |
cmin | 삽입 명령 ID (CID) | 삽입 트랜잭션 내의 특정 명령 식별 (0부터 시작). |
xmax | 삭제 트랜잭션 ID (XID) | 이 행 버전을 삭제한 트랜잭션 식별 (0이면 삭제되지 않음). 삭제 트랜잭션이 커밋되지 않았거나 롤백된 경우 0이 아닐 수 있음 (MVCC 핵심). |
cmax | 삭제 명령 ID (CID) | 삭제 트랜잭션 내의 특정 명령 식별 (0이면 삭제되지 않음). |
ctid | 행 버전의 물리적 위치 (Tuple ID) | 테이블 내 행 버전의 물리적 주소 ((페이지 번호, 페이지 내 아이템 인덱스)). 주의: UPDATE나 VACUUM FULL 등으로 변경될 수 있으므로 장기 식별자로 사용 불가. |
예시: 시스템 열 조회
-- 'employees' 테이블에서 일부 시스템 열과 사용자 정의 열 조회
SELECT ctid, xmin, tableoid, employee_id, name
FROM employees
LIMIT 5;
이 쿼리는 employees 테이블의 처음 5개 행에 대해 물리적 위치(ctid), 삽입 트랜잭션 ID(xmin), 테이블 OID(tableoid) 및 사용자 정의 열(employee_id, name)을 보여줍니다.
시스템 열, 특히 xmin 및 xmax는 PostgreSQL이 MVCC에 사용하는 기본 메커니즘을 노출합니다. 각 행 버전을 관리하는 트랜잭션 상태 및 가시성 규칙에 대한 창을 제공합니다. 애플리케이션은 일반적으로 데이터의 논리적 뷰(트랜잭션 스냅샷과 관련된 커밋된 행만 표시)에서 작동하지만, 이러한 열은 이 동시성 제어를 가능하게 하는 물리적 및 트랜잭션 현실을 보여줍니다.
ctid(물리적 위치)와 PRIMARY KEY(논리적 식별자)의 뚜렷한 특성은 중요합니다. ctid의 불안정성은 업데이트 또는 유지 관리 작업으로 인해 행의 물리적 주소가 변경될 수 있음을 강조하는 반면, 기본 키는 물리적 저장 세부 정보와 독립적인 일정한 논리적 참조 지점을 제공합니다.
트랜잭션 및 명령 식별자(xmin, xmax, cmin, cmax)는 32비트 값입니다. 매우 활동적이고 장기 실행되는 데이터베이스에서는 트랜잭션 ID가 결국 랩어라운드될 수 있습니다. PostgreSQL에는 이를 정상적으로 처리하는 메커니즘(VACUUM 및 트랜잭션 ID 고정 포함)이 있지만, 매우 긴 기간(수십억 건의 트랜잭션) 동안 XID의 절대적인 고유성에 의존하는 것은 현명하지 않다는 점을 강조합니다.
섹션 7: 기존 테이블 수정
데이터베이스 스키마는 거의 정적이지 않습니다. 요구 사항이 진화함에 따라 기존 테이블 구조를 변경해야 합니다. PostgreSQL은 이를 위해 다목적 ALTER TABLE 명령을 제공하여 열 추가에서 테이블 이름 변경에 이르기까지 다양한 수정을 허용합니다.
다양한 ALTER TABLE 하위 명령이 성능과 동시성에 매우 다른 영향을 미칠 수 있다는 점을 인지하는 것이 중요합니다. 일부 작업은 빠르고 메타데이터만 변경하는 반면, 다른 작업은 전체 테이블을 스캔하거나 다시 작성해야 할 수 있으며, 상당한 시간과 리소스를 소비하고 다른 작업을 차단하는 배타적 잠금이 필요할 수 있습니다.
7.1 열 추가 (ADD COLUMN)
기존 테이블에 새 열을 추가하려면 ADD COLUMN 절을 사용합니다. ALTER TABLE table_name ADD COLUMN column_name data_type [column_constraint...];
예시:
-- 'description' 열 추가 (기본값 NULL)
ALTER TABLE products ADD COLUMN description text;
-- 'active' 열 추가 (기본값 TRUE)
ALTER TABLE links ADD COLUMN active boolean DEFAULT TRUE;
-- 열이 존재하지 않을 경우에만 'status' 열 추가
ALTER TABLE orders ADD COLUMN IF NOT EXISTS status varchar(20) DEFAULT 'pending';
첫 번째 예시는 description 열을 추가합니다 . 두 번째 예시는 active 열을 boolean 타입으로 추가하고 기본값을 TRUE로 설정합니다 . 세 번째 예시는 status 열이 아직 없는 경우에만 추가합니다 .
CHECK 또는 DEFAULT와 같은 제약 조건을 열 정의의 일부로 지정할 수 있습니다. 중요한 성능 고려 사항은 DEFAULT 절과 관련이 있습니다. PostgreSQL 11부터 상수(비휘발성) 기본값(예: DEFAULT 0 또는 DEFAULT 'pending')으로 열을 추가하는 것은 매우 빠릅니다. 데이터베이스는 기본값을 테이블의 메타데이터에 저장하고 행을 읽거나 결국 다시 작성할 때 지연 적용하여 모든 기존 행을 즉시 비용이 많이 드는 업데이트를 피합니다. 그러나 기본값이 휘발성(예: DEFAULT clock_timestamp() 또는 DEFAULT random())인 경우 PostgreSQL은 ALTER TABLE 명령이 실행될 때 모든 기존 행에 대해 함수를 평가해야 하므로 전체 테이블 재작성이 필요하며, 이는 큰 테이블에서 매우 느릴 수 있습니다. DEFAULT가 지정되지 않은 경우 기존 행은 새 열에 NULL을 갖게 됩니다. IF NOT EXISTS 옵션은 동일한 이름의 열이 이미 존재하는 경우 오류를 방지하는 데 사용할 수 있습니다.
7.2 열 제거 (DROP COLUMN)
기존 열을 제거하려면 DROP COLUMN 절을 사용합니다. ALTER TABLE table_name DROP COLUMN column_name;
예시:
-- 'description' 열 제거 (종속 객체 없으면 성공)
ALTER TABLE products DROP COLUMN description;
-- 'active' 열 제거 (종속 객체 있어도 강제 제거)
ALTER TABLE links DROP COLUMN active CASCADE;
-- 열이 존재하는 경우에만 'temp_notes' 열 제거
ALTER TABLE tasks DROP COLUMN IF EXISTS temp_notes;
첫 번째 예시는 description 열을 제거합니다 . 두 번째 예시는 active 열과 이에 종속된 모든 객체(예: 뷰)를 함께 제거합니다 . 세 번째 예시는 temp_notes 열이 있을 경우에만 제거합니다 .
삭제된 열과 관련된 모든 인덱스 또는 테이블 제약 조건도 자동으로 제거됩니다. RESTRICT 옵션(기본값)은 다른 데이터베이스 객체(예: 뷰 또는 다른 테이블의 외래 키 제약 조건)가 해당 열에 종속된 경우 열 삭제를 방지합니다. CASCADE 옵션은 종속 객체를 재귀적으로 자동으로 제거하여 삭제를 진행하도록 허용합니다. 이는 극도의 주의를 기울여 사용해야 합니다.
열 삭제는 일반적으로 빠른 메타데이터 수준 작업으로, 후속 SQL 명령에 대해 열을 보이지 않게 만듭니다. 그러나 이전 열 데이터가 차지했던 디스크 공간을 즉시 회수하지는 않습니다. 공간은 사용 가능한 것으로 표시되며 향후 UPDATE 또는 INSERT 작업에 의해 점진적으로 재사용되거나 VACUUM에 의해 회수됩니다. IF EXISTS 옵션은 열이 존재하지 않는 경우 오류를 방지합니다.
7.3 제약 조건 추가 (ADD CONSTRAINT)
CREATE TABLE에서 사용된 테이블 제약 조건 구문을 미러링하는 ADD CONSTRAINT 구문을 사용하여 기존 테이블에 새 제약 조건을 추가할 수 있습니다. ALTER TABLE table_name ADD CONSTRAINT constraint_name constraint_definition; . 이는 CHECK, UNIQUE, PRIMARY KEY, FOREIGN KEY, EXCLUDE 제약 조건에 적용됩니다.
NOT NULL 제약 조건(기술적으로 열 제약 조건)을 추가하려면 다른 구문을 사용합니다. ALTER TABLE table_name ALTER COLUMN column_name SET NOT NULL;
예시:
-- CHECK 제약 조건 추가
ALTER TABLE products ADD CONSTRAINT name_not_empty CHECK (name <> '');
-- UNIQUE 제약 조건 추가
ALTER TABLE products ADD CONSTRAINT unique_product_no UNIQUE (product_no);
-- FOREIGN KEY 제약 조건 추가 (초기 검증 없이)
ALTER TABLE orders ADD CONSTRAINT fk_customer FOREIGN KEY (customer_id) REFERENCES customers (customer_id) NOT VALID;
-- 나중에 검증 수행
ALTER TABLE orders VALIDATE CONSTRAINT fk_customer;
-- NOT NULL 제약 조건 추가
ALTER TABLE products ALTER COLUMN product_no SET NOT NULL;
첫 두 예시는 각각 CHECK 및 UNIQUE 제약 조건을 추가합니다 . 세 번째 예시는 FOREIGN KEY 제약 조건을 NOT VALID 옵션과 함께 추가하여 즉각적인 테이블 스캔을 피하고, 나중에 VALIDATE CONSTRAINT를 사용하여 검증합니다. 마지막 예시는 product_no 열에 NOT NULL 제약 조건을 추가합니다 .
일반적으로 제약 조건을 추가하려면 PostgreSQL이 전체 테이블을 스캔하여 모든 기존 행이 새 규칙을 만족하는지 확인해야 합니다. 이 스캔은 시간이 많이 걸릴 수 있으며 동시 쓰기를 차단하는 잠금이 필요할 수 있습니다. 이를 완화하기 위해 PostgreSQL은 CHECK 및 FOREIGN KEY 제약 조건에 대해 NOT VALID 옵션을 제공합니다. NOT VALID가 지정되면 초기 유효성 검사 스캔이 건너뛰어져 ALTER TABLE 명령이 훨씬 빠르고 덜 방해가 됩니다. 제약 조건은 모든 새 행 또는 업데이트된 행에 대해 즉시 적용되지만 기존 행은 확인되지 않습니다. 제약 조건은 나중에 ALTER TABLE table_name VALIDATE CONSTRAINT constraint_name; 명령을 사용하여 기존 데이터에 대해 유효성을 검사할 수 있으며, 이는 테이블 스캔을 수행하지만 종종 덜 엄격한 잠금으로 수행될 수 있습니다.
7.4 제약 조건 제거 (DROP CONSTRAINT)
기존 제약 조건을 제거하려면 해당 이름을 알아야 합니다. 생성 중에 제약 조건을 명시적으로 명명한 경우 해당 이름을 사용합니다. 그렇지 않은 경우 시스템에서 생성된 이름(예: mytable_col_check)이 있으며, 이는 psql의 \d tablename 명령과 같은 도구를 사용하여 찾을 수 있습니다. 구문은 다음과 같습니다. ALTER TABLE table_name DROP CONSTRAINT constraint_name;
NOT NULL 제약 조건(사용자가 액세스할 수 있는 이름이 없음)을 제거하려면 다음을 사용합니다. ALTER TABLE table_name ALTER COLUMN column_name DROP NOT NULL;
예시:
-- 이름으로 제약 조건 제거
ALTER TABLE products DROP CONSTRAINT unique_product_no;
-- 제약 조건이 존재하는 경우에만 제거
ALTER TABLE orders DROP CONSTRAINT IF EXISTS fk_customer;
-- NOT NULL 제약 조건 제거
ALTER TABLE products ALTER COLUMN product_no DROP NOT NULL;
첫 번째 예시는 unique_product_no라는 이름의 제약 조건을 제거합니다 . 두 번째 예시는 fk_customer 제약 조건이 있을 경우에만 제거합니다 . 마지막 예시는 product_no 열에서 NOT NULL 제약 조건을 제거합니다.
DROP COLUMN과 유사하게 RESTRICT(기본값)는 다른 객체가 제약 조건에 종속된 경우(예: 외래 키가 삭제되는 고유/기본 키 제약 조건에 종속됨) 삭제를 방지하는 반면, CASCADE는 종속 객체를 제거합니다. 제약 조건을 지원하기 위해 자동으로 생성된 모든 인덱스(예: UNIQUE 또는 PRIMARY KEY의 경우)도 삭제됩니다. PostgreSQL 9.4 이전에는 제약 조건 속성을 수정하려면 종종 제약 조건을 삭제하고 다시 추가해야 했습니다. 9.4부터 ALTER TABLE... ALTER CONSTRAINT가 존재하지만, 주요 용도는 외래 키 제약 조건의 지연 가능성을 변경하는 것입니다.
7.5 열의 기본값 변경 (ALTER COLUMN SET/DROP DEFAULT)
열과 연결된 기본값은 다음을 사용하여 변경하거나 제거할 수 있습니다. ALTER TABLE table_name ALTER COLUMN column_name SET DEFAULT expression; ALTER TABLE table_name ALTER COLUMN column_name DROP DEFAULT;
예시:
-- 'target' 열의 기본값을 '_blank'로 설정
ALTER TABLE links ALTER COLUMN target SET DEFAULT '_blank';
-- 'status' 열의 기본값 제거 (NULL로 설정됨)
ALTER TABLE orders ALTER COLUMN status DROP DEFAULT;
첫 번째 예시는 target 열의 기본값을 설정합니다 . 두 번째 예시는 status 열의 기본값을 제거합니다.
이는 빠른 메타데이터 전용 작업입니다. 열에 대한 값이 명시적으로 제공되지 않은 후속 INSERT 또는 UPDATE 명령에만 영향을 미칩니다. 기존 행에 이미 저장된 데이터를 변경하지 않습니다.
7.6 열의 데이터 유형 변경 (ALTER COLUMN TYPE)
기존 열의 데이터 유형 변경은 다음을 사용하여 가능합니다. ALTER TABLE table_name ALTER COLUMN column_name TYPE new_data_type;
예시:
-- 'description' 열의 타입을 VARCHAR(255)로 변경
ALTER TABLE products ALTER COLUMN description TYPE VARCHAR(255);
-- 'price' 열의 타입을 INTEGER로 변경 (USING 절 사용)
ALTER TABLE products ALTER COLUMN price TYPE INTEGER USING price::integer;
-- 'col_name' 열의 타입을 INTEGER로 변경 (빈 문자열을 NULL로 처리)
ALTER TABLE tbl_name ALTER COLUMN col_name TYPE integer USING (NULLIF(col_name, '')::integer);
첫 번째 예시는 단순 타입 변경입니다. 두 번째 예시는 USING 절을 사용하여 기존 numeric 값을 integer로 캐스팅합니다 . 세 번째 예시는 빈 문자열이 있는 varchar 열을 integer로 변경할 때 빈 문자열을 NULL로 변환한 후 캐스팅합니다.
이 작업은 복잡하고 잠재적으로 중단될 수 있습니다. 많은 경우 PostgreSQL은 기존 데이터를 새 유형으로 변환하기 위해 전체 테이블을 다시 작성해야 하며, 이는 매우 느리고 새 테이블 버전에 상당한 디스크 공간을 소비하며 ACCESS EXCLUSIVE 잠금이 필요하여 다른 모든 액세스를 차단할 수 있습니다.
선택적 USING 절은 중요합니다. PostgreSQL이 이전 값에서 새 열 값을 계산하는 방법을 지정합니다. 이전 데이터 유형과 새 데이터 유형 간에 암시적 또는 할당 캐스트를 사용할 수 없는 경우 필수입니다. expression은 이전 열 값(종종 암시적으로 캐스트됨, 예: USING old_col::new_type) 또는 테이블의 다른 열을 참조할 수 있습니다. 열과 관련된 인덱스 및 단순 제약 조건은 일반적으로 해당 정의를 다시 구문 분석하여 자동으로 변환됩니다. 데이터 분포가 변경될 수 있으므로 열 통계가 삭제되며, 이후 테이블에서 ANALYZE를 실행하는 것이 좋습니다.
7.7 열 이름 변경 (RENAME COLUMN)
기존 열의 이름을 변경하는 것은 간단한 메타데이터 변경입니다. ALTER TABLE table_name RENAME COLUMN column_name TO new_column_name;
예시:
-- 'title' 열의 이름을 'link_title'로 변경
ALTER TABLE links RENAME COLUMN title TO link_title;
이 작업은 빠르며 저장된 데이터에 영향을 미치지 않습니다.
7.8 테이블 이름 변경 (RENAME TO)
마찬가지로 전체 테이블의 이름을 변경하는 것은 메타데이터 전용 작업입니다. ALTER TABLE table_name RENAME TO new_table_name;
예시:
-- 'links' 테이블의 이름을 'urls'로 변경
ALTER TABLE links RENAME TO urls;
빠르며 테이블 데이터를 수정하지 않습니다 . 그러나 이 명령은 인덱스, 제약 조건 또는 테이블 열이 소유한 시퀀스와 같은 관련 객체의 이름을 자동으로 변경하지 않는다는 점에 유의하는 것이 중요합니다. 원하는 경우 해당 ALTER 명령(예: ALTER INDEX, ALTER SEQUENCE)을 사용하여 별도로 이름을 변경해야 합니다.
ALTER TABLE에서 사용할 수 있는 광범위한 작업과 CASCADE, IF EXISTS, NOT VALID, USING과 같은 제어 옵션을 결합하면 관리자와 개발자에게 스키마 진화를 위한 강력한 도구를 제공합니다. 그러나 이러한 작업 전반에 걸친 성능 영향 및 잠금 동작의 상당한 변화는 특히 프로덕션 환경에서 큰 테이블을 수정할 때 신중한 계획과 기본 메커니즘에 대한 이해를 필요로 합니다. 기본값이 있는 열 추가 최적화와 같이 PostgreSQL에서 ALTER TABLE 기능의 지속적인 진화는 시간이 지남에 따라 스키마 수정을 덜 방해하고 더 관리하기 쉽게 만들기 위한 지속적인 노력을 보여줍니다.
다음 표는 일반적인 ALTER TABLE 작업과 주요 특징을 요약합니다.
작업 | 구문 요약 | 주요 옵션 / 절 | 성능 / 잠금 고려 사항 |
ADD COLUMN | ALTER TABLE... ADD COLUMN name type [constraints...] | DEFAULT, IF NOT EXISTS | 상수 DEFAULT 사용 시 빠름 (메타데이터만). 휘발성 DEFAULT 사용 시 느림/재작성. 그 외에는 최소 잠금. ACCESS EXCLUSIVE 잠금 잠시 필요 . |
DROP COLUMN | ALTER TABLE... DROP COLUMN name | CASCADE, RESTRICT, IF EXISTS | 빠름 (열을 보이지 않게 표시), 그러나 공간 즉시 회수 안 됨. ACCESS EXCLUSIVE 잠금 필요 . CASCADE는 광범위한 영향을 미칠 수 있음. |
ADD CONSTRAINT | ALTER TABLE... ADD CONSTRAINT... | NOT VALID (CHECK, FK 용) | 데이터 유효성 검사 스캔으로 인해 느리거나 잠금이 많이 발생할 수 있음. NOT VALID는 스캔을 건너뛰고, 더 빠르며, 잠금이 적음 (SHARE UPDATE EXCLUSIVE), 나중에 VALIDATE 필요. FK는 양쪽 테이블에 SHARE ROW EXCLUSIVE 잠금 필요 . |
DROP CONSTRAINT | ALTER TABLE... DROP CONSTRAINT name | CASCADE, RESTRICT, IF EXISTS | 일반적으로 빠름 (메타데이터 변경). ACCESS EXCLUSIVE 잠금 필요 . |
ALTER COLUMN SET/DROP DEFAULT | ALTER TABLE... ALTER COLUMN name SET DEFAULT expr / DROP DEFAULT | - | 빠름 (메타데이터만). ACCESS SHARE UPDATE EXCLUSIVE 잠금 필요 . |
ALTER COLUMN TYPE | ALTER TABLE... ALTER COLUMN name TYPE new_type | USING (종종 필요) | 종종 느리고, 전체 테이블/인덱스 재작성 필요. ACCESS EXCLUSIVE 잠금 필요 . USING 절 중요. |
RENAME COLUMN | ALTER TABLE... RENAME COLUMN name TO new_name | - | 빠름 (메타데이터만). ACCESS EXCLUSIVE 잠금 필요 . |
RENAME TABLE | ALTER TABLE name RENAME TO new_name | IF EXISTS | 빠름 (메타데이터만). ACCESS EXCLUSIVE 잠금 필요 . 관련 객체 이름 변경 안 함. |
섹션 8: 결론
PostgreSQL은 데이터베이스 테이블 구조를 정의, 관리 및 발전시키기 위한 강력하고 기능이 풍부한 환경을 제공합니다. 행, 열, 데이터 유형의 기본 개념부터 생성된 열 및 제외 제약 조건과 같은 고급 기능에 이르기까지 이 시스템은 데이터 구성 및 무결성 적용을 위한 광범위한 기능을 제공합니다.
DEFAULT 값과 같은 열 속성 및 SERIAL과 표준 GENERATED AS IDENTITY 절 간의 미묘한 차이에 대한 철저한 이해는 특히 기본 키 생성에 있어 효과적인 스키마 설계에 중요합니다. CHECK, NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY 및 PostgreSQL 특정 EXCLUDE와 같은 다양한 제약 조건 배열은 복잡한 비즈니스 규칙 구현과 데이터베이스 내에서 직접 참조 무결성 유지를 가능하게 하여 안정성과 일관성을 향상시킵니다. 시스템 열의 존재는 기본 MVCC 메커니즘에 대한 통찰력을 제공하는 반면, 포괄적인 ALTER TABLE 명령은 생성 후 스키마 수정을 위한 유연성을 제공합니다.
PostgreSQL 테이블을 성공적으로 관리하려면 사용 가능한 구문에 대한 지식뿐만 아니라 다양한 작업의 의미에 대한 인식도 필요합니다. 제약 조건 추가 또는 데이터 유형 변경의 성능 영향, 다양한 ALTER TABLE 작업과 관련된 잠금 동작, CASCADE 및 NOT VALID와 같은 제어 옵션의 적절한 사용과 같은 요소는 특히 프로덕션 환경에서 중요한 고려 사항입니다. PostgreSQL의 강력한 기능을 신중하게 활용함으로써 개발자와 관리자는 효율적이고 안정적이며 변화하는 요구 사항에 적응할 수 있는 데이터베이스 스키마를 구축하고 유지 관리할 수 있습니다.
'Database' 카테고리의 다른 글
[Database] PostgreSQL - 권한, 스키마, 상속, 파티셔닝 (0) | 2025.04.23 |
---|---|
[Database] PostgreSQL - SQL Query (0) | 2025.04.18 |
[Database] PostgreSQL - Transaction (0) | 2024.07.18 |
[Database] PostgreSQL - Index (0) | 2024.07.18 |
[Database] PostgreSQL Commands - DCL (0) | 2024.07.18 |