SQL #4 AI 피드백 분석 시스템의 테이블 정규화 #
#2025-07-30
1. 문제 #
AI 피드백 분석 시스템의 테이블 정규화
시나리오
- 여러분은 AI 피드백 분석 시스템을 위한 데이터 모델링을 맡았습니다.
- 현재는 여러 실험 데이터를 한 테이블에 모아두었지만, 벡터 임베딩 처리, 학습데이터 전처리, RAG 문서 기반 검색 등을 고려해 정규화 설계가 필요합니다. [비정규 테이블 예시: Day 2 – 정규화와 제약조건_실습1_예제_ai_feedback_raw.csv]
실습 목표
- LLM Feedback 데이터 정규화 (3NF까지 고려)
- model, user, prompt-response, tags 분리
- tags 필드는:TEXT[ ] 배열로 유지한 구조 (빠른 전처리, FAISS 등 용이)
- feedback_tag라는 별도 테이블로 정규화 (통계, RAG 전처리 유리)
- AI 분석 목적의 전처리 성능 관점에서 두 방식 비교 설명
#
2. Objective #
- 비정규화된 AI 피드백 테이블을 정규화
- 사용자, 모델, 질문-응답(prompt-response), 태그(tags) 정보를 분리
- 태그를 배열 형태(TEXT[]), 또는 별도 정규 테이블(feedback_tag)로 관리하여 AI 분석 목적(임베딩, 전처리, RAG)에 적합한 구조로 테이블 설계
#
3. AI 피드백 테이블 확인 #
원본 데이터를 확인해보면 아래와같고
정규화할 대상과 정규화방법은 아래와 같다.
- model: 모델 정보 테이블로 분리 (models)
- user_id, user_name: 사용자 테이블로 분리 (users)
- prompt, response: 피드백 본문 테이블로 분리 (feedbacks)
- tags: 별도 테이블로 정규화( 배열 유지 방식 + 연결 테이블 (feedback_tags))
- created_at: feedbacks 테이블에 포함
#
4. 테이블 정규화 #
3NF까지 고려해서 정규화하기.
-- 1. 사용자 테이블
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR UNIQUE
);
-- 2. 모델 테이블
CREATE TABLE models (
id SERIAL PRIMARY KEY,
name VARCHAR UNIQUE
);
-- 3. 피드백 테이블
CREATE TABLE feedbacks (
id SERIAL PRIMARY KEY,
feedback_code VARCHAR UNIQUE, -- F001 등
user_id INTEGER REFERENCES users(id),
model_id INTEGER REFERENCES models(id),
prompt TEXT,
response TEXT,
rating NUMERIC(3,1),
tags TEXT[], -- PostgreSQL 배열 형태
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 정규화된 태그 테이블
CREATE TABLE feedback_tags (
feedback_id INTEGER REFERENCES feedbacks(id),
tag TEXT,
PRIMARY KEY (feedback_id, tag)
);
정규화 후 데이터 삽입하기
-- USERS
INSERT INTO users (name) VALUES ('이정열');
INSERT INTO users (name) VALUES ('김민정');
-- MODELS
INSERT INTO models (name) VALUES ('gpt-4');
INSERT INTO models (name) VALUES ('mistral-7b');
-- FEEDBACKS
INSERT INTO feedbacks (feedback_code, user_id, model_id, prompt, response, rating, tags)
VALUES ('F001', 1, 1, '너 오늘 기분 어때?', '나는 기분이 좋지 않아.', 3.5, '{감성,비판적}');
INSERT INTO feedbacks (feedback_code, user_id, model_id, prompt, response, rating, tags)
VALUES ('F002', 2, 2, '안녕, 오늘 날씨 어때?', '오늘은 맑고 따뜻할 거야.', 4.2, '{날씨,정보}');
INSERT INTO feedbacks (feedback_code, user_id, model_id, prompt, response, rating, tags)
VALUES ('F003', 1, 1, '코딩하는 법 알려줘', '파이썬으로는 print부터 시작해봐.', 4.7, '{코딩,학습,친절함}');
-- FEEDBACK_TAGS
INSERT INTO feedback_tags (feedback_id, tag)
VALUES ((SELECT id FROM feedbacks WHERE feedback_code = 'F001'), '감성');
INSERT INTO feedback_tags (feedback_id, tag)
VALUES ((SELECT id FROM feedbacks WHERE feedback_code = 'F001'), '비판적');
INSERT INTO feedback_tags (feedback_id, tag)
VALUES ((SELECT id FROM feedbacks WHERE feedback_code = 'F002'), '날씨');
INSERT INTO feedback_tags (feedback_id, tag)
VALUES ((SELECT id FROM feedbacks WHERE feedback_code = 'F002'), '정보');
INSERT INTO feedback_tags (feedback_id, tag)
VALUES ((SELECT id FROM feedbacks WHERE feedback_code = 'F003'), '코딩');
INSERT INTO feedback_tags (feedback_id, tag)
VALUES ((SELECT id FROM feedbacks WHERE feedback_code = 'F003'), '학습');
INSERT INTO feedback_tags (feedback_id, tag)
VALUES ((SELECT id FROM feedbacks WHERE feedback_code = 'F003'), '친절함');
#
5. ERD 작성 #
dbdiagram 사용 https://dbdiagram.io/
#스키마 구조
Schemas:
- public
└── users
- id (integer, primary key)
- name (varchar, unique)
└── models
- id (integer, primary key)
- name (varchar, unique)
└── feedbacks
- id (integer, primary key)
- feedback_code (varchar, unique)
- user_id (integer, foreign key → users.id)
- model_id (integer, foreign key → models.id)
- prompt (text)
- response (text)
- rating (numeric)
- tags (text[]) # PostgreSQL 배열
- created_at (timestamp)
└── feedback_tags
- feedback_id (integer, foreign key → feedbacks.id)
- tag (text)
- PRIMARY KEY (feedback_id, tag)
#ERD
dbdiagram으로 ERD 작성
Table users {
id integer [primary key, note: '사용자 고유 ID']
name varchar [unique, note: '사용자 이름']
}
Table models {
id integer [primary key, note: 'AI 모델 고유 ID']
name varchar [unique, note: '모델 이름 (ex: gpt-4)']
}
Table feedbacks {
id integer [primary key, note: '피드백 고유 ID']
feedback_code varchar [unique, note: '원본 코드 (ex: F001)']
user_id integer [ref: > users.id, note: '작성자 ID']
model_id integer [ref: > models.id, note: '사용된 모델 ID']
prompt text [note: '사용자 질문']
response text [note: '모델 응답']
rating numeric [note: '사용자 평가 점수']
tags text[] [note: '태그 배열 (예: {감성,비판적})']
created_at timestamp [note: '생성 시간']
}
Table feedback_tags {
feedback_id integer [ref: > feedbacks.id, note: '피드백 ID']
tag text [note: '정규화된 단일 태그']
Note: '태그별 통계, 검색에 유리'
Primary Key (feedback_id, tag)
}
#
6. AI 분석 목적의 전처리 성능 관점에서 두 방식 비교 설명 #
(TEXT[] 배열로 tags를 저장하는 방식 vs tags를 별도 테이블(feedback_tag)로 정규화하는 방식)
먼저 TEXT[] 배열로 tags를 저장하는 방식은 한 피드백에 대한 여러 태그 정보를 하나의 행에 함께 저장하는 구조인데 예를 들어 어떤 사용자 피드백이 “positive”, “concise”, “creative"라는 태그를 갖는다면, 이 세 단어를 배열로 묶어 하나의 셀에 저장합니다.
이 방식의 장점은 빠른 접근성과 효율적인 처리 속도입니다. LLM 기반 피드백 시스템에서는 종종 전체 텍스트나 임베딩을 이용한 벡터 검색(ex. pgvector)을 수행하는데 이때 태그 정보가 같은 행에 묶여 있으면 텍스트 단위 처리 또는 배치 임베딩에 용이하고 특히 모델 학습이나 벡터 임베딩 시 태그 정보를 문맥 정보로 같이 넘겨야 하는 경우 이 구조는 파이프라인 단순화에 큰 도움이 됩니다. 하지만 태그 단위로 집계하거나 통계 분석을 하고자 할 경우 배열 내부 요소를 하나하나 파싱하거나 unnest() 같은 SQL 함수로 분리해 처리해야 하는데 예를 들어 “어떤 태그가 가장 자주 사용되었는가?“라는 질문을 하려면 배열에서 모든 태그를 추출하고 세는 별도 과정이 필요합니다.
반대로 tags를 별도 테이블(feedback_tag)로 정규화하는 방식은 각 태그를 하나의 행으로 저장하고 피드백 ID와 연결하는데 이때 feedback_tag 테이블은 “feedback_id - tag” 형태로 구성되며, 각 피드백에 여러 태그가 있을 경우 그 수만큼의 행이 생성됩니다.
이 방식의 가장 큰 장점은 쿼리 처리에서의 유연성입니다. 앞서 언급한 “가장 많이 쓰인 태그"나 “특정 태그가 달린 피드백 목록"을 매우 쉽게 쿼리할 수 있습니다. 그러나 데이터가 다소 늘어난다는 단점도 있습니다. 예를 들어 100개의 피드백에 평균 4개의 태그가 달려 있다면, feedback_tag 테이블에는 400개의 레코드가 추가로 생기고 피드백을 조인하여 조회하는 경우 JOIN 연산의 비용이 듭니다. 이로 인해 벡터 임베딩이나 배치 학습 시에는 조인을 반복해야 하므로 배열 기반보다 느릴 수 있습니다.
결론적으로, 태그를 배열로 유지하는 방식은 벡터 기반 검색, 임베딩 처리, LLM 학습에 적합하고 시스템의 입력-출력 속도를 높이는 반면, 태그를 별도 테이블로 정규화하는 방식은 태그 관리 및 유지 보수에 유리합니다.