PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Vincolo di esclusione su una colonna bitstring con operatore AND bit per bit

Come chiarito dalla tua modifica, hai installato l'estensione btree_gist . Senza di esso, l'esempio fallirebbe già in name WITH = .

CREATE EXTENSION btree_gist;

Le classi dell'operatore installate da btree_gist coprire molti operatori. Sfortunatamente, il & operatore non è tra questi. Ovviamente perché non restituisce un boolean che ci si aspetterebbe da un operatore per qualificarsi.

Soluzione alternativa

Userei una combinazione di un indice a più colonne b-tree (per la velocità) e un trigger invece. Considera questa demo, testata su 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.

Dovrebbe funzionare molto bene, in realtà meglio del vincolo di esclusione, perché il mantenimento di un indice b-tree è più economico di un indice GiST. E la ricerca con = di base gli operatori dovrebbero essere più veloci delle ipotetiche ricerche con & operatore.

Questa soluzione non è sicura quanto un vincolo di esclusione, perché i trigger possono essere aggirati più facilmente, ad esempio in un trigger successivo sullo stesso evento o se il trigger è temporaneamente disabilitato. Preparati a eseguire controlli aggiuntivi sull'intero tavolo se si applicano tali condizioni.

Condizione più complessa

Il trigger di esempio rileva solo l'inversione di value . Come hai chiarito nel tuo commento, in realtà hai bisogno di una condizione come questa:

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

Questa condizione è leggermente più costosa, ma può comunque utilizzare un indice. L'indice a più colonne dall'alto funzionerebbe, se ne hai comunque bisogno. Oppure, leggermente più efficiente, un semplice indice sul nome:

CREATE INDEX t_name_idx ON t (name);

Come hai commentato, possono esserci solo un massimo di 8 righe distinte per name , meno in pratica. Quindi dovrebbe essere ancora veloce.

Prestazioni INSERT massime

Se INSERT le prestazioni sono fondamentali, soprattutto se molti tentativi di INSERT non soddisfano la condizione, potresti fare di più:creare una vista materializzata che pre-aggregasse il value per name :

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

name è garantito per essere unico qui. Userei una PRIMARY KEY su name per fornire l'indice che stiamo cercando:

ALTER TABLE mv_t SET (fillfactor=90);

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

Quindi il tuo INSERT potrebbe assomigliare a questo:

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 (...)

Il fillfactor è utile solo se la tua tabella riceve molti aggiornamenti.

Aggiorna le righe nella vista materializzata in un TRIGGER AFTER INSERT OR UPDATE OF name, value OR DELETE per mantenerlo aggiornato. Il costo degli oggetti aggiuntivi deve essere soppesato rispetto al guadagno. Dipende in gran parte dal tuo carico tipico.