beginner Por Mathias Paulenko

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

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íaEnfoqueNotas
PythonSQLAlchemy text()SQL crudo dentro del ORM con parámetros vinculados
JavaScriptknex.raw()Query builder con SQL crudo y bindings
JavaJdbiAPI directa sobre JDBC con binding de parámetros

Lo que funciona

  1. Nunca concatenes input de usuario en strings SQL; usa siempre consultas parametrizadas
  2. Usa RETURNING (PostgreSQL) o getGeneratedKeys() (JDBC) en lugar de SELECT MAX(id)
  3. Envuelve múltiples sentencias en transacciones con rollback apropiado ante errores
  4. Valida y haz whitelist de nombres de tabla/columna cuando deban ser en vivo
  5. Registra tiempos de ejecución de SQL para detectar consultas lentas y patrones N+1

Errores Comunes

  1. Usar f-strings de Python, template literals de JS o concatenación + en Java para SQL
  2. Asumir que los ORMs son siempre seguros; .query("..." + input) sigue siendo vulnerable
  3. Sanitizar input con regex en lugar de usar consultas parametrizadas
  4. Olvidar hacer commit de transacciones, dejando datos en estado inconsistente
  5. Usar Statement en lugar de PreparedStatement en Java

Buenas prácticas adicionales

  1. Usa EXPLAIN ANALYZE para validar planes de ejecución. El SQL crudo omite las optimizaciones del ORM. Verifica siempre el plan de ejecución para tablas grandes.

  2. Configura statement_timeout para consultas crudas. Previene que queries descontrolados consuman recursos:

SET statement_timeout = '30s';
  1. 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")
  1. Usa pools de conexiones para SQL crudo. Crear una nueva conexión por query es costoso. Usa psycopg2.pool o el pooling integrado de SQLAlchemy.

  2. Quota identificadores cuando sea necesario. Usa el módulo psycopg2.sql para 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

  1. Usa COPY para inserts masivos en PostgreSQL. Es 10-100x más rápido que INSERT individuales.

  2. Usa execute_values para 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")]
)
  1. 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()
  1. Prefiere EXISTS sobre COUNT(*) para verificaciones de existencia. EXISTS deja de escanear al encontrar la primera coincidencia:
SELECT EXISTS(SELECT 1 FROM users WHERE email = 'alice@example.com');
  1. Usa PREPARE para 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...
Python con SQLAlchemy text()
python
JavaScript con knex.raw()
javascript
Manejo seguro de cláusulas IN
python
Whitelist de nombres de tabla/columna
python