본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 21. 14:40

영어에서 SQL로: LLM이 데이터베이스 스키마를 실제로 이해하는 방법

요약

이 글은 LLM 기반 Text-to-SQL 시스템이 데이터베이스 스키마를 해석하고 이해하는 내부 메커니즘인 '스키마 링킹(schema linking)' 과정을 분석합니다. 모델이 사용자의 자연어 질문을 데이터베이스 구조와 연결하는 방식과 테이블 선택 및 의미론적 추론의 중요성을 다룹니다.

핵심 포인트

  • LLM은 기본적으로 데이터베이스 구조를 알지 못하므로 프롬프트를 통해 스키마 정보를 주입해야 함
  • 스키마 링킹(Schema Linking)은 질문의 의도를 스키마의 구조적 요소와 연결하는 핵심 과정임
  • 효율적인 Text-to-SQL 구현을 위해서는 전체 스키마를 주입하는 대신 관련 테이블을 선별하는 전략이 필요함
  • 모델은 단순 키워드 매칭이 아닌 컬럼의 의미를 추론하여 조인(Join) 관계 등을 파악함

채팅 인터페이스에 "지난달 고객별 총 매출을 보여줘"라고 입력합니다. 1초 후, 여러분의 orders 및 customers 테이블을 대상으로 완벽하게 형식이 지정된 SQL 쿼리가 튀어나옵니다. 마치 마법처럼 느껴집니다. 하지만 마법이 아닙니다. 모델이 데이터베이스 스키마를 어떻게 읽고 해석하는지, 즉 그 메커니즘을 이해하고 나면 왜 Text-to-SQL 도구가 때로는 정답을 맞히고, 때로는 당혹스러울 정도로 틀리는지, 그리고 승률을 높이기 위해 무엇을 할 수 있는지 정확히 알게 될 것입니다. 이 포스트는 LLM 기반 Text-to-SQL 시스템의 스키마 이해 내부 메커니즘을 분석하며, 이러한 도구를 구축하거나 통합하는 개발자들을 위한 실질적인 조언을 제공합니다.

첫 번째 문제: LLM은 기본적으로 여러분의 데이터베이스를 알지 못합니다.
기본적으로 LLM은 여러분의 SaaS 제품에 subscriptions 테이블이 있다는 사실도, mrr이 billing_events에 들어있다는 사실도, 또는 UserID라고 되어 있는 단 하나의 레거시(legacy) 테이블을 제외하고 모든 곳에서 user_id를 소문자 스네이크 케이스(snake_case)로 저장한다는 사실도 알지 못합니다. 그 지식은 매번 모델의 프롬프트(prompt)에 주입되어야 합니다. 어떤 스키마 정보를 포함할지, 그리고 그것을 어떻게 형식화할지를 선택하는 과정을 스키마 링킹 (schema linking)이라고 부릅니다. 이는 모든 Text-to-SQL 파이프라인에서 가장 중요하면서도 가장 흔히 실수하는 부분입니다.

전형적인 스키마 주입은 다음과 같습니다:

-- 시스템 또는 사용자 프롬프트로 LLM에 전달됨:
Table : users
Columns : id ( integer , PK ), email ( text ), created_at ( timestamp ), plan ( text )
Table : orders
Columns : id ( integer , PK ), user_id ( integer , FK → users . id ), amount_cents ( integer ), status ( text ), created_at ( timestamp )
Table : subscriptions
Columns : id ( integer , PK ), user_id ( integer , FK → users . id ), plan_id ( integer ), started_at ( timestamp ), cancelled_at ( timestamp , nullable )

모델은 이를 읽고 데이터 모델에 대한 내부적인 이해를 구축합니다. 즉, 어떤 테이블이 존재하는지, 어떤 컬럼을 가지고 있는지, 서로 어떻게 연관되어 있는지, 그리고 어떤 데이터 타입을 기대해야 하는지를 파악합니다.

1단계: 스키마 파싱 (Schema Parsing) — 모델이 실제로 보는 것

사용자가 "지난달에 이탈한 고객이 누구인가요?"라고 물었을 때, LLM은 단순히 스키마에서 "이탈(churned)"이라는 단어를 검색하는 것이 아닙니다. 모델은 사용 가능한 컬럼들을 바탕으로 "이탈"이 무엇을 의미할지 추론합니다. 아마도 subscriptions를 통해 users와 조인(join)된 데이터 중, 지난 30일 이내에 해당하는 cancelled_at 타임스탬프일 가능성이 높다고 판단하는 식입니다. 질문의 의미론적 의도(semantic intent)를 스키마의 구조적 요소와 연결하는 이러한 추론 과정을 스키마 링킹 (schema linking)이라고 부릅니다. 여기에는 세 가지 하위 문제가 포함됩니다:

  1. 테이블 선택 (Table selection): 이 질문과 관련된 테이블은 무엇인가? 수백 개의 테이블이 있는 대규모 데이터베이스의 경우, 모델은 중요한 몇 개의 테이블로 범위를 좁혀야 합니다. 전체 스키마를 프롬프트(prompt)에 쏟아붓는 방식은 확장성이 없습니다. 300개의 테이블이 있는 데이터베이스는 모델의 유효한 컨텍스트 윈도우 (context window)를 쉽게 초과합니다.
  2. 컬럼 선택 (Column selection): 해당 테이블 내에서 어떤 컬럼이 필요한가? 여기서 명명 규칙 (naming)이 매우 중요해집니다. amt는 모호합니다. amount_cents는 명확합니다. ts는 쓸모가 없습니다. created_at은 유용합니다.
  3. 조인 추론 (Join inference): 관련 테이블들이 어떻게 연결되는가? 모델은 관계 그래프 (relationship graph)를 재구성해야 합니다. 단순히 컬럼 이름 패턴을 보고 추측하기보다는, 제공된 스키마 내의 명시적인 외래 키 (foreign key) 선언을 통해 재구성하는 것이 이상적입니다.

2단계: 컬럼 이름과 테이블 이름이 생각보다 중요한 이유

테스트를 하나 해보겠습니다. LLM에게 다음 두 가지 스키마를 주고 "지난주에 가입한 사용자는 몇 명인가요?"라고 물어보십시오.

스키마 A:
테이블: u
컬럼: uid, em, ts, pl

스키마 B:
테이블: users
컬럼: id, email, created_at, plan

스키마 B가 매번 승리합니다. 컬럼 이름에 포함된 자연어 토큰(email, created_at, plan)은 모델이 이해하는 단어와 깔끔하게 매핑됩니다. 약어는 이러한 매핑을 깨뜨립니다. IBM을 비롯한 여러 기관의 연구에 따르면, 테이블 설명, 컬럼 정의, 샘플 값을 포함한 풍부한 메타데이터 (metadata)가 제공된 스키마 인식 모델 (schema-aware models)이 가공되지 않은 DDL만 전달받은 모델보다 훨씬 더 뛰어난 성능을 보이는 것으로 나타났습니다.

실질적인 시사점: 좋은 명명 규칙 (naming conventions)에 투자하고, 이를 Text-to-SQL 파이프라인에 명시적으로 전달하십시오. 만약 기존의 짧은 컬럼 이름 (column names)을 그대로 사용해야 한다면, 설명을 추가하십시오.

3단계: LLM 이해도를 극대화하기 위한 스키마 구조화 방법
다음은 Text-to-SQL 시스템이 사용할 수 있는 잘 구성된 스키마 프롬프트 (schema prompt)의 예시입니다:

당신은 SQL 전문가입니다. 다음 스키마를 사용하여 사용자의 질문에 답하십시오.

스키마

테이블: users
설명: 등록된 사용자당 한 행.
컬럼:

  • id (integer, primary key)
  • email (text) — 사용자의 로그인 이메일
  • created_at (timestamp) — 계정이 생성된 시점
  • plan (text) — 현재 플랜: 'free', 'pro', 'enterprise'

테이블: orders
설명: 사용자가 수행한 구매.
컬럼:

  • id (integer, primary key)
  • user_id (integer) — 외래 키 (foreign key) → users.id
  • amount_cents (integer) — 센트 단위의 주문 총액 (달러로 변환하려면 100으로 나눔)
  • status (text) — 'pending', 'completed', 'refunded'
  • created_at (timestamp)

질문

'pro' 플랜 사용자 중 지난 30일 동안 3회 이상의 주문을 한 사용자는 누구입니까?

단순히 컬럼 이름 외에 포함된 사항을 주목하십시오:

  • 비즈니스 목적을 설명하는 테이블 설명 (Table descriptions)
  • 모호한 컬럼에 대한 인라인 주석 (Inline comments) (단위, 열거형 등)
  • 평이한 언어로 명시된 외래 키 (foreign key) 관계

이것이 평범한 Text-to-SQL 결과와 실제 운영 환경 (production)에서 실행할 수 있는 결과 사이의 차이입니다.

4단계: 대규모 스키마 문제 — 모든 것을 담을 수 없을 때
대부분의 실제 데이터베이스는 단일 프롬프트에 담고 싶은 것보다 훨씬 더 많은 테이블을 가지고 있습니다. 200개의 테이블 정의를 컨텍스트 윈도우 (context window)에 억지로 밀어 넣으면 두 가지 문제, 즉 비용과 정확도 문제가 발생합니다. 모델은 관련 없는 테이블들에 둘러싸여 있을 때 스키마 세부 사항에 집중하는 능력이 측정 가능한 수준으로 저하됩니다.

해결책은 검색 증강 스키마 선택 (retrieval-augmented schema selection)입니다. LLM을 호출하기 전에, 검색 단계에서 스키마를 필터링합니다:

Text-to-SQL 파이프라인에서 스키마 인식 RAG를 위한 의사 코드 (Pseudocode)

user_question = "지난 6개월 동안 플랜별 월간 매출을 보여줘"

1.

user_question의 임베딩 생성
question_embedding = embed ( user_question )

2. 테이블 + 컬럼 설명이 포함된 벡터 인덱스 검색

relevant_tables = vector_search ( index = schema_index , query = question_embedding , top_k = 5 )

3. 관련 테이블로만 구성된 집중형 스키마 프롬프트(Schema Prompt) 구축

schema_context = format_schema ( relevant_tables )

4. 집중형 프롬프트를 사용하여 LLM 호출

sql = llm . complete ( system_prompt = f " Use this schema: \n { schema_context } " , user_prompt = user_question )

Amazon의 RASL (Retrieval Augmented Schema Linking, 검색 증강 스키마 연결)과 같은 시스템은 수천 개의 테이블이 있는 데이터베이스를 처리하기 위해 이 접근 방식을 사용하며, 단순한 전체 스키마 주입 (Full-schema injection) 방식보다 훨씬 더 나은 정확도를 달성합니다. 핵심 통찰은 다음과 같습니다: 프롬프트 내의 적절한 스키마가 언제나 더 큰 모델보다 낫다는 것입니다.

단계 5: 외래 키 (Foreign Keys)는 비밀 병기입니다
조인 (Joins)은 텍스트-투-SQL (Text-to-SQL) 모델이 가장 눈에 띄게 실패하는 지점입니다. 모델이 usersorders 테이블이 모두 필요하다는 점을 올바르게 식별하더라도, 잘못된 조인 컬럼을 추측하여 깨진 쿼리를 생성할 수 있습니다. 해결책은 명확합니다: 스키마 프롬프트에 항상 외래 키 (Foreign Key) 관계를 포함하십시오.

다음 두 가지 접근 방식을 비교해 보십시오:

모호한 방식 (Vague):
Table: orders
Columns: id, user_id, amount_cents, created_at

명시적인 방식 (Explicit):
Table: orders
Columns:

  • id (integer, primary key)
  • user_id (integer) — foreign key → users.id; never null
  • amount_cents (integer)
  • created_at (timestamp)

외래 키가 선언되면, 모델은 신뢰할 수 있는 정확한 조인 (JOIN)을 생성합니다:

-- FK가 명시되었을 때 올바르게 생성됨:
SELECT u . email , COUNT ( o . id ) AS order_count , SUM ( o . amount_cents ) / 100 . 0 AS total_revenue FROM users u JOIN orders o ON o . user_id = u . id WHERE o . status = 'completed' AND o . created_at >= NOW () - INTERVAL '30 days' GROUP BY u . email ORDER BY total_revenue DESC ;

FK 힌트가 없으면, orders 테이블의 user_idusers 테이블의 id와 매핑된다는 것을 모델이 추측하기를 기다려야 합니다. 모델이 이를 맞히는 경우가 보통이지만,

개발자들이 흔히 하는 실수

설명이 없는 raw DDL (Data Definition Language) 전달하기. CREATE TABLE 문은 시작점일 뿐이지만, 이는 LLM이 아닌 데이터베이스 엔진에 최적화되어 있습니다. 주석과 설명을 추가하세요. -- 단순히 이것만 전달하지 마세요: CREATE TABLE ev ( id SERIAL PRIMARY KEY , uid INT , typ VARCHAR ( 50 ), ts TIMESTAMPTZ ); -- 다음과 같이 포함하세요: -- Table: events (alias: ev) -- Description: user activity events for product analytics -- uid → users.id, typ = 'click' | 'page_view' | 'signup' | 'purchase'

모든 쿼리에 대해 모든 테이블을 포함하기. 토큰 팽창 (Token-bloat)은 정확도를 떨어뜨립니다. 주입하기 전에 관련성에 따라 스키마를 필터링하세요.

열거형 (Enum) 값 무시하기. status와 같은 컬럼이 고정된 값 세트를 가진다면, 그 값들을 나열하세요. 모델은 'completed'가 유효한 상태이지, 'done'이나 'success'가 아니라는 것을 알아야 합니다.

샘플 데이터 부재. 테이블당 단 하나의 예시 행만 있어도 엣지 케이스 (Edge cases) — 특히 날짜 형식, 단위 관례, 그리고 열거형 철자 — 에 대한 정확도가 극적으로 향상됩니다.

핵심 요약 (Key Takeaways)
감탄을 자아내는 Text-to-SQL 도구와 실제 운영 환경(Production)에서 신뢰할 수 있는 도구 사이의 격차는 거의 항상 모델의 품질이 아닌 스키마의 품질에서 발생합니다. 요약하자면 다음과 같습니다:

  • 설명적인 테이블 및 컬럼 이름을 사용하세요. 약어 사용을 피하세요.
  • 스키마 프롬프트에 외래 키 (Foreign key) 관계를 명시적으로 선언하세요.
  • 모호한 필드(단위, 열거형, Null 허용 여부의 의미 등)에 대해 컬럼 설명을 포함하세요.
  • 대규모 데이터베이스의 경우 벡터 검색 (Vector search)을 사용하여 관련성에 따라 스키마를 필터링하세요.
  • 범주형 컬럼에 대해 샘플 값이나 열거형 리스트를 추가하세요.
  • 모델이 당신이 말하지 않은 것을 추론할 것이라고 절대 가정하지 마세요.

LLM은 놀라울 정도로 정교한 추론을 수행하고 있습니다. 즉, 당신의 영어 단어를 스키마 구조와 매칭하고, 조인 (Join)을 추론하며, 문법적으로 유효한 SQL을 단 한 번의 패스 (Pass)로 구축합니다. 적절한 스키마 컨텍스트 (Context)를 제공하면, 실제로 사용할 수 있는 쿼리를 제공할 것입니다.

Text-to-SQL 시스템을 구축하거나 제품에 통합해 보신 적이 있나요?

여러분이 스키마 주입 (Schema Injection) 문제를 어떻게 다루고 계신지 정말 궁금합니다. 여러분만의 접근 방식이 있다면 아래에 댓글로 남겨주세요. 특히 대규모 스키마 (Large-schema) 문제를 흥미로운 방식으로 해결하셨다면 더욱 좋습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0