sql >> Base de Datos >  >> RDS >> PostgreSQL

Restricción de exclusión en una columna de cadena de bits con operador AND bit a bit

Como aclaró su edición, instaló la extensión btree_gist . Sin él, el ejemplo ya fallaría en name WITH = .

CREATE EXTENSION btree_gist;

Las clases de operadores instaladas por btree_gist cubrir muchos operadores. Desafortunadamente, el & el operador no está entre ellos. Obviamente porque no devuelve un boolean que se esperaría de un operador para calificar.

Solución alternativa

Usaría una combinación de un índice de varias columnas de árbol B (para velocidad) y un gatillo en cambio. Considere esta demostración, probada en PostgreSQL 9.1 :

CREATE TABLE t (
  name text 
 ,value bit(8)
);

INSERT INTO t VALUES ('a', B'10101010'); 

CREATE INDEX t_name_value_idx ON t (name, value);

CREATE OR REPLACE FUNCTION trg_t_name_value_inversion_prohibited()
  RETURNS trigger AS
$func$
BEGIN
IF EXISTS (
     SELECT 1 FROM t
     WHERE (name, value) = (NEW.name, ~ NEW.value)  -- example: exclude inversion
     ) THEN

    RAISE EXCEPTION 'Your text here!';
END IF;

RETURN NEW;
END
$func$ LANGUAGE plpgsql;

CREATE TRIGGER insup_bef_t_name_value_inversion_prohibited
BEFORE INSERT OR UPDATE OF name, value  -- only involved columns relevant!
ON t
FOR EACH ROW
EXECUTE PROCEDURE trg_t_name_value_inversion_prohibited();

INSERT INTO t VALUES ('a', ~ B'10101010');  -- fails with your error msg.

Debería funcionar muy bien, en realidad mejor que la restricción de exclusión, porque el mantenimiento de un índice de árbol b es más económico que un índice GiST. Y la búsqueda con = básico los operadores deberían ser más rápidos que las búsquedas hipotéticas con & operador.

Esta solución no es tan segura como una restricción de exclusión, porque los disparadores se pueden eludir más fácilmente, por ejemplo, en un disparador posterior en el mismo evento, o si el disparador se deshabilita temporalmente. Esté preparado para realizar comprobaciones adicionales en toda la tabla si se aplican dichas condiciones.

Condición más compleja

El disparador de ejemplo solo detecta la inversión de value . Como aclaraste en tu comentario, en realidad necesitas una condición como esta:

IF EXISTS (
      SELECT 1 FROM t
      WHERE  name = NEW.name
      AND    value & NEW.value <> B'00000000'::bit(8)
      ) THEN

Esta condición es un poco más costosa, pero aún puede usar un índice. El índice de varias columnas de arriba funcionaría, si lo necesita de todos modos. O, un poco más eficiente, un índice simple en el nombre:

CREATE INDEX t_name_idx ON t (name);

Como comentaste, solo puede haber un máximo de 8 filas distintas por name , menos en la práctica. Así que esto aún debería ser rápido.

Máximo rendimiento de INSERCIÓN

Si INSERT el rendimiento es primordial, especialmente si muchos intentos de INSERCIÓN fallan en la condición, podría hacer más:crear una vista materializada que agregue previamente value por name :

CREATE TABLE mv_t AS 
SELECT name, bit_or(value) AS value
FROM   t
GROUP  BY 1
ORDER  BY 1;

name está garantizado que será único aquí. Usaría una PRIMARY KEY en name para proporcionar el índice que buscamos:

ALTER TABLE mv_t SET (fillfactor=90);

ALTER TABLE mv_t
ADD CONSTRAINT mv_t_pkey PRIMARY KEY(name) WITH (fillfactor=90);

Entonces su INSERT podría verse así:

WITH i(n,v) AS (SELECT 'a'::text, B'10101010'::bit(8)) 
INSERT INTO t (name, value)
SELECT n, v
FROM   i
LEFT   JOIN mv_t m ON m.name = i.n
                  AND m.value & i.v <> B'00000000'::bit(8)
WHERE  m.n IS NULL;          -- alternative syntax for EXISTS (...)

El fillfactor solo es útil si su tabla recibe muchas actualizaciones.

Actualizar filas en la vista materializada en un TRIGGER AFTER INSERT OR UPDATE OF name, value OR DELETE para mantenerlo actualizado. El costo de los objetos adicionales debe sopesarse frente a la ganancia. Depende en gran medida de su carga típica.