Índices Parciales

Jorge corriendo por un carril totalmente distinto al de los demás corredores, con un bocadillo que dice Partial Index

Tabla de contenidos


Índices Parciales

Un índice parcial es un índice que se crea únicamente sobre un subconjunto de filas de una tabla, definido mediante una condición WHERE específica. En lugar de indexar todas las filas, el índice solo incluye aquellas que cumplen ciertos criterios, lo que puede mejorar el rendimiento y reducir el espacio utilizado.

En mi caso particular, necesitaba indexar campos en función de si sus valores eran NULL o NOT NULL. Por ejemplo, los siguientes índices parciales en SQL:

CREATE INDEX index_field_null 
ON table(field) 
WHERE field IS NULL;

CREATE INDEX index_field_not_null 
ON table(field) 
WHERE field IS NOT NULL;

También es posible crear índices parciales que involucren múltiples columnas:

CREATE INDEX index_field1_null_field2_null 
ON table(field1, field2) 
WHERE field1 IS NULL AND field2 IS NULL;

CREATE INDEX index_field1_not_null_field2_not_null 
ON table(field1, field2) 
WHERE field1 IS NOT NULL AND field2 IS NOT NULL;

No todas las bases de datos admiten índices parciales, en mi caso particular estoy usando PostgreSQL


Problema

Por defecto, Vapor no ofrece soporte directo para la creación de índices parciales, ya que la generación estándar de índices no contempla la inclusión de una cláusula WHERE en la definición del índice.

El siguiente fragmento de código crea un índice normal, ya sea sobre uno o varios campos, pero no permite especificar una condición para un índice parcial:

try await db.sqlDatabase
    .create($0.key)
    .on(table)
    .colums($0.colums)
    .run()

Este código funciona para crear índices simples, pero carece de la capacidad para agregar un predicado WHERE.

Este fragmento ha sido adaptado por mí; el método .create del constructor original expone más opciones de configuración, pero en este ejemplo muestro una implementación personalizada y simplificada que uso actualmente.


Alternativa

La forma más directa de crear índices parciales es ejecutar sentencias SQL en crudo raw SQL, tal como se mostró en los ejemplos iniciales. Esto implica construir un método para ejecutar en crudo las sentencias CREATE INDEX con la cláusula WHERE correspondiente.

Aunque efectivo, este enfoque pierde la ventaja de la abstracción y seguridad que ofrece Vapor al construir migraciones y esquemas de base de datos mediante código Swift.


Solución

El builder SQLCreateIndexBuilder admite un predicado opcional predicate. Si este no es nil, agrega una cláusula WHERE al índice.

Por ello, extendí este builder para incluir un método where que acepta una lista de columnas y un tipo de índice parcial (por ejemplo, null o not null). Este método construye la expresión lógica adecuada para el predicado y la asigna al índice.

extension SQLCreateIndexBuilder {
    @discardableResult
    func `where`(
        _ columns: [FieldKey],
        _ partialIndex: SQLPartialIndexEnum?
    ) -> Self {
        guard let partialIndex else {
            return self
        }
        let op: SQLBinaryOperator = partialIndex == .null ? .is : .isNot

        let conditions = columns.map {
            self.where($0, op)
        }

        let combined: SQLBinaryExpression = conditions.dropFirst()
            .reduce(conditions[0]) { .init($0, .and, $1) }

        return self.where(combined)
    }

    private func `where`(
        _ column: FieldKey,
        _ binary: SQLBinaryOperator
    ) -> SQLBinaryExpression {
        .init(
            left: SQLIdentifier(column.description),
            op: binary,
            right: SQLLiteral.null
        )
    }

    private func `where`(_ expression: SQLExpression) -> Self {
        self.createIndex.predicate = expression
        return self
    }
}

Este código permite invocar el método where para uno o varios campos, especificando si se desea un índice parcial para valores NULL o NOT NULL. Si no se proporciona un valor para partialIndex, se devuelve el índice sin predicado, comportándose como un índice normal.

La lógica consiste en crear una lista de expresiones binarias SQLBinaryExpression para cada columna, combinándolas con el operador lógico AND y asignando el resultado como predicado del índice.


Resultado

Con esta extensión, ahora puedo crear índices parciales de forma sencilla en Vapor, agregando únicamente una línea al código original:

try await db.sqlDatabase
    .create($0.key)
    .on(table)
    .colums($0.colums)
    .where($0.colums, $0.partial)
    .run()

Donde $0.partial es un valor opcional que indica el tipo de índice parcial deseado null o not null.


Notas finales

Este enfoque está diseñado específicamente para índices parciales basados en la presencia o ausencia de valores NULL en columnas. Sin embargo, la implementación puede adaptarse para soportar otras condiciones y casos de uso más complejos, simplemente modificando la construcción del predicado.

La solución ofrece una forma limpia y reutilizable de crear índices parciales dentro del ecosistema Vapor, manteniendo la coherencia y seguridad del código Swift.

Si además quieres organizar tus tablas en espacios de nombres usando la propiedad space de Fluent, lo cubrí en Esquemas y Espacios.

Keep coding, keep running 🏃‍♂️