Utilización de las Extensiones en los Índices

Los procedimientos descritos hasta aquí le permiten definir un nuevo tipo, nuevas funciones y nuevos operadores. Sin embargo, todavía no podemos definir un índice secundario (tal como un B-tree, R-tree o método de acceso hash) sobre un nuevo tipo o sus operadores.

Mírese nuevamente El principal sistema de catalogo de Postgres. La mitad derecha muestra los catálogos que debemos modificar para poder indicar a Postgres cómo utilizar un tipo definido por el usuario y/u operadores definidos por el usuario con un índice (es decir, pg_am, pg_amop, pg_amproc, pg_operator y pg_opclass). Desafortunadamente, no existe un comando simple para hacer esto. Demostraremos cómo modificar estos catálogos a través de un ejemplo ejecutable: una nueva clase de operador para el método de acceso B-tree que almacene y ordene números complejos en orden ascendente de valor absoluto.

La clase pg_am contiene una instancia para cada médodo de acceso definido por el usuario. El soporte de acceso a la memoria (heap) está integrado en Postgres, pero todos los demás métodos de acceso están descritos aquí. El esquema es

Tabla 1. Esquema de un Índice

AtributoDescripción
amnamenombre del método de acceso
amowneridentificador de objeto del propietario de esta instancia en pg_user
amstrategiesnúmero de estrategias para este método de acceso (véase más abajo)
amsupportnúmero de rutinas de soporte para este método de acceso (véase más abajo)
amorderstrategycero si el índice no ofrece secuencia de ordenamiento, sino el número de estrategia del operador de estrategia que describe la secuencia de ordenamiento
amgettuple 
aminsert 
...indicadores de procedimiento para las rutinas de interfaz con el método de acceso. Por ejemplo, aquí aparecen identificadores regproc para abrir, cerrar y obtener instancias desde el método de acceso

El identificador de objeto (object ID) de la instancia en pg_am se utiliza como una clave foránea en multitud de otras clases. No es necesario que Ud. agregue una nueva instancia en esta clase; lo que debe interesarle es el identificador de objeto (object ID) de la instancia del método de acceso que quiere extender:

SELECT oid FROM pg_am WHERE amname = 'btree';

         +----+
         |oid |
         +----+
         |403 |
         +----+
   
Utilizaremos ese comando SELECT en una cláusula WHERE posterior.

El atributo amstrategies tiene como finalidad estandarizar comparaciones entre tipos de datos. Por ejemplo, los B-trees imponen un ordenamiento estricto en las claves, de menor a mayor. Como Postgres permite al usuario definir operadores, no puede, a través del nombre del operador (por ej., ">" or "<"), identificar qué tipo de comparación es. De hecho, algunos métodos de acceso no imponen ningún ordenamiento. Por ejemplo, los R-trees expresan una relación de inclusión en un rectángulo, mientras que una estructura de datos de tipo hash expresa únicamente similaridad de bits basada en el valor de una función hash. Postgres necesita alguna forma consistente para interpretar los requisitos en sus consultas, identificando el operador y decidiendo si se puede utilizar un índice existente. Esto implica que Postgres necesita conocer, por ejemplo, que los operadores "<=" y ">" particionan un B-tree. Postgres utiliza estrategias para expresar esas relaciones entre los operadores y las formas en que pueden utilizarse al recorrer los índices.

Definir un nuevo conjunto de estrategias está más allá del alcance de esta exposición, pero explicaremos cómo funcionan las estrategias B-tree porque necesitará conocerlas para agregar una nueva clase de operador. En la clase pg_am, el atributo amstrategies es el número de estrategias definidas para este método de acceso. Para los B-trees, este número es 5. Estas estrategias corresponden a

Tabla 2. Estrategias B-tree

OperaciónÍndice
menor que1
menor que o igual a2
igual3
mayor que o igual a4
mayor que5

La idea es que será necesario agregar procedimientos correspondientes a las comparaciones mencionadas arriba a la tabla pg_amop (véase más abajo). El código de método de acceso puede utilizar estos números de estrategia, sin tener en cuenta el tipo de datos, para resolver cómo particionar el B-tree, calcular la selectividad, etcétera. No se preocupe aún acerca de los detalles para agregar procedimientos; sólo comprenda que debe existir un conjunto de procedimientos para int2, int4, oid, y todos los demás tipos de datos donde puede operar un B-tree.

Algunas veces, las estrategias no proporcionan la información suficiente para resolver la forma de utilizar un índice. Algunos métodos de acceso requieren otras rutinas de soporte para poder funcionar. Por ejemplo, el método de acceso B-tree debe ser capaz de comparar dos claves y determinar si una es mayor que, igual a, o menor que la otra. De manera análoga, el método de acceso R-tree debe ser capaz de calcular intersecciones, uniones, y tamaños de rectángulos. Estas operaciones no corresponden a requisitos del usuario en las consultas SQL; son rutinas administrativas utilizadas por los métodos de acceso, internamente.

Para manejar diversas rutinas de soporte consistentemente entre todos los métodos de acceso de Postgres, pg_am incluye un atributo llamado amsupport. Este atributo almacena el número de rutinas de soporte utilizadas por un método de acceso. Para los B-trees, este número es uno -- la rutina que toma dos claves y devuelve -1, 0, o +1, dependiendo si la primer clave es menor que, igual a, o mayor que la segunda.

Nota

En términos estrictos, esta rutina puede devolver un número negativo (< 0), 0, o un valor positivo distinto de cero (> 0).

La entrada amstrategies en pg_am sólo indica el número de estrategias definidas para el método de acceso en cuestión. Los procedimientos para menor que, menor que o igual a, etcétera no aparecen en pg_am. De manera similar, amsupport es solamente el número de rutinas de soporte que requiere el método de acceso. Las rutinas reales están listadas en otro lado.

Además, la entrada amorderstrategy indica si el método de acceso soporta o no un recorrido ordenado. Cero significa que no; si lo hace, amorderstrategy es el número de la rutina de estrategia que corresponde al operador de ordenamiento. Por ejemplo, btree tiene amorderstrategy = 1 que corresponde al número de estrategia de "menor que".

La próxima clase de interés es pg_opclass. Esta clase tiene como única finalidad asociar un nombre y tipo por defecto con un oid. En pg_amop cada clase de operador B-tree tiene un conjunto de procedimientos, de uno a cinco, descritos más arriba. Algunas clases de operadores (opclasses) son int2_ops,int4_ops, y oid_ops. Es necesario que Ud. agregue una instancia con su nombre de clase de operador (por ejemplo, complex_abs_ops) a pg_opclass. El oid de esta instancia es una clave foránea en otras clases.

INSERT INTO pg_opclass (opcname, opcdeftype)
    SELECT 'complex_abs_ops', oid FROM pg_type WHERE typname = 'complex_abs';

SELECT oid, opcname, opcdeftype
    FROM pg_opclass
    WHERE opcname = 'complex_abs_ops';

         +------+-----------------+------------+
         |oid   | opcname         | opcdeftype |
         +------+-----------------+------------+
         |17314 | complex_abs_ops |      29058 |
         +------+-----------------+------------+
   
¡Nótese que el oid para su instancia de pg_opclass será diferente! No se preocupe por esto. Obtendremos este número del sistema después igual que acabamos de hacerlo con el oid del tipo aquí.

De esta manera ahora tenemos un método de acceso y una clase de operador. Aún necesitamos un conjunto de operadores; el procedimiento para definir operadores fue discutido antes en este manual. Para la clase de operador complex_abs_ops en Btrees, los operadores que necesitamos son:

        valor absoluto menor que (absolute value less-than)
        valor absoluto menor que o igual a (absolute value less-than-or-equal)
        valor absoluto igual (absolute value equal)
        valor absoluto mayor que o igual a (absolute value greater-than-or-equal)
        valor absoluto mayor que (absolute value greater-than)
   

Supongamos que el código que implementa las funciones definidas está almacenado en el archivo PGROOT/src/tutorial/complex.c

Parte del código será parecido a este: (nótese que solamente mostraremos el operador de igualdad en el resto de los ejemplos. Los otros cuatro operadores son muy similares. Refiérase a complex.co complex.source para más detalles.)

#define Mag(c) ((c)->x*(c)->x + (c)->y*(c)->y)

         bool
         complex_abs_eq(Complex *a, Complex *b)
         {
             double amag = Mag(a), bmag = Mag(b);
             return (amag==bmag);
         }
   

Hay un par de cosas importantes que suceden arriba.

Primero, nótese que se están definiendo operadores menor que, menor que o igual a, igual, mayor que o igual a, y mayor que para int4. Todos estos operadores ya están definidos para int4 bajo los nombres <, <=, =, >=, and >. Los nuevos operadores, por supuesto, se comportan de manera distinta. Para garantizar que Postgres usa estos nuevos operadores en vez de los anteriores, es necesario que sean nombrados distinto que ellos. Este es un punto clave: Ud. puede sobrecargar operadores en Postgres, pero sólo si el operador no ha sido definido aún para los tipos de los argumentos. Es decir, si Ud. tiene < definido para (int4, int4), no puede definirlo nuevamente. Postgres no comprueba esto cuando define un nuevo operador, así es que debe ser cuidadoso. Para evitar este problema, se utilizarán nombres dispares para los operadores. Si hace esto mal, los métodos de acceso seguramente fallen cuando intente hacer recorridos.

El otro punto importante es que todas las funciones de operador devuelven valores lógicos (Boolean). Los métodos de acceso cuentan con este hecho. (Por otro lado, las funciones de soporte devuelven cualquier cosa que el método de acceso particular espera -- en este caso, un entero con signo.) La rutina final en el archivo es la "rutina de soporte" mencionada cuando tratábamos el atributo amsupport de la clase pg_am. Utilizaremos esto más adelante. Por ahora, ignórelo.

CREATE FUNCTION complex_abs_eq(complex_abs, complex_abs)
              RETURNS bool
              AS 'PGROOT/tutorial/obj/complex.so'
              LANGUAGE 'c';
   

Ahora defina los operadores que los utilizarán. Como se hizo notar, los nombres de operadores deben ser únicos entre todos los operadores que toman dos operandos int4. Para ver si los nombres de operadores listados más arriba ya han sido ocupados, podemos hacer una consulta sobre pg_operator:

    /*
     * esta consulta utiliza el operador de expresión regular (~)
     * para encontrar nombres de operadores de tres caracteres que terminen 
     * con el carácter &
     */
    SELECT *
     FROM pg_operator
     WHERE oprname ~ '^..&$'::text;
   

para ver si su nombre ya ha sido ocupado para los tipos que Ud. quiere. Las cosas importantes aquí son los procedimientos (que son las funciones Cdefinidas más arriba) y las funciones de restricción y de selectividad de unión. Ud. debería utilizar solamente las que se usan abajo -- nótese que hay distintas funciones para los casos menor que, igual, y mayor que. Éstas deben proporcionarse, o el método de acceso fallará cuando intente utilizar el operador. Debería copiar los nombres para las funciones de restricción y de unión, pero utilice los nombres de procedimiento que definió en el último paso.

CREATE OPERATOR = (
     leftarg = complex_abs, rightarg = complex_abs,
     procedure = complex_abs_eq,
     restrict = eqsel, join = eqjoinsel
         )
   

Téngase en cuenta que se definen cinco operadores correspondientes a menor, menor o igual, igual, mayor, y mayor o igual.

Ya casi hemos terminado. La última cosa que necesitamos hacer es actualizar la tabla pg_amop. Para hacer esto, necesitamos los siguientes atributos:

Tabla 3. Esquema de pg_amproc

AtributoDescripción
amopidel oid de la instancia de pg_am para B-tree (== 403, véase arriba)
amopclaidel oid de la instancia de pg_opclass para complex_abs_ops(== lo que obtuvo en vez de 17314, véase arriba)
amopoprlos oids de los operadores para la clase de operador (opclass) (que obtendremos dentro de un minuto)

Entonces necesitamos los oids de los operadores que acabamos de definir. Buscaremos los nombres de todos los operadores que toman dos argumentos de tipo complex, y así sacaremos los nuestros:

    SELECT o.oid AS opoid, o.oprname
     INTO TABLE complex_ops_tmp
     FROM pg_operator o, pg_type t
     WHERE o.oprleft = t.oid and o.oprright = t.oid
      and t.typname = 'complex_abs';

         +------+---------+
         |oid   | oprname |
         +------+---------+
         |17321 | <    |
         +------+---------+
         |17322 | <=   |
         +------+---------+
         |17323 |  =      |
         +------+---------+
         |17324 | >=   |
         +------+---------+
         |17325 | >    |
         +------+---------+
   
(De nuevo, algunos de sus números de oid serán seguramente diferentes.) Los operadores en los que estamos interesados son los que tienen oids 17321 hasta 17325. Los valores que Ud. obtendrá serán probablemente distintos, y debe sustituirlos abajo por estos valores. Haremos esto con una sentencia SELECT.

Ahora estamos listos para actualizar pg_amop con nuestra nueva clase de operador. La cosa más importante en toda esta explicación es que los operadores están ordenados desde menor que hasta mayor que, en pg_amop. Agregamos las instancias necesarias:

    INSERT INTO pg_amop (amopid, amopclaid, amopopr, amopstrategy)
        SELECT am.oid, opcl.oid, c.opoid, 1
        FROM pg_am am, pg_opclass opcl, complex_abs_ops_tmp c
        WHERE amname = 'btree' AND
            opcname = 'complex_abs_ops' AND
            c.oprname = '<';
   
Ahora haga lo mismo con los otros operadores sustituyendo el "1" en la tercera línea de arriba y el "<" en la última línea. Nótese el orden: "menor que" es 1, "menor que o igual a" es 2, "igual" es 3, "mayor que o igual a" es 4, y "mayor que" es 5.

El próximo paso es registrar la "rutina de soporte" previamente descrita en la explicación de pg_am. El oid de esta rutina de soporte está almacenada en la clase pg_amproc, cuya clave está compuesta por el oid del método de acceso y el oid de la clase de operador. Primero, necesitamos registrar la función en Postgres (recuerde que pusimos el código C que implementa esta rutina al final del archivo en el cual implementamos las rutinas del operador):

    CREATE FUNCTION complex_abs_cmp(complex, complex)
     RETURNS int4
     AS 'PGROOT/tutorial/obj/complex.so'
     LANGUAGE 'c';

    SELECT oid, proname FROM pg_proc
     WHERE proname = 'complex_abs_cmp';

         +------+-----------------+
         |oid   | proname         |
         +------+-----------------+
         |17328 | complex_abs_cmp |
         +------+-----------------+
   
(De nuevo, su número de oid será probablemente distinto y debe sustituirlo abajo por el valor que vea.) Podemos agregar la nueva instancia de la siguiente manera:
    INSERT INTO pg_amproc (amid, amopclaid, amproc, amprocnum)
        SELECT a.oid, b.oid, c.oid, 1
            FROM pg_am a, pg_opclass b, pg_proc c
            WHERE a.amname = 'btree' AND
                b.opcname = 'complex_abs_ops' AND
                c.proname = 'complex_abs_cmp';
   

Ahora necesitamos agregar una estrategia de hash para permitir que el tipo sea indexado. Hacemos esto utilizando otro tipo en pg_am pero reutilizamos los mismos operadores.

    INSERT INTO pg_amop (amopid, amopclaid, amopopr, amopstrategy)
        SELECT am.oid, opcl.oid, c.opoid, 1
        FROM pg_am am, pg_opclass opcl, complex_abs_ops_tmp c
        WHERE amname = 'hash' AND
            opcname = 'complex_abs_ops' AND
            c.oprname = '=';
   

Para utilizar este índice en una cláusula WHERE, necesitamos modificar la clase pg_operator de la siguiente manera.

    UPDATE pg_operator
        SET oprrest = 'eqsel'::regproc, oprjoin = 'eqjoinsel'
        WHERE oprname = '=' AND
            oprleft = oprright AND
            oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
    
    UPDATE pg_operator
        SET oprrest = 'neqsel'::regproc, oprjoin = 'neqjoinsel'
        WHERE oprname = '' AND
            oprleft = oprright AND
            oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
    
    UPDATE pg_operator
        SET oprrest = 'neqsel'::regproc, oprjoin = 'neqjoinsel'
        WHERE oprname = '' AND
            oprleft = oprright AND
            oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
    
    UPDATE pg_operator
        SET oprrest = 'scalarltsel'::regproc, oprjoin = 'scalarltjoinsel'
        WHERE oprname = '<' AND 
            oprleft = oprright AND
            oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
    
    UPDATE pg_operator
        SET oprrest = 'scalarltsel'::regproc, oprjoin = 'scalarltjoinsel'
        WHERE oprname = '<=' AND
            oprleft = oprright AND
            oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
    
    UPDATE pg_operator
        SET oprrest = 'scalargtsel'::regproc, oprjoin = 'scalargtjoinsel'
        WHERE oprname = '>' AND
            oprleft = oprright AND
            oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
    
    UPDATE pg_operator
        SET oprrest = 'scalargtsel'::regproc, oprjoin = 'scalargtjoinsel'
        WHERE oprname = '>=' AND
            oprleft = oprright AND
            oprleft = (SELECT oid FROM pg_type WHERE typname = 'complex_abs');
   

Y por último (¡por fin!) registramos una descripción de este tipo.

    INSERT INTO pg_description (objoid, description) 
    SELECT oid, 'Two part G/L account'
	    FROM pg_type WHERE typname = 'complex_abs';