DBMS 및 SQL 활용 #3 집계함수, 고급 객체기능, 고급 인덱스

DBMS 및 SQL 활용 #3 집계함수, 고급 객체기능, 고급 인덱스 #

#2025-08-28


1. GROUPBY #

GROUP BY

  • 테이블 안에 있는 데이터를 특정 기준으로 묶어서 요약.

  • 테이블 embedding_store에서

    image
    • id, user_id, cluster_id, similarity, tag 5개 컬럼이 있는데
      • 있는 그대로보면 큰 그림을 보기 힘들다 즉 해석이 어렵다.
      • GROUP BY를 쓰면 요약 정보를 만들수있는데
        • user_id로 묶으면 “사용자 A는 총 10건, 사용자 B는 총 5건” 같은 식으로 정리 / cluster_id로 묶으면 “클러스터 1은 평균 유사도가 0.8, 클러스터 2는 0.5” / tag로 묶으면 “계약 태그는 100건, 고객상담 태그는 30건” 같은 결과가 나오고 이렇게 하면 데이터의 전체 분포와 패턴을 이해할 수 있다.

AI 연계?

  • 벡터 데이터에서 클러스터링을 하고 나면 각 클러스터의 특징을 봐야되는데

    image
    • SQL로 GROUP BY cluster_id를 해서 평균 유사도, 최소 유사도 등을 구해서 평균 유사도가 지나치게 낮은 클러스터가 발견되면 “이 클러스터는 불분명하게 묶였네” 이런식으로 클러스터를 판단할수있다
    • SQL로 GROUP BY tag 해서 클러스터내 사용자별 태그 분포를 보면 어떤 사용자가 어떤 패턴을 많이 보이는지를 확인할수있다.
  • 이런식으로 단순 SQL 집계가 단순 통계가 아니라 이상치 탐색, 품질 저하 감지, 태그 자동 분류 같은 AI 전처리 과정에 활용 가능.

#

Vector DB 분석에서 GROUP BY 활용

  • 벡터 데이터는?
    • 문장, 이미지 같은 걸 임베딩해서 저장해둔 값
  • 클러스터링을 하고 나면 각 클러스터가 잘 묶였는지를 확인해야 하고 이때 GROUP BY cluster_id로 묶어서 평균 유사도를 보면 클러스터를 판단할수있다
-- 클러스터별 평균 유사도와 최소 유사도
SELECT cluster_id,
       ROUND(AVG(similarity), 3) AS avg_sim,
       MIN(similarity) AS min_sim
FROM embedding_store
GROUP BY cluster_id;
  • 여기서 평균 유사도가 0.9 이상이면 잘 뭉쳐진 클러스터일 가능성이 크고 0.5 이하라면 내부 데이터가 제각각이라 불분명하게 묶인 클러스터라고 판단가능
  • 이렇게 SQL 집계로 클러스터 품질을 확인할수있다.

#

AI 결과 검증에서 GROUP BY 활용

  • AI 모델이 분류 작업을 했을때
    • 실제 라벨(true_label)과 예측 결과(pred_label)가 테이블에 있고 카테고리별 정확도를 구할 수 있다
-- 카테고리별 정확도 집계
SELECT true_label,
       ROUND(SUM(CASE WHEN true_label = pred_label THEN 1 ELSE 0 END)::decimal
             / COUNT(*), 3) AS accuracy
FROM prediction_results
GROUP BY true_label;
  • 이렇게 하면 “카테고리 A의 정확도는 0.95, 카테고리 B는 0.62” 같은 결과가 나오니까 어떤 클래스에서 모델이 잘 못 맞추는지 바로 확인할 수 있고 이는 모델 개선 포인트로 이어진다.

#

추천 시스템에서 GROUP BY 활용

  • 추천 시스템에서는 사용자가 어떤 아이템을 자주 고르는지, 또는 어떤 유형의 아이템을 선호하는지를 분석해야 하는데
    • 사용자별 선택 기록을 GROUP BY user_id나 GROUP BY item_category로 묶으면 개인의 선호를 확인 가능하다
-- 사용자별 아이템 선택 패턴
SELECT user_id, item_category, COUNT(*) AS choice_count
FROM user_choice_log
GROUP BY user_id, item_category
ORDER BY user_id, choice_count DESC;
  • 이렇게 하면 “사용자 A는 주로 액션 영화를 많이 선택, 사용자 B는 로맨스 위주” 같은 패턴이 보이고 이를 활용해서 토대로 개인화 추천을 강화할 수 있다.

#

분류 성능 비교에서 GROUP BY 활용

  • 분류 모델이 여러 개 있다면 카테고리별로 각 모델의 성능을 나란히 비교할수있다.
-- 모델별, 카테고리별 정확도 비교
SELECT model_name, true_label,
       ROUND(SUM(CASE WHEN true_label = pred_label THEN 1 ELSE 0 END)::decimal
             / COUNT(*), 3) AS accuracy
FROM prediction_results
GROUP BY model_name, true_label;
  • 이렇게 하면 “모델 A는 카테고리 X에서는 정확도가 높지만, 카테고리 Y에서는 낮다” 같은 판단(비교) 가능.

#

2. ROLLUP & CUBE #

-- 원본데이터
region | product | amount
-------+---------+-------
East   | A       | 100
East   | B       | 150
West   | A       | 200
West   | B       |  50

sales_summary 테이블

  • 지역(region), 제품(product), 매출액(amount)
  • East 지역의 A 제품 매출 100, B 제품 매출 150 / West 지역의 A 제품 200, B 제품 50
  • 일반적인 GROUP BY region, product를 쓰면?
-- 기본 GROUP BY
SELECT region, product, SUM(amount) AS total
FROM sales_summary
GROUP BY region, product;
region | product | total
-------+---------+------
East   | A       | 100
East   | B       | 150
West   | A       | 200
West   | B       |  50
  • SUM()으로 합계를 계산했고 그대로 네 줄이 다시 나오면서 매출액이 합계로 정리된다
    • 그런데 이렇게 하면 지역별 합계나 전체 합계를 따로 보려면 다시 쿼리를 작성해야함.

#

  • GROUP BY ROLLUP(region, product)를 쓰면?
-- GROUP BY ROLLUP
SELECT region, product, SUM(amount) AS total
FROM sales_summary
GROUP BY ROLLUP(region, product);
region | product | total
-------+---------+------
East   | A       | 100
East   | B       | 150
East   | NULL    | 250   <- East 소계
West   | A       | 200
West   | B       |  50
West   | NULL    | 250   <- West 소계
NULL   | NULL    | 500   <- 전체 합계
  • 네 줄의 상세 데이터에 더해서 지역별 소계와 전체 합계까지 자동으로 붙는다.
    • East 소계: East 지역은 A 100, B 150을 합쳐 250
    • West 소계: West는 A 200, B 50을 합쳐 250
    • 전체 합계: 500
      • 소계를 표시할 때는 product 칸이 NULL로 나타나고 전체 합계는 region과 product가 모두 NULL로 표시.

#

  • GROUP BY CUBE(region, product)를 쓰면?
-- GROUP BY CUBE
SELECT region, product, SUM(amount) AS total
FROM sales_summary
GROUP BY CUBE(region, product);
region | product | total
-------+---------+------
East   | A       | 100
East   | B       | 150
East   | NULL    | 250   <- East 소계
West   | A       | 200
West   | B       |  50
West   | NULL    | 250   <- West 소계
NULL   | A       | 300   <- 제품 A 전체 합계
NULL   | B       | 200   <- 제품 B 전체 합계
NULL   | NULL    | 500   <- 전체 합계
  • 지역별 합계와 전체 합계뿐 아니라 제품별 합계도 같이 나온다.
    • East-A, East-B, West-A, West-B 같은 상세 데이터가 나오고 (기본 GROUP BY)
    • East 전체, West 전체, 그냥 전체 데이터가 나오고 (GROUP BY ROLLUP)
    • 제품 A 전체, 제품 B 전체 데이터도 나온다.

#

ROLLUP과 CUBE의 차이?

  • ROLLUP은 계층적으로 요약
    • ROLLUP(region, product)이면
      • 첫 번째 컬럼(region)을 기준으로 묶고 -> 그 “안에서” 두 번째 컬럼(product)을 묶고 -> 마지막으로 전체 합계까지 올라감
      • East-A 100, East-B 150, East 전체 250 / West-A 200, West-B 50, West 전체 250 / 전체 500
        • 보면 East / West 로 묶고 -> East 안에서 A/B로 묶고 -> 전체 500 함.
  • CUBE는 가능한 모든 조합
    • CUBE(region, product)이면
      • East-A 100, East-B 150, East 전체 250 / West-A 200, West-B 50, West 전체 250 / 제품 A 전체 300 / 제품 B 전체 200 / 전체 500
        • 보면 East / West 로 묶고 -> East 안에서 A/B로 묶고 -> A/B로 묶고 -> A안에서 East/West로 묶는건 의미없으니 없고 -> 전체 500 함.

#

3. UDF & 시퀀스 & 저장 프로시저 & UDT & 트리거 #

(p.95-101)

UDF

  • SQL 문법만으로는 반복적인 계산이나 특정 규칙 적용이 어려운데
    • UDF를 만들어놓으면 데이터베이스 안에 내장된 함수 외에도 필요할 때 불러다 쓸 수 있다.
  • is_similar 함수
    • 두 개의 실수값이 주어진 임계치 이상으로 가까운지를 판별하는함수
  • 실질적 활용?
    • 임베딩 스토어에서 코사인 유사도가 일정 기준 이상인 후보만 필터링하는 기능이니까
      • 데이터베이스 안에서 바로 AI 예측 후보 선별에 쓸수있다.

#

시퀀스

  • 자동으로 증가하는 고유 ID를 만들어줌
    • 테이블에 데이터를 넣을 때 시퀀스를 만들어 두고 nextval로 꺼내 쓰면 순차적으로 값이 올라가니까 데이터마다 일일이 ID를 붙이지 않아도 된다.
-- 시퀀스 생성
CREATE SEQUENCE my_seq START 1;

-- 임베딩을 저장하는 테이블 embedding_store
CREATE TABLE embedding_store (
    id BIGINT PRIMARY KEY DEFAULT nextval('my_seq'),
    user_text TEXT,
    embedding VECTOR(4)
);

-- nextval('my_seq')로 자동 증가하는 ID를 달아주기
INSERT INTO embedding_store (user_text, embedding)
VALUES ('hello world', '[0.1, 0.2, 0.3, 0.4]');

-- 또 다른 데이터를 넣으면 ID가 자동으로 2가 됨
INSERT INTO embedding_store (user_text, embedding)
VALUES ('how are you', '[0.5, 0.6, 0.7, 0.8]');

-- 확인해보면?
SELECT * FROM embedding_store;
id |   user_text   |      embedding
---+---------------+------------------------
 1 | hello world   | [0.1, 0.2, 0.3, 0.4]
 2 | how are you   | [0.5, 0.6, 0.7, 0.8]
  • 예시
    • CREATE SEQUENCE my_seq START 1; -> 이렇게 만들어 두면
    • DEFAULT nextval(‘my_seq’)를 컬럼에 달아주면
    • INSERT INTO embedding_store (user_text, embedding) 할 때 자동으로 ID가 올라간다
      • 매번 새로운 번호가 붙기 때문에 중복 없는 고유 ID를 쉽게 관리할수있다.
  • 실질적 활용?
    • 모델 예측 결과나 벡터 데이터가 쌓일 때 결과를 추적하거나 버전을 구분할때
      • 벡터를 하나씩 저장할 때마다 고유 번호를 자동으로 달아주면 나중에 “이 임베딩이 어떤 실험에서 나온 것인지”를 관리하기 쉽다.
    • 결과 추적?
      • 어떤 문장을 임베딩해서 384차원짜리 벡터를 만들었고 -> 벡터를 테이블에 저장할건데 -> 임베딩은 숫자 배열이므로 나중에 “이 벡터가 언제, 어떤 실험, 어떤 모델로 만들어진 건지”를 추적하기 어려운데 -> 이때 시퀀스로 생성한 고유 ID를 같이 붙여 주면?
        • 첫 번째 벡터 저장 → ID = 1000
        • 두 번째 벡터 저장 → ID = 1001
        • 세 번째 벡터 저장 → ID = 1002
        • 이렇게 고유 ID가 붙으면 나중에 분석할 때 “ID=1002인 벡터는 실험 X에서 나온 결과다” 하고 연결하기 쉽다.
    • 버전 관리?
      • 같은 문장을 두번 실험에 다르게 임베딩했으면 1차 실험 때는 모델 버전 1로 뽑은 벡터 2차 실험 때는 모델 버전 2로 뽑은 벡터가 있을 수 있고 -> 이럴 때 고유 ID를 붙여 두면 “실험 1번에서 나온 ID 1010 벡터와, 실험 2번에서 나온 ID 2020 벡터를 비교하자” 이렇게 버전 관리 할수있다.

#

저장 프로시저

  • 여러 SQL 문장을 묶어 하나의 절차처럼 실행
  • 예시
    • 예측 결과 테이블 prediction_results가 있고 실제 라벨(true_label)과 모델이 예측한 라벨(pred_label)이 있다.
    • AI 모델이 예측한 결과를 5개 저장하려고 한다.
-- 예측 결과 테이블 prediction_results
CREATE TABLE prediction_results (
    id BIGSERIAL PRIMARY KEY,
    true_label TEXT,
    pred_label TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 저장 프로시저 insert_prediction_batch
CREATE OR REPLACE PROCEDURE insert_prediction_batch(
    p_true_label TEXT,
    p_pred_label TEXT,
    p_count INT
)
LANGUAGE plpgsql
AS $$
BEGIN
    FOR i IN 1..p_count LOOP
        INSERT INTO prediction_results (true_label, pred_label)
        VALUES (p_true_label, p_pred_label);
    END LOOP;
END;
$$;

-- AI 모델의 예측 결과 저장하기
-- 저장 프로시저 안쓴경우
INSERT INTO prediction_results (true_label, pred_label) VALUES ('spam', 'spam');
INSERT INTO prediction_results (true_label, pred_label) VALUES ('spam', 'spam');
INSERT INTO prediction_results (true_label, pred_label) VALUES ('spam', 'spam');
INSERT INTO prediction_results (true_label, pred_label) VALUES ('spam', 'spam');
INSERT INTO prediction_results (true_label, pred_label) VALUES ('spam', 'spam');

-- 저장 프로시저를 쓴 경우
CALL insert_prediction_batch('spam', 'spam', 5);
  • 저장 프로시저가 없으면 개발자가 직접 5번 INSERT 문을 날려야하는데
  • 저장 프로시저가 있으면 똑같이 5건을 넣어야 하는 상황에서 CALL 한 줄만 쓰면 된다.
    • 프로시저 내부에 반복문(FOR i IN 1..p_count)이 있어서 알아서 5번 INSERT를 실행해준다

#

사용자 정의 데이터 타입(UDT)

  • 보통 테이블 컬럼은 숫자, 문자열 같은 단순 타입인데 내가 원하는 구조를 만들어서 하나의 타입처럼 쓸 수 있다.
  • 예측 결과를 저장하려고 할때.
    • 썼을때와 안썼을때의 차이를 보면?
-- 안 썼을 때
-- 예측 테이블 prediction_logs_plain 생성
CREATE TABLE prediction_logs_plain (
    id SERIAL PRIMARY KEY,
    model_name TEXT,
    label TEXT,
    score FLOAT,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 데이터 넣기
INSERT INTO prediction_logs_plain (model_name, label, score)
VALUES ('spam_detector', 'spam', 0.92);

-- 조회
SELECT id, model_name, label, score
FROM prediction_logs_plain;
 id | model_name    | label | score
----+---------------+-------+-------
  1 | spam_detector | spam  | 0.92
-- 썼을 때
-- UDT로 (label, score)를 하나의 덩어리로 정의
CREATE TYPE prediction_result_type AS (
    label TEXT,
    score FLOAT
);

-- 예측 테이블 prediction_logs_udt 생성
CREATE TABLE prediction_logs_udt (
    id SERIAL PRIMARY KEY,
    model_name TEXT,
    result prediction_result_type,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 데이터 넣기
INSERT INTO prediction_logs_udt (model_name, result)
VALUES ('spam_detector', ROW('spam', 0.92)::prediction_result_type);

-- 조회
SELECT id, model_name, (result).label, (result).score
FROM prediction_logs_udt;
 id | model_name    | label | score
----+---------------+-------+-------
  1 | spam_detector | spam  | 0.92
  • 구체적으로 어디가 다르냐면
    • 데이터 넣기
      • 안썼을때: (model_name, label, score) -> label과 score를 각각 컬럼에 직접 넣는다.
      • udt 썼을때: (model_name, result) -> label과 score를 ROW()로 묶어서 result라는 한 컬럼에 넣는다.
    • 조회
      • 안썼을때: SELECT label, score -> 그냥 컬럼 이름(label, score)으로 바로 꺼낸다.
      • udt 썼을때: SELECT (result).label, (result).score -> result 안에서 필드를 꺼내는 방식으로 꺼낸다.
  • 의문점
    • 출력 결과가 똑같은데 왜쓰는거지?
    • 출력 결과만 비교하면 같지만 확장성에서 차이가있다.
      • 안 썼을 때는 함수가 여러 개 값을 리턴해야 하면 RETURNS TABLE(label TEXT, score FLOAT) 같은 형태로 정의해야 하는데 썼을 때는 함수가 RETURNS prediction_result_type로 정의되니까 “이 함수는 예측 결과 하나를 리턴한다”라고 직관적으로 쓸 수 있다 즉 데이터 구조를 하나의 타입으로 추상화할 수 있다.
      • 안 썼을 때는 label, score를 다른 테이블에서도 쓰려면 매번 두 컬럼을 복사해야 하는데 썼을 때는 그냥 result prediction_result_type 하나만 선언하면 되니까 중복 정의를 줄이고 일관성 유지 가능(이건 예시에선 2개여서 메리트 없어보이는데 개수 늘어나면 납득됨)
      • 복잡한 구조 확장
        • 예측 결과가 단순히 label+score로 끝나지 않고 label, score, confidence_interval, metadata 같이 커질 수 있는데 안 썼을 때는 컬럼이 점점 늘어나고 테이블마다 다 복사해야 하지만 썼을 때는 타입만 확장하면 모든 테이블·함수에서 동일하게 활용 가능하다.

#

트리거

  • 데이터가 삽입, 수정, 삭제될 때 자동으로 실행되는 규칙
  • 예시
    • 새로운 벡터가 들어왔는데 유사도가 0.5보다 낮으면 경고 테이블에 따로 기록하려고 할때?
-- 트리거 안 썼을 때
-- 1. 메인 테이블
CREATE TABLE embedding_store_plain (
    id SERIAL PRIMARY KEY,
    text_input TEXT,
    similarity FLOAT
);

CREATE TABLE similarity_warnings_plain (
    warning_id SERIAL PRIMARY KEY,
    embedding_id INT,
    similarity FLOAT,
    warning_msg TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 2. 데이터 넣을 때
-- 새 데이터 삽입
INSERT INTO embedding_store_plain (text_input, similarity)
VALUES ('bad case', 0.3);

-- 임계치 체크 후 경고 삽입
INSERT INTO similarity_warnings_plain (embedding_id, similarity, warning_msg)
VALUES (currval('embedding_store_plain_id_seq'), 0.3, 'Similarity below threshold');
-- 트리거 썼을 때
-- 1. 메인 테이블 + 경고 테이블
CREATE TABLE embedding_store (
    id SERIAL PRIMARY KEY,
    text_input TEXT,
    similarity FLOAT
);

CREATE TABLE similarity_warnings (
    warning_id SERIAL PRIMARY KEY,
    embedding_id INT,
    similarity FLOAT,
    warning_msg TEXT,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 2. 트리거 함수 + 트리거 생성
CREATE OR REPLACE FUNCTION check_similarity_threshold()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.similarity < 0.5 THEN
        INSERT INTO similarity_warnings (embedding_id, similarity, warning_msg)
        VALUES (NEW.id, NEW.similarity, 'Similarity below threshold');
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_check_similarity
AFTER INSERT ON embedding_store
FOR EACH ROW
EXECUTE FUNCTION check_similarity_threshold();

-- 3. 데이터 넣기
INSERT INTO embedding_store (text_input, similarity)
VALUES ('bad case', 0.3);
  • 궁극적인 차이는
    • 메인 테이블 + 경고 테이블: 트리거를 쓰든 안 쓰든 구조는 똑같음
    • 데이터 넣을 때
      • 트리거 안 쓰면: INSERT (메인 테이블), INSERT (경고 테이블, 조건 만족 시) -> N개의 쿼리를 개발자가 직접 작성
      • 트리거 쓰면: INSERT (메인 테이블) -> → 1줄만 작성하면 나머지(조건 체크 + 경고 INSERT)는 DB가 자동 처리.

#

4. 윈도우 함수 #

(p.126-129)

집계함수와 윈도우함수 차이

  • 비슷하지만 GROUP BY처럼 그룹을 한 줄로 압축하지 않고, 각 행마다 순위, 누적합, 이전 값 같은 걸 계산함
-- scores 데이터
CREATE TABLE scores (
    student TEXT,
    subject TEXT,
    score INT
);

INSERT INTO scores VALUES
('철수', '수학', 80),
('철수', '영어', 90),
('영희', '수학', 95),
('영희', '영어', 85),
('민수', '수학', 70),
('민수', '영어', 75);

-- 집계 함수 GROUP BY로 학생별 평균 점수 구하기
SELECT student, AVG(score) AS avg_score
FROM scores
GROUP BY student;
student | avg_score
--------+----------
철수     |   85
영희     |   90
민수     |   72.5
-- 윈도우 함수 OVER로 학생별 평균 점수 구하기
SELECT student, subject, score,
       AVG(score) OVER (PARTITION BY student) AS avg_score
FROM scores;
student | subject | score | avg_score
--------+---------+-------+----------
철수     | 수학    |  80   |   85
철수     | 영어    |  90   |   85
영희     | 수학    |  95   |   90
영희     | 영어    |  85   |   90
민수     | 수학    |  70   |  72.5
민수     | 영어    |  75   |  72.5
  • GROUPBY -> 학생(그룹) 단위로 묶어서 한 줄로 결과를 압축했다.
  • OVER (PARTITION BY student) -> 학생(그룹)별로 평균을 계산하되 결과는 행마다 달아줬다.

#

윈도우 함수

  • ROW_NUMBER()
    • 그룹 안에서 순번을 매긴다.
      • 사용자별로 점수를 내림차순 정렬하고 ROW_NUMBER를 매기면, 그 사용자 안에서 1등, 2등, 3등을 구할 수 있다.
  • RANK()
    • 동점이 있을 때 같은 순위를 부여하고 건너뛰기가 발생한다.
      • 1등이 두 명이면 다음 순위는 3등.
  • DENSE_RANK()
    • 같은 순위가 있더라도 건너뛰지 않고 다음을 2등으로 붙인다.
  • NTILE(n)
    • 데이터를 n개 구간으로 자른다.
    • 100명을 NTILE(5)로 나누면 성적을 기준으로 20명씩 다섯 구간으로 나눌 수 있다.
  • LAG() & LEAD()
    • 현재 행 기준으로 앞 행이나 뒤 행 값을 참고할 수 있어서 시간 순서대로 점수를 나열해 두면 바로 직전 점수와 비교하거나 다음 점수를 미리 볼 수 있다
  • SUM() OVER, AVG() OVER
    • 누적합이나 누적평균 구한다.
-- ROW_NUMBER()
student | subject | score | rownum
--------+---------+-------+-------
철수     | 수학    |  95   |   1
철수     | 영어    |  95   |   2
철수     | 과학    |  90   |   3
영희     | 수학    | 100   |   1
영희     | 영어    |  80   |   2
민수     | 수학    |  70   |   1
민수     | 과학    |  70   |   2
민수     | 영어    |  60   |   3

-- RANK()
student | subject | score | rnk
--------+---------+-------+----
철수     | 수학    |  95   | 1
철수     | 영어    |  95   | 1
철수     | 과학    |  90   | 3

-- NTILE(n)
student | subject | score | bucket
--------+---------+-------+-------
철수     | 수학    | 95    | 1
철수     | 영어    | 95    | 1
철수     | 과학    | 90    | 2
-- 상위 절반 = 그룹 1, 하위 절반 = 그룹 2

-- LAG() & LEAD()
student | subject | score | prev_score | next_score
--------+---------+-------+------------+------------
민수     | 과학    |  70   |   (null)   |  60
민수     | 수학    |  70   |    70      |  60
민수     | 영어    |  60   |    70      | (null)
-- 직전 점수와 다음 점수 참조: 민수의 행마다 앞뒤 점수가 달라 붙음.

-- SUM() OVER, AVG() OVER
student | subject | score | running_total | avg_score
--------+---------+-------+----------------+----------
철수     | 수학    |  95   |       95       |   93.3
철수     | 영어    |  95   |      190       |   93.3
철수     | 과학    |  90   |      280       |   93.3

- running_total은 점수 누적합, avg_score는 평균.

#

AI 연계

  • 예측 결과를 저장한 prediction_logs 테이블
  • 활용
    • 여러 모델 버전이 같은 사용자에 대해 점수를 매겼을 때 그중 가장 높은 점수를 고르기.
      • ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY pred_score DESC) -> rownum = 1인 행만 선택
    • 모델 간 성능 비교
      • RANK() OVER (PARTITION BY user_id ORDER BY pred_score DESC)
    • 예측 점수 상위 20% 사용자 그룹을 뽑기
      • NTILE(5) OVER (ORDER BY pred_score DESC) -> bucket = 1인 행만 선택
    • 이전 점수와 비교해 사용자의 점수가 올랐는지 떨어졌는지 확인
      • LAG(pred_score) OVER (PARTITION BY user_id ORDER BY created_at) -> pred_score - prev_score 차이 계산
    • 모델 정확도의 누적 변화 확인
      • SUM(pred_score) OVER (…), AVG(pred_score) OVER (…)

#

5. 고급 인덱스 #

(p.144-148)

고급 인덱스?

  • 일반적인 데이터베이스 인덱스는 B-Tree 인덱스.
  • AI에서 다루는 데이터는 단순 숫자 키가 아니라 JSON 문서, 벡터, 시계열 로그처럼 복잡하거나 대용량 특성이 있어서 다른 종류의 인덱스들이 필요하다.

#

GIN 인덱스

  • Inverted Index: 거꾸로 색인.
    • 보통 행 -> 마다 단어가 있는데
    • “찾고 싶은 단어 -> 그 단어가 들어 있는 행"으로 인덱스를 만든다.
{"category": "esg", "type": "finance"} -> 1
{"category": "ai", "type": "tech"} -> 2
{"category": "esg", "type": "policy"} -> 3
"category"="esg" -> 1행, 3행
"category"="ai" -> 2행
"type"="finance" -> 1행
"type"="tech" -> 2행
"type"="policy" -> 3행
  • “category”=“esg"인 행을 찾고 싶으면 테이블을 처음부터 끝까지 보지 않고 인덱스를 통해 곧바로 1, 3행을 볼수있다.

#

GiST 인덱스

  • AI에서 쓰는 벡터 데이터에서의 인덱싱은
    • 사전처럼 정확한 값을 빠르게 찾기보다는 이 벡터와 가장 비슷한 벡터를 찾는, 정확히 같은 값이 아니라 가까운 값을 찾는 경우가 많다.
    • GiST 인덱스는 “거리 기반” 검색을 빠르게 해 주는 구조여서 가까운 것을 찾는 인덱싱에 적합하다.

#

BRIN 인덱스

  • 범위별 최소·최대 값만 기록해 두고, 그 안에 데이터가 있을 거라고 좁혀 가는 방식
  • 일기장이 날짜 순으로
    • 1월 1일~1월 10일 -> 1권
    • 1월 11일~1월 20일 -> 2권
    • 1월 21일~1월 31일 -> 3권
      • 이렇게 적혀있으면 1월 15일 일기를 찾으려고하면 2권만 열어 보면 된다.
  • 빠른 이유는 범위만 보고 필요한 블록만 열어보면 되기 때문.
    • 잘 맞는 경우는 로그, 시계열 데이터
    • 잘 안 맞는 경우는 무작위 데이터. 왜냐면 “최소~최대”로 구간을 좁힐 수 없기 때문에 범위가 의미가 없다.

#