Índice único adiado no postgres

14

Examinando a documentação do postgres para alterar tabelas , parece que restrições regulares podem ser marcadas como DEFERRABLE(mais concretamente, INITIALLY DEFERREDé nisso que estou interessado).

Os índices também podem ser associados a uma restrição, desde que:

O índice não pode ter colunas de expressão nem ser um índice parcial

O que me leva a acreditar que atualmente não há como ter um índice exclusivo com condições, como:

CREATE UNIQUE INDEX unique_booking
  ON public.booking
  USING btree
  (check_in, check_out)
  WHERE booking_status = 1;

Ou seja INITIALLY DEFERRED, que a restrição de exclusividade será verificada apenas no final da transação (se SET CONSTRAINTS ALL DEFERRED;for usada).

Minha suposição está correta e, em caso afirmativo, existe alguma maneira de atingir o comportamento pretendido?

obrigado

jcristovao
fonte

Respostas:

15

Um índice não pode ser adiado - não importa se é UNIQUEou não, parcial ou não, apenas uma UNIQUErestrição. Outros tipos de restrições ( FOREIGN KEY, PRIMARY KEY, EXCLUDE) também estão deferrable - mas não CHECKrestrições.

Portanto, o índice parcial exclusivo (e a restrição implícita que ele implementa) serão verificados em todas as instruções (e, de fato, após cada inserção / atualização de linha na implementação atual), não no final da transação.


O que você pode fazer, se desejar implementar essa restrição como adiada, é adicionar mais uma tabela no design. Algo assim:

CREATE TABLE public.booking_status
  ( booking_id int NOT NULL,               -- same types
    check_in timestamp NOT NULL,           -- as in  
    check_out timestamp NOT NULL,          -- booking
    CONSTRAINT unique_booking
        UNIQUE (check_in, check_out)
        DEFERRABLE INITIALLY DEFERRED,
    CONSTRAINT unique_booking_fk
        FOREIGN KEY (booking_id, check_in, check_out)
        REFERENCES public.booking (booking_id, check_in, check_out)
        DEFERRABLE INITIALLY DEFERRED
  ) ;

Com esse design e assumindo que booking_statusbookingexistem 2 opções possíveis (0 e 1), você pode removê-lo totalmente de (se houver uma linha em booking_status, é 1, se não for 0).


Outra maneira seria (ab) usar uma EXCLUDErestrição:

ALTER TABLE booking
    ADD CONSTRAINT unique_booking
        EXCLUDE 
          ( check_in  WITH =, 
            check_out WITH =, 
            (CASE WHEN booking_status = 1 THEN TRUE END) WITH =
          ) 
        DEFERRABLE INITIALLY DEFERRED ;

Testado no dbfiddle .

O que o acima faz:

  • A CASEexpressão se torna NULLquando booking_statusé nula ou diferente de 1. Poderíamos escrever (CASE WHEN booking_status = 1 THEN TRUE END)como (booking_status = 1 OR NULL)se isso tornasse mais claro.

  • As restrições exclusivas e de exclusão aceitam linhas em que uma ou mais das expressões é NULL. Por isso, atua como um índice filtrado com WHERE booking_status = 1.

  • Todos os WITHoperadores são =assim que age como uma UNIQUErestrição.

  • Esses dois combinados fazem a restrição atuar como um índice exclusivo filtrado.

  • Mas é uma restrição e as EXCLUDErestrições podem ser adiadas.

ypercubeᵀᴹ
fonte
2
+1 para a versão EXCLUDE era o que eu precisava. Aqui está outro exemplo mostrando as capacidades de excluir: cybertec-postgresql.com/en/postgresql-exclude-beyond-unique
Benjamin Peter
(CASE WHEN booking_status = 1 THEN TRUE END) WITH =)deve ser substituído ) WHERE (booking_status = 1)por "As restrições de exclusão são implementadas usando um índice", e esse índice parcial WHEREserá menor e mais rápido - postgresql.org/docs/current/sql-createtable.html e postgresql.org/docs/current/sql- createindex.html
Denis Ryzhkov
1

Embora os anos dessa pergunta tenham passado, gostaria de esclarecer para os falantes de espanhol, os testes foram feitos no Postgres:

A seguinte restrição foi adicionada a uma tabela de 1337 registros, em que o kit é a chave primária:

**Bloque 1**
ALTER TABLE ele_kitscompletos
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit) 

Isso cria uma chave primária padrão NÃO DEFERIDA para a tabela, portanto, ao tentar a próxima ATUALIZAÇÃO, obtemos um erro:

update ele_kitscompletos
set div_nkit = div_nkit + 1; 

ERRO: chave duplicada viola a restrição de exclusividade «unique_div_nkit»

No Postgres, a execução de uma atualização para cada linha verifica se a restrição ou restrição é atendida.


O CONSTRAINT IMMEDIATE agora é criado e cada instrução é executada separadamente:

ALTER TABLE ele_kitscompletos
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit)
DEFERRABLE INITIALLY IMMEDIATE

**Bloque 2**
BEGIN;   
UPDATE ele_kitscompletos set div_nkit = div_nkit + 1;
INSERT INTO public.ele_kitscompletos(div_nkit, otro_campo)
VALUES 
  (1338, '888150502');
COMMIT;

Consulta OK, 0 linhas afetadas (tempo de execução: 0 ms; tempo total: 0 ms) Consulta OK, 1328 linhas afetadas (tempo de execução: 858 ms; tempo total: 858 ms) ERRO: llave duplicado : Você existe o arquivo (div_nkit) = (1338).

Aqui, o SI permite alterar a chave primária, uma vez que executa toda a primeira frase completa (1328 linhas); mas, embora esteja em transação (BEGIN), o CONSTRAINT é validado imediatamente após o término de cada sentença sem ter feito COMMIT, portanto, gera o erro ao executar o INSERT. Por fim, criamos o CONSTRAINT DEFERRED, faça o seguinte:

**Bloque 3**
ALTER TABLE public.ele_edivipol
DROP CONSTRAINT unique_div_nkit RESTRICT;   

ALTER TABLE ele_edivipol
ADD CONSTRAINT unique_div_nkit
PRIMARY KEY (div_nkit)
DEFERRABLE INITIALLY DEFERRED

Se executarmos cada instrução do ** Bloco 2 **, cada sentença separadamente, nenhum erro será gerado para o INSERT, pois ele não valida, mas o COMMIT final é executado onde encontra uma inconsistência.


Para informações completas em inglês, sugiro que você verifique os links:

Restrições SQL adiadas em profundidade

NÃO DEFERRABLE versus DEFERRABLE INICIALMENTE IMEDIATO

David Campos
fonte