Natural Language to SQL: 자연어로 데이터베이스 조회하기
Natural Language to SQL: 자연어로 데이터베이스 조회하기
데이터베이스에서 원하는 정보를 찾기 위해 복잡한 SQL 쿼리를 작성하는 것은 개발자에게도 때로는 번거로운 일입니다. 하물며 비개발자들에게는 더욱 어려운 일이죠. 이런 문제를 해결하기 위해 등장한 것이 바로 **Natural Language to SQL(NL2SQL)** 기술입니다.
NL2SQL이란?
NL2SQL은 자연어로 작성된 질문을 SQL 쿼리로 자동 변환하는 기술입니다. 예를 들어:
- 자연어: "지난달 매출이 가장 높은 제품은 무엇인가?"
- SQL: `SELECT product_name FROM sales WHERE sale_date >= '2024-05-01' ORDER BY amount DESC LIMIT 1;`
이처럼 일상 언어로 질문하면 시스템이 자동으로 해당하는 SQL 쿼리를 생성해줍니다.
핵심 동작 원리
1. 자연어 이해 (Natural Language Understanding)
사용자의 질문에서 핵심 요소들을 추출합니다:
- 의도: 조회, 집계, 비교 등
- 엔티티: 테이블명, 컬럼명에 해당하는 개념들
- 조건: 필터링, 정렬, 그룹화 조건들
2. 스키마 매핑 (Schema Mapping)
자연어 개념을 실제 데이터베이스 스키마와 연결합니다:
- 제품 → `products` 테이블
- 매출 → `sales.amount` 컬럼
- 지난달 → 날짜 범위 조건
3. SQL 생성 (SQL Generation)
파악된 정보를 바탕으로 실행 가능한 SQL 쿼리를 생성합니다.
실제 구현 예제
간단한 NL2SQL 시스템을 Python으로 구현해보겠습니다. 이 예제는 교육 목적으로 만든 것이며, 실제 프로덕션에서는 더 정교한 처리가 필요합니다.
기본 설정
python
import re
import sqlite3
from datetime import datetime, timedelta
from typing import Dict, List, Tuple
class SimpleNL2SQL:
def __init__(self, db_path: str):
self.conn = sqlite3.connect(db_path)
self.schema_mapping = {
# 테이블 매핑
'제품': 'products',
'상품': 'products',
'판매': 'sales',
'매출': 'sales',
'고객': 'customers',
'사용자': 'customers',
# 컬럼 매핑
'이름': 'name',
'가격': 'price',
'금액': 'amount',
'날짜': 'date',
'수량': 'quantity'
}
# 의도 패턴
self.intent_patterns = {
'select': [r'무엇', r'어떤', r'보여줘', r'알려줘', r'찾아줘'],
'count': [r'몇 개', r'개수', r'얼마나'],
'max': [r'가장 높은', r'최대', r'최고'],
'min': [r'가장 낮은', r'최소', r'최저'],
'sum': [r'총합', r'합계', r'전체']
}
# 조건 패턴
self.condition_patterns = {
'last_month': [r'지난달', r'저번달'],
'this_month': [r'이번달', r'현재달'],
'greater_than': [r'이상', r'보다 큰'],
'less_than': [r'이하', r'보다 작은']
}
자연어 파싱 함수
python
def parse_natural_language(self, question: str) -> Dict:
"""자연어 질문을 파싱하여 의도와 조건을 추출"""
result = {
'intent': 'select',
'table': None,
'columns': [],
'conditions': [],
'order_by': None,
'limit': None
}
# 의도 파악
for intent, patterns in self.intent_patterns.items():
if any(re.search(pattern, question) for pattern in patterns):
result['intent'] = intent
break
# 테이블 매핑
for korean_term, english_term in self.schema_mapping.items():
if korean_term in question:
if english_term in ['products', 'sales', 'customers']:
result['table'] = english_term
else:
result['columns'].append(english_term)
# 조건 처리
if any(re.search(pattern, question) for pattern in self.condition_patterns['last_month']):
last_month = datetime.now() - timedelta(days=30)
result['conditions'].append(f"date >= '{last_month.strftime('%Y-%m-%d')}'")
# 정렬 및 제한
if result['intent'] in ['max', 'min']:
order_direction = 'DESC' if result['intent'] == 'max' else 'ASC'
if 'amount' in result['columns'] or '금액' in question:
result['order_by'] = f"ORDER BY amount {order_direction}"
elif 'price' in result['columns'] or '가격' in question:
result['order_by'] = f"ORDER BY price {order_direction}"
result['limit'] = "LIMIT 1"
return result
SQL 생성 함수
python
def generate_sql(self, parsed_data: Dict) -> str:
"""파싱된 데이터를 기반으로 SQL 쿼리 생성"""
# 기본 SELECT 구조
if parsed_data['intent'] == 'count':
select_clause = "SELECT COUNT(*)"
elif parsed_data['intent'] in ['sum']:
column = parsed_data['columns'][0] if parsed_data['columns'] else 'amount'
select_clause = f"SELECT SUM({column})"
else:
columns = ', '.join(parsed_data['columns']) if parsed_data['columns'] else '*'
select_clause = f"SELECT {columns}"
# FROM 절
table = parsed_data['table'] or 'sales' # 기본 테이블
from_clause = f"FROM {table}"
# WHERE 절
where_clause = ""
if parsed_data['conditions']:
where_clause = f"WHERE {' AND '.join(parsed_data['conditions'])}"
# ORDER BY 절
order_clause = parsed_data['order_by'] or ""
# LIMIT 절
limit_clause = parsed_data['limit'] or ""
# 최종 쿼리 조합
query_parts = [select_clause, from_clause, where_clause, order_clause, limit_clause]
sql_query = ' '.join(part for part in query_parts if part)
return sql_query
def query(self, question: str) -> Tuple[str, List]:
"""자연어 질문을 SQL로 변환하고 실행"""
try:
parsed = self.parse_natural_language(question)
sql_query = self.generate_sql(parsed)
cursor = self.conn.cursor()
cursor.execute(sql_query)
results = cursor.fetchall()
return sql_query, results
except Exception as e:
return f"Error: {str(e)}", []
테스트 데이터베이스 설정
python
def setup_test_database():
"""테스트용 데이터베이스 생성"""
conn = sqlite3.connect('test.db')
cursor = conn.cursor()
# 테이블 생성
cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY,
name TEXT,
price REAL,
category TEXT
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS sales (
id INTEGER PRIMARY KEY,
product_id INTEGER,
amount REAL,
quantity INTEGER,
date TEXT,
FOREIGN KEY (product_id) REFERENCES products (id)
)
''')
# 샘플 데이터 삽입
products_data = [
(1, '노트북', 1200000, '전자제품'),
(2, '마우스', 25000, '전자제품'),
(3, '키보드', 80000, '전자제품'),
(4, '모니터', 300000, '전자제품')
]
sales_data = [
(1, 1, 1200000, 1, '2024-05-15'),
(2, 2, 50000, 2, '2024-05-16'),
(3, 3, 160000, 2, '2024-05-17'),
(4, 1, 2400000, 2, '2024-05-18'),
(5, 4, 300000, 1, '2024-06-01')
]
cursor.executemany('INSERT OR REPLACE INTO products VALUES (?, ?, ?, ?)', products_data)
cursor.executemany('INSERT OR REPLACE INTO sales VALUES (?, ?, ?, ?, ?)', sales_data)
conn.commit()
conn.close()
사용 예제
python
# 데이터베이스 설정
setup_test_database()
# NL2SQL 시스템 초기화
nl2sql = SimpleNL2SQL('test.db')
# 테스트 질문들
test_questions = [
"지난달 매출이 가장 높은 제품은 무엇인가?",
"전체 판매 개수는 몇 개인가?",
"모든 제품을 보여줘",
"총 매출 합계는 얼마인가?"
]
print("=== NL2SQL 테스트 결과 ===\n")
for question in test_questions:
print(f"질문: {question}")
sql_query, results = nl2sql.query(question)
print(f"생성된 SQL: {sql_query}")
print(f"결과: {results}")
print("-" * 50)
```
**실행 결과:**
```
=== NL2SQL 테스트 결과 ===
질문: 지난달 매출이 가장 높은 제품은 무엇인가?
생성된 SQL: SELECT * FROM sales WHERE date >= '2024-05-03' ORDER BY amount DESC LIMIT 1
결과: [(4, 1, 2400000.0, 2, '2024-05-18')]
질문: 전체 판매 개수는 몇 개인가?
생성된 SQL: SELECT COUNT(*) FROM sales
결과: [(5,)]
질문: 모든 제품을 보여줘
생성된 SQL: SELECT * FROM products
결과: [(1, '노트북', 1200000.0, '전자제품'), (2, '마우스', 25000.0, '전자제품'), ...]
고급 기능 구현
LLM 기반 NL2SQL
실제 프로덕션 환경에서는 OpenAI GPT나 다른 LLM을 활용할 수 있습니다:
python
import openai
from typing import Dict, Any
class LLMBasedNL2SQL:
def __init__(self, api_key: str, db_schema: Dict[str, Any]):
self.client = openai.OpenAI(api_key=api_key)
self.db_schema = db_schema
def generate_sql_with_llm(self, question: str) -> str:
"""LLM을 사용해 자연어를 SQL로 변환"""
schema_description = self._format_schema_for_prompt()
prompt = f"""
다음 데이터베이스 스키마를 참고하여 자연어 질문을 SQL 쿼리로 변환해주세요.
데이터베이스 스키마:
{schema_description}
질문: {question}
응답은 실행 가능한 SQL 쿼리만 반환해주세요.
"""
response = self.client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "당신은 자연어를 SQL로 변환하는 전문가입니다."},
{"role": "user", "content": prompt}
],
temperature=0.1
)
return response.choices[0].message.content.strip()
def _format_schema_for_prompt(self) -> str:
"""스키마 정보를 프롬프트용으로 포맷팅"""
schema_text = ""
for table_name, table_info in self.db_schema.items():
schema_text += f"테이블: {table_name}\n"
schema_text += f"컬럼: {', '.join(table_info['columns'])}\n\n"
return schema_text
주요 도전과제와 해결 방안
1. 스키마 복잡성
실제 데이터베이스는 수백 개의 테이블을 가질 수 있습니다. 이를 해결하기 위해:
- 스키마 임베딩: 테이블과 컬럼 정보를 벡터로 변환하여 의미적 유사성 기반 매칭
- 계층적 스키마 탐색: 관련성 높은 테이블부터 우선 고려
2. 모호성 해결
"큰 주문"이 금액인지 수량인지 명확하지 않은 경우:
- 컨텍스트 학습: 이전 대화 내용 참고
- 사용자 확인: 모호한 경우 명확화 질문
3. 복합 쿼리 처리
여러 조건과 조인이 포함된 복잡한 질문:
- 단계별 분해: 복잡한 질문을 단순한 하위 질문으로 분해
- 템플릿 기반 접근: 자주 사용되는 쿼리 패턴을 템플릿화
실제 활용 사례
1. 비즈니스 인텔리전스
python
# 예시: 대시보드에서 자연어 질의
questions = [
"이번 분기 매출 TOP 10 제품은?",
"작년 대비 매출 증가율은?",
"지역별 고객 분포는 어떻게 되나?"
]
2. 고객 서비스 챗봇
python
# 예시: 고객이 주문 상태를 자연어로 문의
def handle_customer_query(customer_id: str, question: str):
# "내 지난달 주문 내역 보여줘" -> SQL 변환
personalized_question = f"고객 ID {customer_id}의 {question}"
return nl2sql.query(personalized_question)
성능 최적화 팁
1. 캐싱 전략
python
from functools import lru_cache
class OptimizedNL2SQL(SimpleNL2SQL):
@lru_cache(maxsize=1000)
def cached_query(self, question: str):
"""자주 사용되는 질문을 캐싱"""
return self.query(question)
2. 인덱스 최적화
sql
-- 자주 조회되는 컬럼에 인덱스 생성
CREATE INDEX idx_sales_date ON sales(date);
CREATE INDEX idx_sales_amount ON sales(amount);
마무리
NL2SQL 기술은 데이터 접근성을 크게 향상시키는 혁신적인 기술입니다. 간단한 규칙 기반 시스템부터 최신 LLM을 활용한 고도화된 시스템까지, 프로젝트의 요구사항에 맞는 적절한 수준에서 구현할 수 있습니다.