+ Clearly follows the requested cost and finding structure.
- Invents details and exceeds scope with a Redis redesign.
Finds what will break later from your DDL, leading with the decisions that are expensive to reverse.
| Category | Development › Data & databases |
|---|---|
| Tags | ReviewingAnalyzingDeveloper |
Review this database schema. Order findings by **how expensive they are to change after data exists**, not by severity: 1. Irreversible-ish — primary key type and shape, partitioning, denormalization, column that should have been a table 2. Costly — nullability, uniqueness across existing rows, type widening or narrowing, timezone handling 3. Cheap — index changes, names, defaults, comments For each finding: what breaks, under what query or volume, and the change. Check specifically: - Does every foreign key have an index on the child side? Missing ones make parent deletes and joins collapse at scale. - Soft delete combined with a full-table unique constraint — that permanently locks the deleted value. - Columns storing more than one fact, and facts split across columns that should be rows. - Money as float. Timestamps without timezone. Enum as a database type where values will change. - Counters updated on the same row that carries indexed columns. - What the access patterns need that no index covers. Rules: - *Separate structural problems from preferences* and label them. A naming opinion is not a finding. - Where the access patterns do not tell you enough to judge an index, say what query you would need to see. - Do not propose a redesign. Name the smallest change per finding.
Schemas freeze once data accumulates. This surfaces "one line now, a migration later" items first and separates structural problems from matters of taste.
ChatGPT is cautious but has ordering and duplication issues. Gemini is best structured but includes speculation; [C] is missing.
+ Clearly follows the requested cost and finding structure.
- Invents details and exceeds scope with a Redis redesign.
+ Carefully avoids assumptions and proposes minimal changes.
- Misclassifies timezone cost and duplicates index findings.
| Criterion | ChatGPT | Gemini | Leader |
|---|---|---|---|
| Instruction following | 7 | 8 | Gemini +14% |
| Accuracy | 8 | 7 | ChatGPT +14% |
| Specificity | 9 | 9 | Tie |
| Structure | 7 | 9 | Gemini +29% |
| Right length | 7 | 7 | Tie |
Scored 1–10 by gpt-5.6-sol with model names hidden (2026-09-25). This is an AI review, not a measurement.
We gave three models the same input and copied their answers unedited. Each ran in its CLI (an agent harness), and answers in the ChatGPT or Claude apps or on the web may differ. Outputs are in Korean.
DDL: CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, phone VARCHAR(20), is_deleted BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT now() ); CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), total_price FLOAT NOT NULL, status VARCHAR(20), created_at TIMESTAMP DEFAULT now() ); CREATE TABLE order_items ( id SERIAL PRIMARY KEY, order_id INTEGER REFERENCES orders(id), product_name VARCHAR(255), option_info VARCHAR(500), price FLOAT ); CREATE TABLE product_view_count ( product_id INTEGER PRIMARY KEY, view_count INTEGER DEFAULT 0, updated_at TIMESTAMP ); Access patterns: 1) 이메일로 로그인 조회 2) 특정 유저의 최근 주문 목록 조회(user_id, created_at DESC) 3) 상품 상세 진입 시 view_count +1 UPDATE (피크 초당 50건) 4) 주문의 옵션별 매출 집계 Expected scale: 가입자 30만 명, 일 주문 약 5,000건, product_view_count UPDATE가 전체 쓰기 트래픽 중 가장 큼
option_info가 여러 사실을 한 컬럼에 저장옵션이 JSON·문자열로 들어가면 옵션별 매출 집계 시 파싱이 필요하고, 옵션명·옵션값별 필터/그룹화가 인덱스를 효율적으로 사용할 수 없습니다. 주문량과 주문 항목이 늘수록 전체 스캔과 문자열 파싱 비용이 커집니다.
옵션별 매출 집계option_info의 실제 집계 기준이 되는 단일 값만 별도 컬럼으로 분리하거나, 이미 정해진 옵션 식별자를 저장하는 컬럼을 추가product_name은 주문 당시 상품명 스냅샷이라면 의도적인 비정규화일 수 있으므로 문제로 단정하지 않습니다.
SERIAL 기반 정수 PK/FK현재 예상 규모에서는 즉시 문제가 되지 않지만, SERIAL은 32비트 정수라 약 21억 개에서 고갈됩니다. 특히 모든 FK가 같은 INTEGER 타입을 사용하므로 나중에 BIGINT로 바꾸면 전체 PK/FK와 참조 코드의 동시 변경이 필요합니다.
BIGINT PK/FK 사용created_at, updated_at이 TIMESTAMP라 서버 세션 시간대에 따라 해석이 달라질 수 있습니다. 여러 지역의 사용자, 배치 서버, 리포팅 서버가 섞이면 “최근 주문” 경계나 일별 매출 집계가 어긋납니다.
TIMESTAMP WITH TIME ZONE으로 통일하고 UTC 기준으로 저장users.email UNIQUE는 삭제된 사용자까지 포함합니다. 삭제된 계정의 이메일로 재가입하거나 복구하려 하면 계속 실패합니다.
is_deleted = true인 사용자의 이메일 재사용WHERE is_deleted = false 조건의 부분 unique index로 교체email = ? AND is_deleted = false를 사용해야 합니다.현재 is_deleted 자체에는 기본값만 있고 NOT NULL이 없어 NULL이 들어갈 수 있습니다.
is_deleted BOOLEAN NOT NULL DEFAULT falseFLOAT 사용부동소수점 오차로 합계가 정확히 떨어지지 않을 수 있습니다. 특히 option_info별 매출 집계에서 여러 행을 합산하면 오차가 누적됩니다.
NUMERIC(정밀도, scale)로 변경하거나 최소 단위 정수(예: 원 단위)로 변경total_price와 order_items.price의 표현을 동일하게 맞춰야 합니다.현재 두 FK 모두 자식 쪽 인덱스가 없습니다.
orders.user_id REFERENCES users(id)order_items.order_id REFERENCES orders(id)부모 삭제·갱신 시 자식 존재 여부를 확인하는 스캔이 발생할 수 있고, 조인도 대량 데이터에서 느려집니다.
orders(user_id) 인덱스order_items(order_id) 인덱스특정 유저의 최근 주문 조회에는 단순 인덱스보다 다음 복합 인덱스가 필요합니다.
CREATE INDEX ON orders (user_id, created_at DESC);
현재 제시된 패턴만으로는 삭제된 주문 제외, 상태 필터, 페이지네이션 여부를 알 수 없습니다. 실제 쿼리에 status 또는 is_deleted 조건이 있다면 그 조건까지 포함한 인덱스가 필요한지 쿼리를 봐야 판단할 수 있습니다.
status가 변경 가능한 값인데 데이터베이스 enum을 사용하지 않음현재는 VARCHAR(20)이므로 enum 타입 자체의 변경 비용 문제는 없습니다. 오히려 상태 값이 자주 바뀐다면 현재 방식이 변경에는 유리합니다.
다만 유효하지 않은 상태 문자열을 막아야 한다면 최소 변경은 CHECK 제약입니다. 상태 목록과 변경 빈도를 모르면 별도 테이블이나 enum으로 바꿀 근거는 부족합니다.
요구 쿼리가 다음 형태라면 현재 인덱스로는 user_id 기준 필터와 created_at DESC 정렬을 모두 지원하지 못합니다.
SELECT ...
FROM orders
WHERE user_id = ?
ORDER BY created_at DESC;
최소 변경:
CREATE INDEX ON orders (user_id, created_at DESC);
email VARCHAR(255) UNIQUE NOT NULL이므로 이메일 조회용 unique index는 이미 생성됩니다. 별도 인덱스는 필요하지 않습니다.
단, soft delete를 반영해 부분 unique index로 바꾸면 그 인덱스가 로그인 조회도 담당하게 해야 합니다.
product_view_count는 product_id가 PK이므로 조회수 증가 때 인덱스가 있는 동일 행을 갱신합니다. 같은 상품에 초당 50건이 집중되면 해당 행의 row lock 경합과 WAL/페이지 갱신이 병목이 될 수 있습니다.
UPDATE product_view_count
SET view_count = view_count + 1,
updated_at = now()
WHERE product_id = ?;
이 방식으로도 경합이 확인되면 카운터를 여러 행으로 분산하거나 비동기 집계해야 하지만, 이는 실제 hot product의 락 대기·TPS 측정 후 결정할 사안입니다.
created_at DEFAULT now(), view_count DEFAULT 0 등은 기능상 큰 문제는 없습니다. 컬럼명이나 제약 이름을 바꾸는 것은 데이터가 존재한 뒤에도 비교적 저렴하므로, 일관성이 목적이 아니라면 finding으로 보지 않습니다.
| Advanced Text Converter for Large Datasets | |
| AI-powered data extraction and organization tool | |
| AI2sql SQL Model — Query Generator | |
| Backend Architect | |
| base-R |