RAG vs SQL: Por que tu agente de IA deberia consultar, no crear embeddings
Michael San Martim · 2026-04-22
Cada semana, otro equipo anuncia que esta “construyendo RAG para su data warehouse.” Fragmentan sus tablas en vectores, los almacenan en Pinecone y le piden a un LLM que responda preguntas de negocio. Funciona para documentos. Falla catastroficamente para datos estructurados.
El problema: RAG sobre tablas
Esto es lo que pasa cuando usas RAG para datos estructurados:
# The RAG approach to answering "What was revenue in Q3?"
# Step 1: Chunk your tables into textchunks = []for row in orders_table: chunks.append(f"Order {row.id}: customer {row.customer}, " f"amount ${row.amount}, date {row.date}")
# Step 2: Embed chunksembeddings = openai.embed(chunks) # $0.0001 per chunk, 1M rows = $100
# Step 3: Store in vector DBpinecone_index.upsert(vectors=embeddings) # $70/month for 1M vectors
# Step 4: At query time, retrieve "relevant" chunksquery_embedding = openai.embed("What was revenue in Q3?")results = pinecone_index.query(query_embedding, top_k=20)
# Step 5: Ask LLM to synthesizeanswer = openai.chat( f"Based on these records: {results}\n" f"Answer: What was revenue in Q3?")# Returns: "Based on the retrieved records, Q3 revenue appears to be# approximately $2.3M" (actual answer: $4.7M)Por que falla:
- La recuperacion pierde filas. La similitud vectorial encuentra fragmentos “relevantes”, no TODAS las filas que coinciden. Si tienes 50,000 ordenes del Q3, recuperaste 20.
- La agregacion es imposible. No puedes SUM, AVG o COUNT desde 20 muestras aleatorias.
- Los datos se desactualizan. Necesitas re-crear embeddings cada vez que los datos cambian.
- Es caro. Embeddings de millones de filas + hosting de vector DB + tokens LLM para contexto.
- Alucina con confianza. El LLM ve 20 filas y extrapola.
El enfoque correcto: SQL
La misma pregunta respondida con SQL:
SELECT SUM(amount) AS revenueFROM ordersWHERE created_at >= '2024-07-01' AND created_at < '2024-10-01'Resultado: $4,712,843.00 — exacto, instantaneo, gratis.
MCP + SQL con DataSpoc Lens
DataSpoc Lens expone tu data lake cómo un servidor MCP. Los agentes de IA ejecutan consultas SQL reales en lugar de adivinar a partir de embeddings.
Configuracion
pip install dataspoc-lens[mcp]dataspoc-lens add-bucket s3://company-lakedataspoc-lens mcpComo funcióna el agente
from dataspoc_lens import LensClient
client = LensClient()
# Agent discovers what data existstables = client.list_tables()# ['raw.postgres.orders', 'raw.postgres.customers', 'raw.postgres.products']
# Agent inspects schemaschema = client.get_schema("raw.postgres.orders")# [{"name": "id", "type": "INTEGER"},# {"name": "customer_id", "type": "INTEGER"},# {"name": "amount", "type": "DECIMAL"},# {"name": "created_at", "type": "TIMESTAMP"}, ...]
# Agent writes and executes SQLresult = client.query(""" SELECT SUM(amount) AS revenue FROM raw.postgres.orders WHERE created_at >= '2024-07-01' AND created_at < '2024-10-01'""")# [{"revenue": 4712843.00}]Sin embeddings. Sin vector DB. Sin alucinaciones. Datos reales, respuestas reales.
Comparación de código lado a lado
Pipeline RAG (lo que los equipos estan construyendo)
# ingest.py — runs daily, takes 45 minutes for 1M rowsimport pandas as pdfrom langchain.text_splitter import RecursiveCharacterTextSplitterfrom langchain_openai import OpenAIEmbeddingsfrom langchain_pinecone import PineconeVectorStore
df = pd.read_parquet("s3://company-lake/raw/postgres/orders/")
# Convert rows to text chunksdocuments = []for _, row in df.iterrows(): text = f"Order {row['id']}: {row['customer_name']} purchased " text += f"${row['amount']:.2f} on {row['created_at']}" documents.append(text)
# Split and embedsplitter = RecursiveCharacterTextSplitter(chunk_size=500)chunks = splitter.create_documents(documents)embeddings = OpenAIEmbeddings()vectorstore = PineconeVectorStore.from_documents(chunks, embeddings, index_name="orders")
# query.py — at query timeretriever = vectorstore.as_retriever(search_kwargs={"k": 50})docs = retriever.get_relevant_documents("What was Q3 revenue?")# Gets 50 random-ish order records out of 250,000 Q3 ordersPipeline SQL (lo que deberias construir)
# query.py — that's it, no ingest step neededfrom dataspoc_lens import LensClient
client = LensClient()result = client.query("SELECT SUM(amount) FROM orders WHERE quarter = 'Q3-2024'")# Scans all 250,000 rows in 0.3 seconds, returns exact answerLa comparación
| Dimension | RAG sobre tablas | MCP + SQL |
|---|---|---|
| Precision | ~60% (pierde filas, no puede agregar) | 99%+ (computo exacto) |
| Velocidad | 3-8 segundos (embedding query + recuperar + LLM) | 0.1-2 segundos (ejecucion SQL) |
| Frescura | Desactualizado hasta re-crear embeddings (horas/dias) | Tiempo real (consulta datos en vivo) |
| Costo por consulta | $0.02-0.10 (embedding + tokens LLM) | $0.001 (solo computo DuckDB) |
| Infra mensual | $200+ (vector DB + embedding API) | $0 (DuckDB es gratis) |
| Auditabilidad | ”Basado en el contexto recuperado…” | Consulta SQL exacta, reproducible |
| Agregaciones | Imposible (SUM de 20 filas != SUM de 250K) | Nativo (eso es lo que SQL hace) |
| Joins | No puede unir a traves de embeddings | Joins multi-tabla nativos |
| Actualizaciones | Re-crear embeddings de todo el dataset | Automatico (lee el Parquet mas reciente) |
Cuando RAG ES la opcion correcta
RAG es excelente para documentos no estructurados:
- Reportes PDF, contratos legales, tickets de soporte
- Busqueda de documentación
- Bases de conocimiento con contenido en prosa
- Cualquier dato donde la busqueda por palabra clave/semantica es el patrón de acceso
La regla es simple:
Tablas y números = SQL. Documentos y prosa = RAG.
Una arquitectura hibrida
Para la mayoria de las empresas, la mejor arquitectura combina ambos:
┌─────────────────────────────────────────────────┐│ AI Agent │├─────────────────────────────────────────────────┤│ ││ Structured data? ──► DataSpoc Lens (MCP + SQL) ││ ││ Unstructured docs? ──► RAG (vector search) ││ │└─────────────────────────────────────────────────┘from dataspoc_lens import LensClientfrom langchain_pinecone import PineconeVectorStore
lens = LensClient()vectorstore = PineconeVectorStore(index_name="docs")
def route_question(question: str) -> str: """Route to SQL for data questions, RAG for document questions.""" data_keywords = ["revenue", "count", "average", "total", "growth", "last month", "quarterly", "how many", "top 10"]
if any(kw in question.lower() for kw in data_keywords): # SQL path — exact answers from structured data result = lens.ask(question) return result["answer"] else: # RAG path — semantic search over documents docs = vectorstore.similarity_search(question, k=5) return synthesize_from_docs(docs, question)El costo de equivocarse
Un ejemplo real de una startup Serie B:
| Metrica | Su configuración RAG | Despues de cambiar a SQL |
|---|---|---|
| Costo mensual | $2,400 (Pinecone + embeddings OpenAI) | $0 (DuckDB sobre Parquet) |
| Precision de consultas | 58% (validado contra números reales) | 99.7% |
| Frescura de datos | 6 horas de retraso (re-embedding diario) | Tiempo real |
| Mantenimiento | 2 ingenieros tiempo parcial | Cero (autoservicio) |
| Tiempo de respuesta | 4.2 segundos promedio | 0.8 segundos promedio |
Estaban gastando $2,400/mes para obtener respuestas incorrectas lentamente.
Comenzando
# If your data is already in Parquet/S3:pip install dataspoc-lens[mcp]dataspoc-lens add-bucket s3://your-bucketdataspoc-lens mcp # Start MCP server for AI agents
# If your data is in Postgres/MySQL:pip install dataspoc-pipedataspoc-pipe init && dataspoc-pipe add postgres --tables ordersdataspoc-pipe run # Extracts to Parquet
# Then query with SQLdataspoc-lens query "SELECT SUM(amount) FROM raw.postgres.orders WHERE created_at > '2024-01-01'"Deja de crear embeddings de tus tablas. Comienza a consultarlas.