Text-to-SQL 설명: AI가 일반 영어 문장을 SQL 쿼리로 변환하는 방법
요약
Text-to-SQL 시스템이 자연어를 SQL 쿼리로 변환하는 4단계 프로세스를 설명합니다. LLM이 스키마 정보를 모른다는 점을 지적하며, 정확한 쿼리 생성을 위한 스키마 컨텍스트 주입의 중요성을 강조합니다.
핵심 포인트
- Text-to-SQL은 스키마 주입, 파싱, 생성, 실행의 4단계를 거침
- LLM은 SQL 문법은 알지만 사용자의 특정 데이터베이스 스키마는 모름
- 정확한 결과 도출을 위해서는 프롬프트에 상세한 스키마 컨텍스트 제공이 필수적임
- 시스템의 성공 여부는 LLM뿐만 아니라 데이터 배관(plumbing) 설계에 달려 있음
Text-to-SQL 설명: AI가 일반 영어 문장을 SQL 쿼리로 변환하는 방법
지금쯤 데모를 보셨을 겁니다. 누군가 채팅창에 "지난달 매출 기준으로 상위 5개 고객을 보여줘"라고 입력하면, 완성된 SQL 쿼리가 나타나고 실행되어 테이블을 반환합니다. 마법처럼 느껴지거나, 수년간 익힌 WHERE 절이 사라지는 느낌일 수도 있습니다.
하지만 그렇지 않습니다. Text-to-SQL은 실제로 유용하고 존재하는 기능이지만, 동시에 누수가 심한 추상화(leaky abstraction)이기도 합니다. 만약 사용자가 데이터베이스에 질문을 할 수 있게 하는 제품을 만들고 있거나, 단순히 같은 보고서 쿼리를 작성하는 것에 지쳤다면, 영어와 SQL 사이에서 실제로 무슨 일이 일어나는지 이해할 가치가 있습니다. 왜냐하면 문제가 생겼을 때(그리고 분명히 생길 것이므로), 파이프라인을 아는 것이 5분 만에 해결되는 것과 "숫자가 틀렸다"라는 설명만 붙은 지원 티켓을 받는 것 사이의 차이를 만들기 때문입니다.
본 게시물에서는 질문이 도착하는 순간부터 쿼리가 실행될 때까지, 그리고 각 단계가 실패하기 쉬운 지점들을 포함하여 Text-to-SQL 시스템이 실제로 어떻게 작동하는지 설명합니다.
Text-to-SQL 시스템의 네 가지 단계
내부적으로 볼 때, 거의 모든 Text-to-SQL 시스템(자체 구축한 LLM 래퍼든 세련된 분석 제품이든)은 동일한 네 단계를 거칩니다:
- 스키마 컨텍스트 주입 (Schema context injection) — 모델에게 어떤 테이블과 컬럼이 존재하는지 알려줍니다.
- 자연어 파싱 (Natural language parsing) — 모델이 사용자가 실제로 무엇을 묻고 있는지 해석합니다.
- SQL 생성 (SQL generation) — 모델이 쿼리를 작성합니다.
- 실행 및 결과 전달 (Execution and result delivery) — 쿼리가 실행되고 결과가 반환됩니다.
흥미로운 점은 LLM이 오직 두 번째와 세 번째 단계만을 담당한다는 것입니다. 첫 번째와 네 번째 단계는 사용자가 제어하는 '배관(plumbing)'이며, 이 배관이 보통 전체 시스템 작동 여부를 결정합니다.
1단계: 모델은 데이터베이스를 모른다
사람들이 놓치는 사실은 이것입니다: LLM은 당신의 데이터베이스를 본 적이 없습니다. 모델은 당신에게 orders 테이블이 있다는 사실도, status가 'refunded'가 될 수 있다는 점도, 혹은 매출(revenue)이 orders.total이 아니라 subscriptions.mrr_cents에 저장되어 있다는 사실도 알지 못합니다. 기본 상태에서 모델이 아는 것은 _SQL 구문 (SQL syntax)_일 뿐, _당신의 스키마 (schema)_가 아닙니다.
따라서 모델이 단 한 줄이라도 작성하기 전에, 시스템은 프롬프트(prompt)에 스키마 컨텍스트 (schema context)를 주입합니다: 테이블 이름, 컬럼 이름, 데이터 타입(data types), 그리고 이들을 연결하는 외래 키(foreign-key) 관계 등이 포함됩니다. 모델이 받는 정보의 단순화된 버전은 다음과 같습니다:
Table: orders
id (int, primary key)
user_id (int, foreign key -> users.id)
...
이러한 컨텍스트와 _"지난달 프로 사용자로부터 발생한 매출은 얼마인가요?"_라는 질문이 주어지면, 모델은 이제 다음과 같은 결과를 생성할 수 있습니다:
SELECT SUM(o.amount_cents) / 100.0 AS revenue_dollars
FROM orders o
JOIN users u ON u.id = o.user_id
...
이러한 정확성이 스키마 힌트 (schema hints)에 얼마나 많이 의존하는지 주목하십시오. 모델이 status = 'paid'를 필터링하고(환불 건은 제외), 100으로 나누어야 한다는 것을 알 수 있었던 이유는 오직 스키마가 금액은 센트(cents) 단위로 저장되며 상태(status)는 열거형(enum)이라는 것을 알려주었기 때문입니다. 이 컨텍스트를 제거하면, 환불 금액을 조용히 중복 계산하는, 그럴싸해 보이기만 하는 쿼리가 생성됩니다.
이것이 바로 스키마 연결 (schema linking) — 질문의 단어를 올바른 테이블 및 컬럼과 매칭하는 작업 — 이 실패의 가장 큰 원인인 이유이기도 합니다. Text-to-SQL 시스템에 대한 연구에 따르면, 스키마 연결 오류가 전체 실수의 절반 이상을 차지한다는 결과가 일관되게 나타납니다. 데이터베이스에 수백 개의 컬럼이 있는 경우, 전체 스키마를 프롬프트에 쏟아붓는 것은 오히려 해롭습니다. 관련 없는 테이블들이 노이즈 (noise)가 되어 모델이 잘못된 조인 (join)을 수행하도록 유도하기 때문입니다. 더 뛰어난 시스템들은 무언가를 생성하기 전에 질문과 관련이 있을 법한 소수의 테이블만을 검색(retrieve)하여 사용합니다.
2단계 및 3단계: 모호한 영어에서 정밀한 SQL로
여기에 근본적인 긴장 관계가 존재합니다. 인간의 언어는 모호하고 문맥 의존적(context-dependent)입니다. 반면 SQL은 정밀하며 타협이 없습니다. "지난달"은 직전 달력을 의미할 수도 있고, 지난 30일을 의미할 수도 있습니다. "활성 사용자(Active users)"는 로그인한 모든 사람을 의미할 수도, 유료 플랜을 가진 사람을 의미할 수도, 혹은 소프트 삭제(soft-deleted)되지 않은 모든 사람을 의미할 수도 있습니다. 모델은 이러한 모호함을 하나의 정확한 해석으로 압축해야 하며, 사용자가 요구하지 않는 한 모델은 자신이 어떤 해석을 선택했는지 알려주지 않습니다.
_"우리의 우수 고객을 보여줘(Show me our best customers)."_라는 문장을 생각해 봅시다. 단 하나의 정답 쿼리는 없습니다. 모델은 다음과 같은 쿼리를 생성할 수 있습니다:
SELECT u.email, SUM(o.amount_cents) / 100.0 AS lifetime_value
FROM users u
JOIN orders o ON o.user_id = u.id
...
이는 "우수함(best)"을 생애 가치(lifetime spend)로 매핑한 합리적인 추측입니다. 하지만 귀하의 비즈니스에서는 "우수함"을 가장 빈번한 구매자, 가장 최근의 구매자, 또는 가장 높은 유지율(retention)로 정의할 수도 있습니다. SQL 자체는 유효하지만, 논리는 틀렸을 수 있습니다. 이것이 가장 치명적인 실패 모드(failure mode)입니다. 오류가 발생하여 멈추는 것이 아니라, 확신에 찬 잘못된 수치를 제공하기 때문입니다.
이 단계를 유의미하게 개선하는 두 가지 기술이 있습니다. 첫 번째는 **퓨샷 예시(few-shot examples)**입니다. 모델에게 귀하의 도메인에서 추출한 몇 가지 질문/쿼리 쌍을 보여주는 것입니다. 연구에 따르면 좋은 예시를 제공하는 것만으로도 실행 정확도(execution-accuracy)가 9~12% 범위 내에서 향상된다고 합니다. 이는 모델에게 귀하의 관례(예: "매출에는 항상 환불 금액이 제외됨", "deleted_at IS NULL 조건을 필터링함")를 가르치기 때문입니다. 두 번째는 모델이 SQL을 작성하기 전에 **추론할 수 있는 여유를 주는 것(giving the model room to reason)**입니다. 즉, 복잡한 질문을 한 번에 쿼리로 내뱉는 대신 단계별로 분해하도록 하는 것입니다. 추론(reasoning) 기능이 없는 모델은 GROUP BY 컬럼을 누락하거나, 잘못된 테이블을 선택하거나, 필터를 누락하는 등, 문법적으로는 유효하지만 논리적으로는 틀린 SQL을 생성하는 것으로 악명이 높습니다.
4단계: 실행하기 (주의해서)
SQL이 생성되면 실행되어야 합니다. 모델이 생성한 것을 그대로 실행하는 단순한 방식은 보고용 도구에서 DELETE 명령어를 실행하는 참사를 초래할 수 있습니다. 운영 환경의 시스템은 이 단계를 가드레일(guardrails)로 감쌉니다:
- 모든 작업을 읽기 전용 (read-only) 데이터베이스 역할로 실행하여
INSERT,UPDATE,DELETE명령이 실행되지 않도록 합니다. - 실행 전 검증 (Validate before executing) — SQL을 파싱하여
SELECT가 아니거나 사용자가 봐서는 안 되는 테이블을 건드리는 모든 쿼리를 거부합니다. LIMIT및 **문장 타임아웃 (statement timeout)**을 강제하여 실수로 인한 크로스 조인 (cross join)이 데이터베이스를 다운시키지 않도록 합니다.
더 나은 시스템은 자기 수정 루프 (self-correction loop)를 추가합니다. 만약 쿼리에서 오류가 발생하면, 오류 메시지를 모델에 다시 전달하여 모델이 스스로 SQL을 수정하게 합니다. 존재하지 않는 컬럼인 revenue를 참조하는 쿼리가 column does not exist 오류를 발생시키면, 모델은 재시도하며 — 종종 두 번째 시도에서 amount_cents를 찾아냅니다.
다음은 해당 루프를 의사코드 (pseudocode)로 나타낸 최소한의 버전입니다:
def text_to_sql(question, schema, max_retries=2):
prompt = build_prompt(schema, question, few_shot_examples)
for attempt in range(max_retries + 1):
...
개발자가 직면하는 일반적인 문제점 (Common gotchas)
이러한 시스템을 배포하고 나면 몇 가지 패턴이 반복해서 나타납니다:
| 문제점 (Gotcha) | 발생하는 현상 | 완화 방법 |
|---|---|---|
| 스키마 과부하 (Schema overload) | 프롬프트에 300개의 컬럼을 쏟아부으면 모델이 혼란을 느끼고 비용이 상승함 | 질문당 관련 있는 테이블만 검색 (Retrieve) |
| ... |
핵심 교훈: 데이터와 스키마의 품질이 영리한 프롬프팅 (prompting)보다 더 중요합니다. 프롬프트가 아무리 훌륭하더라도, col1, col2, flag_3로 가득 찬 테이블보다는 제약 조건 (constraints)이 합리적이고 이름이 잘 지정된 깨끗한 테이블이 더 나은 SQL을 생성합니다. 만약 여러분의 스키마가 신입 엔지니어에게도 혼란스럽다면, 모델에게도 혼란스러울 것입니다.
핵심 요약 (Key takeaways)
Text-to-SQL은 마법도 아니고 블랙박스 (black box)도 아닙니다. 이는 4단계 파이프라인입니다: 스키마 컨텍스트 (schema context) 주입, 질문 파싱, SQL 생성, 가드레일 (guardrails)을 통한 실행. LLM은 중간의 두 단계만을 담당합니다. 여러분이 제공하는 스키마와 실행 단계에 둘러싸는 안전망 (safety net)이 여러분이 제어할 수 있는 부분이며, 시스템의 작동 여부를 결정하는 부분입니다.
가장 큰 두 가지 실패 모드(failure modes)는 스키마 연결(schema linking, 모델이 잘못된 테이블을 조인하는 경우)과 잠재적 논리 오류(silent logic errors, SQL은 유효하지만 비즈니스 정의가 틀린 경우)입니다. 첫 번째 문제는 집중적인 스키마 검색(schema retrieval)으로 대응하고, 두 번째 문제는 도메인 특화 예시(domain-specific examples)를 통해 해결합니다. 그리고 생성된 SQL은 반드시, 항상 타임아웃(timeout)이 설정된 읽기 전용 역할(read-only role)로 실행해야 합니다. 모델은 언젠가 반드시 여러분을 당황하게 만들 것이기 때문입니다.
이러한 방식으로 사용될 때, Text-to-SQL은 SQL을 아는 사람을 대체하지 않습니다. 대신 기술적 지식이 없는 팀원들에게 데이터로 향하는 안전한 진입로(on-ramp)를 제공하며, 나머지 우리에게는 질문에서 정답으로 가는 더 빠른 경로를 제공합니다. 이것이 바로 Draxlr와 같은 도구들이 의존하는 정확한 패턴입니다. 즉, AI가 생성한 SQL을 스키마 인식 계층(schema-aware layer) 및 읽기 전용 가드레일(read-only guardrails)과 결합하여, 실제로 실행되는 쿼리를 신뢰할 수 있도록 만드는 것입니다.
여러분의 차례
자연어 쿼리(natural-language query) 기능을 출시해 본 적이 있나요? 무엇이 가장 먼저 고장 났나요 — 스키마 연결(schema linking), 모호한 질문, 아니면 예상치 못한 무언가였나요? 만약 비즈니스 정의를 확실하게 잡아내는 프롬프팅 기술(prompting trick)을 발견했다면 댓글로 공유해 주세요. 함께 의견을 나누고 싶습니다.
출처: AWS: Build a robust text-to-SQL solution, Oracle: Natural Language to SQL Generation, RSL-SQL: Robust Schema Linking in Text-to-SQL, SQL-of-Thought: Multi-agentic Text-to-SQL.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기