Ejecutar SQL Crudo
Cómo ejecutar consultas SQL crudas de forma segura con sentencias parametrizadas.
Visión General
Incluso con ORMs, SQL crudo a veces es necesario para consultas complejas, migraciones o optimización de rendimiento. Sin embargo, ejecutar SQL sin salvaguardas es una causa principal de vulnerabilidades de inyección SQL. Cómo ejecutar SQL de forma segura usando consultas parametrizadas en Python, JavaScript y Java.
Cuándo Usar
- For alternatives, see Connect to MySQL.
Usa este recurso cuando:
- Escribes consultas analíticas complejas que los ORMs no pueden expresar eficientemente
- Ejecutas migraciones de base de datos o comandos administrativos
- Optimizas rendimiento con capacidades SQL específicas de la base de datos
Solución
Python
import psycopg2
conn = psycopg2.connect(host="localhost", database="mydb", user="user", password="pass")
cursor = conn.cursor()
# Consulta parametrizada segura
cursor.execute("SELECT * FROM users WHERE email = %s AND active = %s", (email, True))
rows = cursor.fetchall()
# Inserción segura con RETURNING
cursor.execute(
"INSERT INTO users (email, role) VALUES (%s, %s) RETURNING id",
(email, role)
)
user_id = cursor.fetchone()[0]
conn.commit()
cursor.close()
conn.close()
JavaScript
const { Pool } = require('pg');
const pool = new Pool({ /* config */ });
// Consulta parametrizada segura
async function findUser(email) {
const result = await pool.query(
'SELECT * FROM users WHERE email = $1 AND active = $2',
[email, true]
);
return result.rows;
}
// Inserción segura con RETURNING
async function createUser(email, role) {
const result = await pool.query(
'INSERT INTO users (email, role) VALUES ($1, $2) RETURNING id',
[email, role]
);
return result.rows[0].id;
}
Java
import java.sql.*;
public class RawSQL {
public void findUser(Connection conn, String email) throws SQLException {
String sql = "SELECT * FROM users WHERE email = ? AND active = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, email);
stmt.setBoolean(2, true);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("name"));
}
}
}
public int createUser(Connection conn, String email, String role) throws SQLException {
String sql = "INSERT INTO users (email, role) VALUES (?, ?) RETURNING id";
try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, email);
stmt.setString(2, role);
stmt.executeUpdate();
try (ResultSet keys = stmt.getGeneratedKeys()) {
keys.next();
return keys.getInt(1);
}
}
}
}
Explicación
Las consultas parametrizadas (prepared statements) separan la lógica SQL de los datos. La base de datos compila la plantilla SQL una vez y vincula los valores en tiempo de ejecución, haciendo la inyección imposible. En Python, %s es un placeholder, no un format string. En JavaScript, $1, $2 son parámetros posicionales. En Java, ? es el placeholder de JDBC. Ninguno de estos concatena input de usuario en el string SQL.
Variantes
| Tecnología | Enfoque | Notas |
|---|---|---|
| Python | SQLAlchemy text() | SQL crudo dentro del ORM con parámetros vinculados |
| JavaScript | knex.raw() | Query builder con SQL crudo y bindings |
| Java | Jdbi | API directa sobre JDBC con binding de parámetros |
Lo que funciona
- Nunca concatenes input de usuario en strings SQL; usa siempre consultas parametrizadas
- Usa
RETURNING(PostgreSQL) ogetGeneratedKeys()(JDBC) en lugar deSELECT MAX(id) - Envuelve múltiples sentencias en transacciones con rollback apropiado ante errores
- Valida y haz whitelist de nombres de tabla/columna cuando deban ser en vivo
- Registra tiempos de ejecución de SQL para detectar consultas lentas y patrones N+1
Errores Comunes
- Usar f-strings de Python, template literals de JS o concatenación
+en Java para SQL - Asumir que los ORMs son siempre seguros;
.query("..." + input)sigue siendo vulnerable - Sanitizar input con regex en lugar de usar consultas parametrizadas
- Olvidar hacer commit de transacciones, dejando datos en estado inconsistente
- Usar
Statementen lugar dePreparedStatementen Java
Buenas prácticas adicionales
-
Usa
EXPLAIN ANALYZEpara validar planes de ejecución. El SQL crudo omite las optimizaciones del ORM. Verifica siempre el plan de ejecución para tablas grandes. -
Configura
statement_timeoutpara consultas crudas. Previene que queries descontrolados consuman recursos:
SET statement_timeout = '30s';
- Registra consultas lentas. Rastrea el tiempo de ejecución del SQL crudo para identificar regresiones de rendimiento:
import time
start = time.monotonic()
cursor.execute("SELECT * FROM large_table WHERE ...")
elapsed = time.monotonic() - start
if elapsed > 0.5:
logger.warning(f"Query lenta tomó {elapsed:.2f}s")
-
Usa pools de conexiones para SQL crudo. Crear una nueva conexión por query es costoso. Usa
psycopg2.poolo el pooling integrado de SQLAlchemy. -
Quota identificadores cuando sea necesario. Usa el módulo
psycopg2.sqlpara quoting seguro de identificadores:
from psycopg2 import sql
query = sql.SQL("SELECT * FROM {} WHERE {} = %s").format(
sql.Identifier("users"),
sql.Identifier("email")
)
cursor.execute(query, ("alice@example.com",))
Tips de Rendimiento
-
Usa
COPYpara inserts masivos en PostgreSQL. Es 10-100x más rápido queINSERTindividuales. -
Usa
execute_valuespara batch inserts en Python. Reduce round-trips enviando múltiples filas en una sentencia:
from psycopg2.extras import execute_values
execute_values(
cursor,
"INSERT INTO users (email, name) VALUES %s",
[("alice@example.com", "Alice"), ("bob@example.com", "Bob")]
)
- Usa cursores server-side para result sets grandes. Evita cargar millones de filas en memoria:
cursor = conn.cursor("server_side_cursor")
cursor.execute("SELECT * FROM large_table")
for row in cursor:
process(row)
cursor.close()
- Prefiere
EXISTSsobreCOUNT(*)para verificaciones de existencia.EXISTSdeja de escanear al encontrar la primera coincidencia:
SELECT EXISTS(SELECT 1 FROM users WHERE email = 'alice@example.com');
- Usa
PREPAREpara queries repetidas frecuentemente. PostgreSQL cachea el plan de consulta, reduciendo overhead de parse para ejecuciones repetidas.”
Preguntas frecuentes
¿Es seguro cursor.execute(f"SELECT * FROM {table}")?
No. Los nombres de tabla y columna no pueden parametrizarse en la mayoría de drivers. Si necesitas nombres de tabla en vivo, haz whitelist contra un conjunto conocido de nombres válidos.
¿Puedo usar consultas parametrizadas para cláusulas IN?
La mayoría de drivers no soportan IN (%s) con una lista. Usa extensiones específicas del driver: ANY($1) en PostgreSQL, genera placeholders dinámicamente en Python/Java, o usa find_in_set en MySQL.
¿Debería evitar SQL crudo por completo y usar solo ORMs?
No necesariamente. Los ORMs sobresalen en CRUD pero luchan con agregaciones complejas, funciones de ventana y optimizaciones específicas de la base de datos. Usa SQL crudo para estos casos, pero parametriza siempre los inputs.
Python con SQLAlchemy text()
from sqlalchemy import create_engine, text
engine = create_engine("postgresql://user:pass@localhost/mydb")
with engine.connect() as conn:
# SQL crudo parametrizado dentro de SQLAlchemy
result = conn.execute(
text("SELECT * FROM users WHERE email = :email AND active = :active"),
{"email": "alice@example.com", "active": True}
)
for row in result:
print(row.name, row.email)
# Transacción con SQL crudo
conn.execute(
text("INSERT INTO audit_log (action, user_id) VALUES (:action, :user_id)"),
{"action": "login", "user_id": 1}
)
conn.commit()
JavaScript con knex.raw()
const knex = require('knex')({
client: 'pg',
connection: 'postgresql://user:pass@localhost/mydb'
});
// SQL crudo con bindings
const users = await knex.raw(
'SELECT * FROM users WHERE email = ? AND active = ?',
['alice@example.com', true]
);
// SQL crudo en una cadena de query builder
const activeUsers = await knex('users')
.whereRaw('created_at > NOW() - INTERVAL ? DAYS', [30])
.select('id', 'email');
Manejo seguro de cláusulas IN
# Python: generar placeholders dinámicamente
emails = ['alice@example.com', 'bob@example.com']
placeholders = ','.join(['%s'] * len(emails))
cursor.execute(
f"SELECT * FROM users WHERE email IN ({placeholders})",
emails
)
# PostgreSQL: usar ANY() con un array
cursor.execute(
"SELECT * FROM users WHERE email = ANY(%s)",
(emails,)
)
// JavaScript: usar ANY() en PostgreSQL
const result = await pool.query(
'SELECT * FROM users WHERE email = ANY($1::text[])',
[emails]
);
// Java: construir PreparedStatement con placeholders dinámicos
List<String> emails = List.of("alice@example.com", "bob@example.com");
String placeholders = String.join(",", Collections.nCopies(emails.size(), "?"));
String sql = "SELECT * FROM users WHERE email IN (" + placeholders + ")";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
for (int i = 0; i < emails.size(); i++) {
stmt.setString(i + 1, emails.get(i));
}
ResultSet rs = stmt.executeQuery();
}
Whitelist de nombres de tabla/columna
ALLOWED_TABLES = {"users", "orders", "products"}
ALLOWED_COLUMNS = {"id", "name", "email", "amount", "status"}
def safe_query(table_name, column_name, value):
if table_name not in ALLOWED_TABLES:
raise ValueError(f"Tabla inválida: {table_name}")
if column_name not in ALLOWED_COLUMNS:
raise ValueError(f"Columna inválida: {column_name}")
cursor.execute(
f"SELECT * FROM {table_name} WHERE {column_name} = %s",
(value,)
)
return cursor.fetchall()
Recursos Relacionados
Conectar a MySQL
Cómo conectar a bases de datos MySQL en Python, JavaScript y Java.
RecipeConectar a PostgreSQL
Cómo conectar a bases de datos PostgreSQL en Python, JavaScript y Java.
RecipeConectar a Redis
Cómo conectarse a Redis y realizar operaciones básicas en Python, JavaScript y Java.
RecipeEscapar Entidades HTML
Cómo escapar entidades HTML para prevenir ataques XSS en Python, Java y JavaScript.
RecipeSanitizar Input de Usuario
Cómo sanitizar y validar input de usuario en Python, Java y JavaScript para prevenir ataques de inyección.
Recipe[ES] Use ORM for CRUD
Guía práctica de operaciones CRUD con ORMs: crea, lee, actualiza y elimina datos usando SQLAlchemy, Prisma y Hibernate en Python, JavaScript y Java.