PostgreSQL 규칙
적용 방법
두 가지 모드가 있습니다. 무언가를 작성하기 전에 어떤 모드를 적용할지 결정하십시오.
모드 A — 사용자가 이름을 선택합니다. 사용자가 스키마, 엔티티 또는 마이그레이션을 요청했지만 이름을 지정하지 않았습니다. 아래 규칙을 직접 적용하십시오.
모드 B — 이름이 이미 존재하거나 지정되었습니다. 사용자가 이름을 지정했거나, 기존 코드, 마이그레이션 또는 데이터베이스에 이러한 규칙에서 벗어난 이름이 포함되어 있습니다. 자동으로 이름을 변경하지 말고 보고하고 질문하십시오.
변경하기 전에 사용자 언어로 된 보고 형식:
Naming:
Must fix
- `"UserProfile"` (table) → `user_profiles` — quoted PascalCase는 모든 쿼리에서 따옴표로 묶어야 함
- `created` timestamp DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC') → `created_at timestamptz DEFAULT now()` — 세션 TimeZone이 UTC가 아닐 때 값이 변경됨
Recommended
- `users.user_email` → `users.email` — 테이블 이름이 이미 컨텍스트를 제공함
그런 다음 한 가지 질문을 합니다: 모두 적용, "Must fix"만 적용, 또는 현재 상태 유지. 답변을 기다리십시오.
보고 규칙:
- Must fix = 버그를 유발하거나 쿼리 또는 도구를 손상시킵니다: 따옴표로 묶인 혼합 대소문자 식별자, 예약어, 63바이트를 초과하는 이름, 시간대가 없는
timestamp, 잘못된 시간 기본값,jsonb/numeric이 필요한 데이터에json/float/money, FK 인덱스 누락, 트랜잭션 외부의 잠금. - Recommended = 스타일 및 일관성.
- 현재 작업에서 다루는 객체만 보고합니다. 다른 곳에서 다른 편차를 발견한 경우, 한 줄을 추가하십시오: "<장소>에 유사한 편차 N개 더 있음 — 목록을 표시할까요?"
- 적용된 마이그레이션 또는 라이브 데이터베이스의 객체: 이름 변경은 호환성을 깨는 변경입니다. 제안만 하고; 승인되면 새 마이그레이션으로 구현하십시오. 적용된 마이그레이션을 편집하지 마십시오.
- 코드베이스가 일관되게 다른 규칙을 사용하는 경우, 해당 규칙이 우선합니다. 차이점을 한 번 언급한 다음, 각 발생을 플래그 지정하지 않고 프로젝트 규칙을 따르십시오.
- ORM 명명 규칙(예: EFCore.NamingConventions)으로 생성된 이름은 그대로 허용됩니다.
- 사용자가 규칙을 따르지 않는 이름을 확인하면, 해당 이름을 사용하고 이 세션에서 다시 언급하지 마십시오.
일반 규칙
snake_case, 소문자, 따옴표로 묶이지 않은 식별자.- 식별자당 최대 63바이트; 더 긴 이름은 자동으로 잘립니다. 자르지 말고 의미 있게 줄이십시오.
- 테이블 복수형:
users,orders,order_items. - 컬럼 단수형, 테이블 접두사 없음:
users.email,users.user_email아님. - 일반적인 약어를 제외한 전체 단어:
id,url,ip,sku. - 예약어 또는 모호한 단어를 이름으로 사용하지 마십시오:
user,order,group,table,limit,offset,desc,end,date,time,timestamp. - 형식 또는 객체 접두사 없음:
tbl_users,str_name사용 금지. - 부울은 긍정적으로:
is_deleted,is_not_deleted사용 금지.
표준 컬럼
| 용도 | 이름 | 형식 |
|---|---|---|
| 기본 키 | id | bigint generated always as identity 또는 uuid v7 |
| 생성 / 수정 | created_at, updated_at | timestamptz not null default now() |
| 소프트 삭제 | deleted_at | timestamptz null |
| 감사 | created_by, updated_by | FK to users.id |
| 플래그 | is_active, is_verified, has_children, can_edit | boolean not null default false |
| 상태 | status | text + check, enum, 또는 참조 테이블 |
| 날짜 전용 | birth_date, due_on | date |
| 유효성 | expires_at, valid_from, valid_to | timestamptz |
| 금액 | amount, unit_price, total_amount + currency_code | numeric(19,4) + char(3), 또는 amount_minor bigint |
| 카운터 | items_count, retry_count | integer / bigint |
| 정렬 | sort_order | integer |
| 비즈니스 키 | code, slug, external_id | text + unique |
| 테넌트 | tenant_id | FK |
| 동시성 | version 또는 시스템 xmin | integer / xid |
| 자유 형식 데이터 | metadata, payload, settings | jsonb |
접미사는 의미를 전달합니다: _at 순간, _on/_date 달력 날짜, _id 참조, _count 카운터, _url, _path, _hash.
키 및 제약 조건
| 객체 | 패턴 | 예제 |
|---|---|---|
| 기본 키 | pk_<table> | pk_orders |
| 외래 키 | fk_<table>_<ref_table>_<column> | fk_orders_customers_customer_id |
| 고유 제약 조건 | uq_<table>_<cols> | uq_users_email |
| 대체 키 (EF Core) | ak_<table>_<cols> | ak_products_sku |
| 검사 | ck_<table>_<rule> | ck_orders_amount_positive |
| 배타 | ex_<table>_<cols> | ex_bookings_room_id_period |
| 인덱스 | ix_<table>_<cols> | ix_orders_customer_id_created_at |
| 고유 인덱스 | ux_<table>_<cols> | ux_users_email |
| 부분 인덱스 | ix_<table>_<cols>_<condition> | ix_users_email_not_deleted |
| 메서드별 인덱스 | ix_<table>_<col>_<method> | ix_products_tags_gin |
| 트리거 | trg_<table>_<action> | trg_orders_set_updated_at |
| 트리거 함수 | 동사 + 객체 | set_updated_at() |
| 시퀀스 | PostgreSQL 기본값 | orders_id_seq |
- FK 컬럼:
<참조 테이블, 단수형>_id, 또는 역할이 중요할 때 역할 이름:author_id,sender_id,recipient_id,parent_id. - FK 패턴에는 컬럼이 포함되므로, 하나의 테이블에 대한 여러 FK는 고유하게 유지됩니다:
fk_messages_users_sender_id,fk_messages_users_recipient_id. - PostgreSQL은 FK 컬럼을 인덱싱하지 않습니다. 모든 FK는 복합 인덱스가 이미 FK로 시작하지 않는 한 인덱스가 필요합니다. EF Core는 기본적으로 이를 생성합니다. Drizzle, Prisma, TypeORM, Knex 및 sqlc는 그렇지 않습니다.
관계
| 종류 | 형태 | 예제 |
|---|---|---|
| 일대다 | 자식의 FK | order_items.order_id → orders.id |
| 일대일 | FK + unique, 또는 PK = FK | user_profiles.user_id |
| 다대다 | 접합 <a>_<b>, 복합 PK | user_roles (user_id, role_id), pk_user_roles |
| 속성이 있는 다대다 | 의미 있는 엔티티 이름 | memberships, enrollments |
| 자기 참조 | parent_id 또는 역할 | categories.parent_id, employees.manager_id |
다형성 쌍(commentable_type + commentable_id)은 FK를 가질 수 없습니다. 정확히 하나가 설정되었는지 확인하는 검사가 있는 별도의 nullable FK 컬럼 또는 별도의 테이블을 선호하십시오.
스키마, 뷰, 함수, 타입
| 객체 | 패턴 | 예제 |
|---|---|---|
| 스키마 | 도메인 또는 모듈 | auth, billing, catalog, audit |
| 뷰 | v_<name> | v_active_users |
| 구체화된 뷰 | mv_<name> | mv_daily_sales |
| 함수 / 프로시저 | 동사 + 객체 | get_user_balance, calculate_order_total |
| 열거형 타입 | <entity>_<attribute> | order_status |
| 열거형 값 | 소문자 snake_case | pending, paid, refunded |
| 데이터베이스 역할 | <app>_<access> | shop_app_rw, shop_app_ro, shop_migrator |
열거형 값은 타입을 다시 생성하지 않고는 삭제할 수 없습니다. 변경되는 집합의 경우 text + 검사 또는 참조 테이블을 선호하십시오.
타입 및 기본값
- 순간을 위한
timestamptz, 기본값now().timestamptz컬럼의CURRENT_TIMESTAMP AT TIME ZONE 'UTC'는 버그입니다. - 길이가 비즈니스 규칙이 아닌 이상
varchar(n)대신text; 이를 위해check (char_length(col) <= n)을 선호하십시오. jsonb, 절대json사용 금지.- 금액은
numeric; 절대real,double precision또는money사용 금지. - 새 identity 컬럼은
serial/bigserial이 아닌generated always as identity를 사용합니다. - UUID 기본 키는 v7 사용: PostgreSQL 18
uuidv7(), .NET 9Guid.CreateVersion7(), Nodeuuid패키지v7(), Gogithub.com/google/uuiduuid.NewV7(). 무작위 v4 키는 B-트리 인덱스를 조각화합니다. gen_random_uuid()는 PostgreSQL 13부터 내장되어 있습니다. 이를 위해uuid-ossp및pgcrypto는 필요하지 않습니다.- 대소문자 구분 없는 고유성을 위해
citext또는lower(email)고유 인덱스를 사용합니다.
동시성 및 쿼리 패턴
- 낙관적 동시성, EF Core + Npgsql:
IsRowVersion()로 구성된uint속성은xmin에 매핑됩니다. - 낙관적 동시성, Node 및 Go:
version integer not null default 0,where id = $1 and version = $2및version = version + 1로 업데이트됨; 영향을 받은 행이 0이면 충돌을 의미합니다. sqlc에서는:execrows를 사용하여 영향을 받은 행 수를 가져옵니다. select ... for update및for update skip locked은 명시적 트랜잭션 내에서만; 자동 커밋에서는 잠금이 즉시 해제됩니다.- 어드바이저리 잠금: 트랜잭션 내
pg_try_advisory_xact_lock. 세션 수준pg_advisory_lock은 잠금 해제될 때까지 열려 있는 전용 연결에서만; 풀링을 사용하면 잠금 및 잠금 해제가 다른 연결에 할당될 수 있습니다. - 크거나 무한한 목록은 키셋 페이지네이션을 사용합니다:
where (created_at, id) < ($1, $2) order by created_at desc, id desc limit $3.offset은 작고 제한된 목록에만 사용합니다. - 미래 테이블에 대한 권한 부여는
grant ... on all tables뿐만 아니라alter default privileges가 필요합니다.
스네이크 케이스를 위한 ORM 설정
| 스택 | 설정 |
|---|---|
| EF Core + Npgsql | EFCore.NamingConventions, UseSnakeCaseNamingConvention() |
| Drizzle | casing: 'snake_case' |
| Prisma | 필드에 @map, 모델에 @@map |
| TypeORM | typeorm-naming-strategies의 SnakeNamingStrategy |
| Knex / Objection | knexSnakeCaseMappers() |
| Go + sqlc | 없음; Go 이름은 snake_case에서 자동으로 생성됨 |
프로젝트가 스네이크 케이스로 구성되지 않은 ORM을 사용하는 경우, 첫 번째 마이그레이션 전에 Must fix로 보고하십시오. 마이그레이션이 존재한 후에는 마이그레이션 비용과 함께 Recommended로 보고하십시오.
Go + sqlc
sqlc는 SQL 우선입니다: 스키마는 마이그레이션 파일에서 비롯되므로 위의 모든 규칙이 직접 적용됩니다.
sqlc.yaml의schema를 마이그레이션 디렉토리로 지정하십시오 (goose, golang-migrate, dbmate 및 atlas 형식이 지원됨; down 마이그레이션은 무시됨). 마이그레이션 옆에 별도의 수동 작성 스키마 파일을 유지하지 마십시오.- 복수 snake_case 테이블은 단수 Go 구조체를 제공합니다:
order_items→OrderItem. Enumorder_status→ 타입OrderStatus와 상수OrderStatusPending. sql_package: pgx/v5를 사용하십시오.uuid→github.com/google/uuid.UUID및 원하는 경우timestamptz→time.Time을pgtype래퍼 대신 재정의하십시오.- 컬럼 이름을 바꾸지 않고
rename(url: URL,ip: IP)으로 두문자어를 수정하십시오. - 쿼리 이름은 PascalCase 동사 + 객체이며, 결과 종류를 포함합니다:
| 용도 | 예제 |
|---|---|
| 한 행 | -- name: GetUserByEmail :one |
| 여러 행 | -- name: ListOrdersByCustomer :many |
| 삽입 | -- name: CreateOrder :one + returning * |
| 업데이트 | -- name: UpdateOrderStatus :execrows |
| 삭제 | -- name: DeleteSession :exec |
| 대량 로드 | -- name: CreateOrderItems :copyfrom |
- 매개변수 이름은
@name또는sqlc.arg(name)으로 지정하고, nullable 매개변수는sqlc.narg(name)으로 지정하십시오. 위치 기반$1은Column1스타일의 Go 필드를 생성합니다. - 조인의 경우 임시 별칭으로 컬럼을 선택하는 대신
sqlc.embed(table)를 사용하십시오. - 쓰기에서
returning *을 제외하고 스키마 변경에도 살아남아야 하는 쿼리에서는select *를 사용하지 마십시오.