RAG vs SQL: Por que tu agente de IA deberia consultar, no crear embeddings
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.