Ejecutar SQL Crudo
Cómo ejecutar consultas SQL crudas de forma segura con sentencias parametrizadas.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
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.”
Related Resources
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...
- Python con SQLAlchemy text()
- python
- JavaScript con knex.raw()
- javascript
- Manejo seguro de cláusulas IN
- python
- Whitelist de nombres de tabla/columna
- python