스키마 이해 및 주입
LLM 컨텍스트를 위해 DB 스키마를 추출하고 형식화합니다. 테이블, 열, 관계를 다룹니다.
스키마 이해 및 주입은(는) CoddyKit의 무료 AI Agents 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 AI Agents 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.
스키마 컨텍스트가 중요한 이유
LLM은 SQL 문법은 알지만 여러분의 데이터베이스에 대해서는 아무것도 알지 못합니다. 스키마 컨텍스트가 없으면 테이블과 열 이름을 지어냅니다.
스키마 주입은 데이터베이스 구조를 프로그래밍 방식으로 추출하여 모든 프롬프트에 포함하는 것을 의미합니다. 이를 통해 LLM이 정확한 테이블, 열, 타입을 알 수 있습니다.
INFORMATION_SCHEMA 쿼리
모든 주요 관계형 데이터베이스는 INFORMATION_SCHEMA를 통해 메타데이터를 제공합니다. 이를 쿼리하면 애플리케이션 코드를 건드리지 않고 모든 테이블, 열 이름, 데이터 타입을 가져올 수 있습니다.
이 방식은 PostgreSQL, MySQL, SQL Server, SQLite에서 사용할 수 있습니다(약간의 차이는 있습니다).
import psycopg2
def get_schema(conn):
query = '''
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position
'''
with conn.cursor() as cur:
cur.execute(query)
return cur.fetchall()테이블별 열 그룹화
원시 INFORMATION_SCHEMA 결과는 행의 평면 목록입니다. 테이블 이름을 기준으로 그룹화하면 프롬프트에 형식을 지정하기 쉬운 구조화된 표현을 만들 수 있습니다.
from collections import defaultdict
def build_schema_dict(conn):
rows = get_schema(conn)
schema = defaultdict(list)
for table_name, column_name, data_type in rows:
schema[table_name].append({
'name': column_name,
'type': data_type
})
return dict(schema)
# Result:
# {
# 'users': [{'name': 'id', 'type': 'integer'}, {'name': 'email', 'type': 'character varying'}],
# 'orders': [{'name': 'id', 'type': 'integer'}, {'name': 'user_id', 'type': 'integer'}]
# }LLM 프롬프트용 스키마 형식 지정
LLM은 스키마를 일반 텍스트로 읽습니다. 열 이름과 타입을 괄호 안에 넣고 테이블마다 한 줄을 사용하는 간결하고 읽기 쉬운 형식을 사용합니다.
기본 키(PK)와 외래 키(FK)를 포함하면 LLM이 올바른 JOIN 문을 작성하는 데 도움이 됩니다.
def format_schema_for_prompt(schema_dict, pk_info=None, fk_info=None):
lines = []
for table, columns in schema_dict.items():
col_parts = []
for col in columns:
label = col['name']
if pk_info and (table, col['name']) in pk_info:
label += ' PK'
if fk_info and (table, col['name']) in fk_info:
label += f' FK->{fk_info[(table, col["name"])]}'
col_parts.append(f"{label} ({col['type']})")
lines.append(f"Table {table}: {', '.join(col_parts)}")
return '\n'.join(lines)
# Output:
# Table users: id PK (integer), email (varchar), created_at (timestamp)
# Table orders: id PK (integer), user_id FK->users.id (integer), total (float)
if __name__ == '__main__':
demo_schema = {'users': [{'name': 'id', 'type': 'integer'}, {'name': 'email', 'type': 'varchar'}]}
demo_pk = {('users', 'id')}
print(format_schema_for_prompt(demo_schema, pk_info=demo_pk))
기본 키와 외래 키 포함
외래 키 관계는 스키마 컨텍스트에서 가장 중요한 부분입니다. LLM에 JOIN을 작성하는 방법을 알려 주기 때문입니다. information_schema.table_constraints와 key_column_usage를 쿼리하여 관계를 추출합니다.
def get_foreign_keys(conn):
query = '''
SELECT
kcu.table_name,
kcu.column_name,
ccu.table_name AS foreign_table,
ccu.column_name AS foreign_column
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
'''
with conn.cursor() as cur:
cur.execute(query)
return {
(row[0], row[1]): f'{row[2]}.{row[3]}'
for row in cur.fetchall()
}
if __name__ == '__main__':
class FakeCursor:
def __enter__(self): return self
def __exit__(self, *a): return False
def execute(self, query): pass
def fetchall(self):
return [('orders', 'user_id', 'users', 'id')]
class FakeConn:
def cursor(self): return FakeCursor()
fks = get_foreign_keys(FakeConn())
print('Foreign keys found:')
for (table, col), ref in fks.items():
print(f' {table}.{col} -> {ref}')
스키마 압축: 문제
실제 기업 데이터베이스에는 200개가 넘는 테이블이 있을 수 있습니다. 전체 스키마를 주입하면 GPT-4의 컨텍스트 창을 초과하고 토큰에 비용을 낭비하게 됩니다.
각각 20개의 열을 가진 200개 테이블의 스키마는 대략 40,000개 이상의 토큰이므로 모든 쿼리마다 전송하기에는 비용이 너무 큽니다.
def estimate_schema_tokens(schema_dict):
text = format_schema_for_prompt(schema_dict)
# Rough estimate: 1 token per 4 characters
estimated_tokens = len(text) // 4
print(f'Tables: {len(schema_dict)}')
print(f'Estimated schema tokens: {estimated_tokens}')
return estimated_tokens
# 200 tables * 15 columns * 25 chars/col = 75,000 chars = ~18,750 tokens
# Plus user question + system prompt = easily over context limit스키마 압축: 선택적 주입
가장 효과적인 압축 전략은 질문과 관련된 테이블만 주입하는 것입니다. 2단계 접근 방식을 사용합니다. 먼저 LLM에 필요한 테이블을 질문한 다음 해당 테이블의 스키마만 주입합니다.
def select_relevant_tables(question, all_table_names, n=5):
table_list = ', '.join(all_table_names)
prompt = f'''Database tables: {table_list}
Question: {question}
List the {n} most relevant table names as a JSON array.
Example: ["users", "orders", "products"]'''
response = llm_call(prompt)
import json
return json.loads(response)
def compressed_schema(question, conn):
all_tables = list(build_schema_dict(conn).keys())
relevant = select_relevant_tables(question, all_tables)
full_schema = build_schema_dict(conn)
return {t: full_schema[t] for t in relevant if t in full_schema}스키마 압축: 불필요한 열 제외
많은 테이블에는 created_at, updated_at, deleted_at, version, created_by처럼 비즈니스 쿼리와 거의 관련이 없는 감사 열이 있습니다. 이러한 열을 제거하여 토큰 수를 줄입니다.
AUDIT_COLUMNS = {
'created_at', 'updated_at', 'deleted_at', 'created_by',
'updated_by', 'version', 'is_deleted', 'modified_at'
}
def compress_schema(schema_dict, exclude_audit=True):
compressed = {}
for table, columns in schema_dict.items():
# Skip internal/system tables
if table.startswith('_') or table.startswith('pg_'):
continue
if exclude_audit:
columns = [c for c in columns if c['name'] not in AUDIT_COLUMNS]
if columns: # only include if columns remain
compressed[table] = columns
return compressed
if __name__ == '__main__':
demo_schema = {
'users': [{'name': 'id', 'type': 'INT'}, {'name': 'email', 'type': 'VARCHAR'}, {'name': 'created_at', 'type': 'TIMESTAMP'}],
'pg_stat': [{'name': 'x', 'type': 'INT'}],
}
compressed = compress_schema(demo_schema)
print('Tables kept:', list(compressed.keys()))
print('users columns after compression:', [c['name'] for c in compressed['users']])
테이블 설명 추가
열 이름만으로는 의미가 항상 분명하지 않습니다. 각 테이블이 무엇을 나타내는지 자연어로 설명하면 SQL 생성 품질이 크게 향상됩니다.
설명을 구성 파일이나 PostgreSQL 테이블 주석으로 저장합니다.
TABLE_DESCRIPTIONS = {
'users': 'Registered app users with authentication info',
'orders': 'Customer purchase orders',
'order_items': 'Individual line items within an order',
'products': 'Product catalog with pricing',
'payments': 'Payment transactions linked to orders'
}
def format_schema_with_descriptions(schema_dict):
lines = []
for table, columns in schema_dict.items():
desc = TABLE_DESCRIPTIONS.get(table, '')
col_str = ', '.join(f"{c['name']} ({c['type']})" for c in columns)
if desc:
lines.append(f"Table {table} ({desc}): {col_str}")
else:
lines.append(f"Table {table}: {col_str}")
return '\n'.join(lines)
if __name__ == '__main__':
demo_schema = {'users': [{'name': 'id', 'type': 'INT'}], 'orders': [{'name': 'id', 'type': 'INT'}]}
print(format_schema_with_descriptions(demo_schema))
스키마 캐싱
데이터베이스 스키마는 거의 변경되지 않습니다. 매 쿼리마다 INFORMATION_SCHEMA를 조회하면 지연 시간과 부하가 증가합니다. 형식이 지정된 스키마 문자열을 캐시하고, 스키마 변경 이벤트가 발생하거나 시간 기반 TTL이 만료될 때 캐시를 무효화하십시오.
import time
class SchemaCache:
def __init__(self, ttl_seconds=300):
self._cache = None
self._timestamp = 0
self.ttl = ttl_seconds
def get(self, conn):
now = time.time()
if self._cache is None or (now - self._timestamp) > self.ttl:
print('Refreshing schema cache...')
schema_dict = build_schema_dict(conn)
fk_info = get_foreign_keys(conn)
self._cache = format_schema_for_prompt(schema_dict, fk_info=fk_info)
self._timestamp = now
return self._cache
schema_cache = SchemaCache(ttl_seconds=300)전체 스키마 주입 흐름
모든 기법을 결합하는 방법은 다음과 같습니다. 압축된 스키마를 캐시하고 시스템 프롬프트에 주입하며, 대규모 데이터베이스에는 선택적 테이블 필터링을 사용합니다.
def build_sql_agent_prompt(question, conn, large_db=False):
if large_db:
schema = compressed_schema(question, conn)
schema_text = format_schema_with_descriptions(schema)
else:
schema_text = schema_cache.get(conn)
system = f'''You are a PostgreSQL expert.
Return ONLY a valid SELECT query based on this schema:
{schema_text}
Rules:
- Use only SELECT statements
- Use table aliases for clarity
- Limit results to 100 rows unless asked for all
'''
return system지식 확인
전체 스키마를 주입하는 대신 선택적 테이블 주입을 사용해야 하는 경우는 언제입니까?
복습: 스키마 이해 및 주입
효과적인 스키마 주입은 신뢰할 수 있는 NL-to-SQL 에이전트의 기반입니다. INFORMATION_SCHEMA에서 구조를 추출하고 기본 키와 외래 키 관계를 포함한 다음, LLM에 맞는 간결한 텍스트로 형식을 지정하십시오.
대규모 데이터베이스에서는 스키마를 캐시하고 감사용 열을 제거하며, 각 질문과 관련된 테이블만 전송하도록 선택적 주입을 사용하십시오. 자연어로 작성한 테이블 설명을 추가하면 쿼리 품질이 더욱 향상됩니다.
자주 묻는 질문
“스키마 이해 및 주입” 강의는 무료인가요?
네 — “스키마 이해 및 주입” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 AI Agents 강의 전체를 잠금 해제할 수 있습니다. AI Agents 강의에는 총 4개의 강의가 포함되어 있습니다.
“스키마 이해 및 주입”에서 뭘 배우나요?
LLM 컨텍스트를 위해 DB 스키마를 추출하고 형식화합니다. 테이블, 열, 관계를 다룹니다. 브라우저에서 직접 실행하는 실습 코드로 AI Agents을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
AI Agents을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 AI Agents은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“스키마 이해 및 주입” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 AI Agents 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 AI Agents 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.