El Viaje de la Consulta SQL: Reescritura de Queries

📚 El Viaje de la Consulta SQL (4 of 4)
  1. 1. Visión General
  2. 2. Conexiones y Comunicación
  3. 3. Parsing y Análisis
  4. 4. Reescritura de Queries Estás aquí
Reescritura de Queries

En el artículo anterior, exploramos cómo PostgreSQL transforma texto SQL en un Query Tree validado mediante análisis sintáctico y semántico. Al final de ese viaje, PostgreSQL sabe que tus tablas existen, tus columnas son válidas, tus tipos coinciden y tu query tiene sentido.

Pero antes de que el planificador pueda determinar cómo ejecutar tu query, hay un paso más de transformación crítico: la reescritura de queries.

Déjame mostrarte por qué esto es importante. Cuando escribes esta simple query:

SELECT name FROM user_view WHERE active = true;

PostgreSQL no necesariamente ejecuta lo que escribiste. Si tu base de datos tiene vistas, políticas de seguridad a nivel de fila u otras reglas de reescritura, tu query podría ser reescrita a algo como:

SELECT u.name FROM users u
JOIN departments d ON u.dept_id = d.id
WHERE u.status = 'active' AND d.active = true
  AND u.region = current_user_region();  -- ¡Añadido automáticamente!

Esta transformación ocurriría si user_view fuera una vista que une múltiples tablas, y tuvieras políticas de seguridad a nivel de fila que restringen el acceso por región. El reescritor expandiría automáticamente la definición de la vista y aplicaría las políticas de seguridad—todo de forma transparente.

El reescritor funciona como el motor de transformación de PostgreSQL, implementando abstracciones sofisticadas de base de datos mientras preserva la semántica de las queries. En este artículo, exploraremos cómo funciona el sistema de reglas de PostgreSQL, cómo puedes crear reglas de reescritura personalizadas para lógica de negocio, cómo se implementan las vistas como reglas de transformación, y cómo se aplican automáticamente las políticas de seguridad a nivel de fila (RLS) durante la reescritura.

Antes de profundizar en la mecánica de las reglas y políticas de seguridad, entendamos dónde encaja el reescritor en el flujo general de procesamiento de queries de PostgreSQL.

El Pipeline del Reescritor

El reescritor recibe el Query Tree validado del analizador y lo transforma antes de pasarlo al planificador:

digraph RewriteProcess {
    rankdir=TB;
    node [shape=box, style="rounded,filled", fillcolor=lightblue];

    // Input
    ParserAnalyzer [label="Parser/Analizador", fillcolor=lightgreen];
    QueryTree [label="Query Tree", shape=cylinder, fillcolor=lightyellow];

    // Rewriter
    Rewriter [label="Reescritor", fillcolor=orange, fontsize=14];

    subgraph cluster_rewriter {
        label="Proceso de Reescritura";
        style=filled;
        fillcolor=lightgray;

        ApplyRules [label="Aplicar Reglas\n(incluyendo Vistas)"];
        ApplyRLS [label="Aplicar Seguridad\na Nivel de Fila"];
    }

    RewrittenQueries [label="Query Trees Reescritos", shape=cylinder, fillcolor=lightyellow];

    // Output
    Planner [label="Planificador", fillcolor=lightgreen];

    // Main flow
    ParserAnalyzer -> QueryTree;
    QueryTree -> Rewriter;
    Rewriter -> ApplyRules;
    ApplyRules -> ApplyRLS;
    ApplyRLS -> RewrittenQueries;
    RewrittenQueries -> Planner;
}

El diagrama muestra el viaje completo a través de la reescritura. El Parser/Analizador produce un Query Tree validado—esto es lo que cubrimos en el artículo anterior. Este Query Tree entra en el reescritor, que realiza dos transformaciones principales:

Primero, Aplicar Reglas expande las vistas en sus tablas subyacentes y aplica reglas de reescritura personalizadas. Cuando consultas una vista, este paso reemplaza la referencia a la vista con la definición de query real. Las reglas personalizadas también pueden añadir operaciones adicionales o reemplazar queries por completo.

Segundo, Aplicar Seguridad a Nivel de Fila añade predicados de seguridad para hacer cumplir políticas de acceso. Si tienes políticas RLS definidas, este paso inyecta automáticamente condiciones WHERE que restringen qué filas pueden ver o modificar los usuarios.

El resultado son uno o más Query Trees Reescritos que fluyen al planificador. Nota que el reescritor puede producir múltiples Query Trees desde una sola entrada—esto ocurre cuando reglas personalizadas añaden queries adicionales junto a la original.

Todo esto sucede a través del sistema de reglas de PostgreSQL, implementado en la función QueryRewrite() (en src/backend/rewrite/rewriteHandler.c). ¿Pero qué son exactamente estas “reglas” que seguimos mencionando? Exploremos cómo funcionan.

El Sistema de Reglas: El Motor de Transformación de PostgreSQL

El sistema de reglas de PostgreSQL proporciona la base para la reescritura de queries. Las reglas son transformaciones almacenadas que PostgreSQL aplica automáticamente cuando se cumplen ciertas condiciones. Se almacenan en el catálogo del sistema pg_rewrite y contienen todo lo necesario para transformar queries.

Cada regla tiene varios componentes clave:

Eventos de Regla especifican cuándo debe activarse la regla. PostgreSQL soporta reglas para operaciones SELECT, INSERT, UPDATE y DELETE. Una regla se activa cuando su relación objetivo es accedida con el tipo de operación correspondiente.

Tipos de Regla se dividen en dos categorías:

  • Reglas INSTEAD reemplazan completamente la query original. Cuando se activa una regla INSTEAD, PostgreSQL ejecuta la acción de la regla en lugar de la operación original.
  • Reglas ALSO añaden queries adicionales junto a la original. La operación original aún se ejecuta, pero la regla añade acciones extra.

Condiciones de Regla determinan si una regla debe ser aplicada. Las reglas pueden ser incondicionales (siempre se aplican) o condicionales (solo se aplican cuando se cumplen criterios específicos).

Acciones de Regla contienen las queries de reemplazo o adicionales que PostgreSQL ejecuta. Estas acciones se almacenan como Query Trees en el catálogo y pueden incluir operaciones complejas multi-tabla.

Puedes explorar las reglas en tu base de datos consultando el catálogo pg_rewrite:

SELECT
    rulename,
    ev_type,
    ev_class::regclass as table_name,
    is_instead,
    ev_qual IS NOT NULL as has_condition
FROM pg_rewrite
WHERE ev_class = 'your_table'::regclass;

Con estos fundamentos establecidos, veamos el sistema de reglas en acción creando una regla personalizada.

Creando Reglas de Reescritura Personalizadas

PostgreSQL te permite crear reglas personalizadas manualmente para comportamiento sofisticado. Las reglas pueden ser INSTEAD (reemplazar la operación original) o ALSO (añadir operaciones adicionales), y pueden ser condicionales (solo activarse cuando se cumplen ciertos criterios) o incondicionales (siempre aplicarse).

Déjame mostrarte un ejemplo práctico usando una regla ALSO condicional: un sistema de auditoría que rastrea cambios a usuarios administradores. Esta regla ALSO se ejecutará además del UPDATE original—la actualización aún ocurre, pero automáticamente registramos cambios a cuentas de administrador.

-- Crear una tabla de auditoría
CREATE TABLE user_audit (
    audit_id SERIAL PRIMARY KEY,
    user_id INT,
    action TEXT,
    old_values JSONB,
    new_values JSONB,
    audit_time TIMESTAMP DEFAULT NOW()
);

-- Crear una regla condicional que solo audita actualizaciones a usuarios admin
CREATE RULE audit_admin_updates AS
    ON UPDATE TO users
    WHERE (OLD.role = 'admin' OR NEW.role = 'admin')
    DO ALSO
        INSERT INTO user_audit (user_id, action, old_values, new_values)
        VALUES (OLD.id, 'UPDATE',
                row_to_json(OLD),
                row_to_json(NEW));

Esta regla tiene varias propiedades interesantes:

  • Evento: Se activa en operaciones UPDATE a la tabla users
  • Condición: WHERE (OLD.role = 'admin' OR NEW.role = 'admin') - solo se activa cuando usuarios admin están involucrados
  • Acción: INSERT en la tabla de auditoría
  • Tipo: Regla ALSO - se ejecuta además del UPDATE original (no en su lugar)

Veámosla en acción:

-- Esta actualización NO activará la regla de auditoría
UPDATE users SET email = 'new@email.com' WHERE role = 'customer';
-- Resultado: email actualizado, sin registro de auditoría creado
-- Esta actualización SÍ activará la regla de auditoría
UPDATE users SET email = 'admin@company.com' WHERE role = 'admin';
-- Resultado: email actualizado Y registro de auditoría creado automáticamente

Cuando la regla se activa, PostgreSQL reescribe tu única sentencia UPDATE en dos operaciones:

-- Original
UPDATE users SET email = 'admin@company.com' WHERE role = 'admin';

-- Reescrito (representación simplificada)
BEGIN;
  UPDATE users SET email = 'admin@company.com' WHERE role = 'admin';
  INSERT INTO user_audit (user_id, action, old_values, new_values)
  VALUES (OLD.id, 'UPDATE', row_to_json(OLD), row_to_json(NEW));
COMMIT;

El reescritor evalúa la condición de la regla durante el procesamiento de la query y añade el INSERT de auditoría automáticamente cuando usuarios admin son afectados. Esto ocurre completamente de forma transparente—tu aplicación solo emite sentencias UPDATE, y PostgreSQL asegura que los cambios a administradores sean registrados.

Las reglas personalizadas proveen capacidades poderosas para implementar lógica de negocio a nivel de base de datos—auditoría, validación de datos, desnormalización automática, y más. Sin embargo, son menos comunes en aplicaciones modernas porque los triggers a menudo proporcionan funcionalidad similar con más flexibilidad. La diferencia clave es que las reglas funcionan a través de reescritura de queries (transformando SQL antes de la ejecución) mientras que los triggers ejecutan código procedural durante la ejecución.

Ahora que hemos visto reglas personalizadas, veamos cómo PostgreSQL usa el sistema de reglas para implementar vistas.

Cómo las Vistas se Convierten en Reglas

Este es el concepto clave: cuando creas una vista en PostgreSQL, no estás almacenando datos—estás almacenando una regla de transformación. Internamente, PostgreSQL no solo crea el objeto vista sino que también almacena una regla de reescritura que le indica “cuando alguien consulte esta vista, reemplázala con esta query subyacente.”

Cuando creas esta vista:

CREATE VIEW active_users AS
SELECT id, name, email FROM users WHERE active = true;

PostgreSQL internamente crea una regla de reescritura equivalente a:

CREATE RULE "_RETURN" AS
    ON SELECT TO active_users
    DO INSTEAD
        SELECT id, name, email FROM users WHERE active = true;

La regla dice: “En SELECT desde active_users, haz INSTEAD esta otra query.” De esta manera, las vistas actúan como azúcar sintáctico para reglas INSTEAD en operaciones SELECT—una forma conveniente de crear reglas de reescritura sin escribir CREATE RULE explícitamente.

Puedes ver esta regla tú mismo:

SELECT
    rulename,
    ev_type,
    ev_class::regclass as table_name,
    is_instead
FROM pg_rewrite
WHERE ev_class = 'active_users'::regclass;

Esta query revela:

  • rulename: _RETURN (nombre estándar de PostgreSQL para reglas de vista)
  • ev_type: 1 (evento SELECT)
  • table_name: active_users
  • is_instead: true (regla INSTEAD—reemplaza la query original)

Cuando consultas la vista, PostgreSQL aplica esta regla y reescribe tu consulta para usar la tabla base en su lugar.

Hemos visto cómo el sistema de reglas maneja transformaciones personalizadas y expansión de vistas. Pero el reescritor tiene otra responsabilidad crítica: hacer cumplir la seguridad de datos a través de políticas de acceso a nivel de fila.

Seguridad Automática: Políticas de Seguridad a Nivel de Fila

Aquí es donde se pone realmente interesante. El sistema de seguridad a nivel de fila (RLS) de PostgreSQL se integra directamente en el reescritor de queries. Cuando habilitas RLS en una tabla, PostgreSQL añade automáticamente condiciones de seguridad a cada query—no tienes que recordar verificar permisos en tu código de aplicación.

El reescritor aplica políticas de seguridad a través de la función get_row_security_policies() (en src/backend/rewrite/rowsecurity.c), que examina cada referencia de tabla y determina qué políticas aplican basándose en el usuario actual y el tipo de operación.

Déjame mostrarte un ejemplo práctico:

CREATE TABLE documents (
    id INT PRIMARY KEY,
    title TEXT,
    content TEXT,
    owner_id INT,
    department TEXT
);

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_documents ON documents
FOR ALL TO employees
USING (owner_id = current_user_id() OR department = current_user_department());

Esta política dice: “Los usuarios solo pueden ver documentos que poseen o que pertenecen a su departamento.”

Ahora cuando un empleado ejecuta esta query aparentemente inocente:

SELECT title FROM documents WHERE title LIKE '%budget%';

PostgreSQL automáticamente la reescribe a:

SELECT title FROM documents
WHERE title LIKE '%budget%'
  AND (owner_id = current_user_id() OR department = current_user_department());

La condición de seguridad es añadida permanentemente—no puede ser evitada u optimizada. La aplicación no necesita saber sobre la política de seguridad; PostgreSQL la hace cumplir automáticamente durante la reescritura.

Hemos explorado los componentes principales de la reescritura de queries—reglas personalizadas, vistas, y seguridad a nivel de fila. Ahora veamos cómo todo funciona junto en una sola query que combina expansión de vista con aplicación automática de seguridad.

Juntándolo Todo: Un Ejemplo Completo de Reescritura

Veamos un último ejemplo completo que demuestra todo lo que hemos aprendido. Imagina que tenemos:

-- Tabla base con RLS habilitado
CREATE TABLE orders (
    id INT, customer_id INT, amount DECIMAL, region TEXT
);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY regional_orders ON orders
    USING (region = current_user_region());

-- Vista simple
CREATE VIEW large_orders AS
    SELECT * FROM orders WHERE amount > 1000;

Ahora ejecutamos esta query:

SELECT customer_id, amount FROM large_orders WHERE customer_id = 123;

Así es como el reescritor la transforma:

Original:

SELECT customer_id, amount
FROM large_orders
WHERE customer_id = 123

Después de expansión de vista:

SELECT customer_id, amount
FROM orders
WHERE amount > 1000
  AND customer_id = 123

Después de aplicación de política de seguridad:

SELECT customer_id, amount
FROM orders
WHERE amount > 1000
  AND customer_id = 123
  AND region = current_user_region()

Esta es la query reescrita que PostgreSQL pasa al planificador. La referencia a la vista ha sido reemplazada con la tabla base, la condición WHERE de la vista (amount > 1000) ha sido fusionada con la condición de query original, y la política de seguridad ha sido añadida como una cláusula WHERE adicional. Todo esto ocurre automáticamente, de forma transparente, durante la fase de reescritura.

Ahora que hemos visto cómo el reescritor transforma queries internamente, exploremos por qué es importante entender este proceso para desarrolladores de bases de datos y arquitectos de aplicaciones.

Por Qué Es Importante

Entender la reescritura de queries ayuda a explicar algunos comportamientos importantes de PostgreSQL:

Rendimiento a través de vistas: Las vistas no ralentizan inherentemente las queries. Porque PostgreSQL las reescribe en operaciones de tabla base, el planificador puede optimizarlas igual de bien que si hubieras escrito la query directamente contra las tablas.

Seguridad transparente: Las políticas de seguridad a nivel de fila no requieren cambios en la aplicación. Una vez que defines políticas, PostgreSQL las hace cumplir automáticamente para todas las queries, incluyendo aquellas que vienen de código de aplicación existente.

Depuración de queries: Cuando usas EXPLAIN para entender el rendimiento de queries, estás viendo la query reescrita, no tu original. Esto explica por qué el plan de ejecución podría referenciar tablas y condiciones que no mencionaste explícitamente.

Estas implicaciones prácticas muestran cómo la reescritura de queries afecta tu trabajo diario con PostgreSQL, desde diseñar esquemas hasta depurar problemas de rendimiento. Recapitulemos lo que hemos aprendido sobre esta fase crítica de transformación.

Resumiendo

Hemos recorrido la fase de reescritura de queries de PostgreSQL—el paso de transformación entre análisis semántico y planificación. El reescritor aplica dos tipos principales de transformaciones: expansión de queries basada en reglas y aplicación automática de seguridad.

Las ideas clave:

Las reglas almacenadas en pg_rewrite pueden ser INSTEAD (reemplazar operaciones) o ALSO (añadir operaciones), y activarse condicional o incondicionalmente. Las vistas son simplemente reglas INSTEAD que PostgreSQL crea automáticamente—por esto las vistas no ralentizan las queries, ya que el planificador optimiza las operaciones de tabla base expandidas.

Las reglas personalizadas te permiten implementar lógica de negocio de forma transparente, como auditar automáticamente cambios de usuarios admin. Aunque los triggers son más comunes hoy, las reglas ofrecen capacidades únicas al transformar queries antes de la ejecución.

La seguridad a nivel de fila se integra directamente en la reescritura añadiendo condiciones WHERE que hacen cumplir políticas de acceso. El reescritor inyecta automáticamente predicados de seguridad que no pueden ser evitados, proporcionando seguridad transparente e independiente de la aplicación.

Cuando la reescritura se completa, tu query ha sido transformada—vistas expandidas, seguridad aplicada, reglas personalizadas ejecutadas. El reescritor asegura que las abstracciones de base de datos funcionen de forma transparente mientras da al planificador el panorama completo de lo que necesita ser ejecutado.

Con el Query Tree reescrito listo, PostgreSQL enfrenta su siguiente desafío: cómo ejecutarlo de la manera más eficiente. ¿Debería usar un índice o escanear la tabla? ¿Qué algoritmo de join? ¿Qué orden para múltiples joins? Estas decisiones de optimización pertenecen al planificador, que exploraremos en el siguiente artículo.