PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

PostgreSQL – Como eliminar valores repetidos


É possível que em uma tabela seja necessário algum campo que tenha valores repetidos para deixá-lo como único.
E como proceder com valores repetidos sem eliminá-los todos?
Seria possível deixar apenas os mais atuais ?

Coluna do sistema ctid


Cada tabela possui algumas colunas definidas implicitamente pelo sistema, cujos nomes são reservados.
Atualmente as colunas do sistema são:tableoid, xmin, cmin, xmax, cmax e ctid. Cada um possui metadados da tabela a que pertencem.
A coluna do sistema ctid destina-se a armazenar a versão da localização física da linha. Esta versão pode mudar se a linha
for atualizada (UPDATE) ou a tabela passar por um VACUUM FULL.
O tipo de dado do ctid é tid, ou seja, identificador de tupla (ou identificador de linha), que é um par (número do bloco, índice da tupla dentro do bloco)
que identifica a localização física da linha dentro da tabela.
Esta coluna sempre tem seu valor único na tabela, portanto, quando houver linhas com valores repetidos pode ser usado como critério para sua eliminação.



Criação da tabela de teste:
CREATE TABLE tb_test_ctid (
    col1 int,
    col2 text);

Insira alguns dados:
INSERT INTO tb_test_ctid VALUES 
(1, 'foo'),
(2, 'bar'),
(3, 'baz');

Verifique as linhas atuais:
SELECT ctid, * FROM tb_test_ctid;
 ctid  | col1 | col2 
-------+------+------
 (0,1) |    1 | foo
 (0,2) |    2 | bar
 (0,3) |    3 | baz

Atualizar uma linha:
UPDATE tb_test_ctid SET col2 = 'spam' WHERE col1 = 1;


Verifique novamente a tabela:
SELECT ctid, * FROM tb_test_ctid;
 ctid  | col1 | col2 
-------+------+------
 (0,2) |    2 | bar
 (0,3) |    3 | baz
 (0,4) |    1 | spam

Podemos notar que a linha atualizada teve seu ctid alterado também…

Um simples teste VACUUM FULL:
VACUUM FULL tb_test_ctid;

Verificando a tabela após VACUUM:
SELECT ctid, * FROM tb_test_ctid;

ctid   | col1 | col2 
-------+------+------
(0,1)  | 2    | bar
(0,2)  | 3    | baz
(0,3)  | 1    | spam

Atualize a mesma linha novamente usando a cláusula RETURNING:
UPDATE tb_test_ctid
    SET col2 = 'eggs'
    WHERE col1 = 1
    RETURNING ctid;

 ctid  
-------
 (0,4)

Verifique novamente a tabela:
SELECT ctid, * FROM tb_test_ctid;
 ctid  | col1 | col2 
-------+------+------
 (0,2) |    2 | bar
 (0,3) |    3 | baz
 (0,4) |    1 | spam

Eliminando valores repetidos com ctid


Imagine uma tabela que tem valores repetidos em um campo e esse mesmo campo é decidido a torná-lo único posteriormente.
Lembre-se que um campo PRIMARY KEY também é único.
OK, foi decidido que os valores repetidos em esse campo será excluído.
Agora é necessário estabelecer um critério para decidir entre esses valores repetidos quais permanecerão.
No caso a seguir, o critério é a linha mais atual, ou seja, aquela com o maior valor de ctid.



Criação de nova tabela de teste:
CREATE TABLE tb_foo(
    id_ int,  --This field will be the primary key in the future!
    letter char(1)
);

Insira 10 registros:
INSERT INTO tb_foo (id_, letter) SELECT generate_series(1, 10), 'a';

Confira a tabela:
SELECT id_, letter FROM tb_foo;

 id_ | letter 
-----+--------
   1 | a
   2 | a
   3 | a
   4 | a
   5 | a
   6 | a
   7 | a
   8 | a
   9 | a
  10 | a
Insira mais 3 registros:
INSERT INTO tb_foo (id_, letter) SELECT generate_series(1, 3), 'b';


Verifique os valores repetidos:
SELECT id_, letter FROM tb_foo WHERE id_ <= 3;

 id_ | letter  
-----+--------
   1 | a
   2 | a
   3 | a
   1 | b
   2 | b
   3 | b

Existem valores repetidos no campo id_ da tabela…



Tente tornar o campo id_ uma chave primária:
ALTER TABLE tb_foo ADD CONSTRAINT tb_foo_pkey PRIMARY KEY (id_);

ERROR:  could not create unique index "tb_foo_pkey"
DETAIL:  Key (id_)=(3) is duplicated.



Usando as funções CTE e janela, descubra quais valores repetidos serão mantidos:
WITH t AS (
SELECT
    id_,
    count(id_) OVER (PARTITION BY id_) AS count_id,  -- Count
    ctid,
    max(ctid) OVER (PARTITION BY id_) AS max_ctid  -- Most current ctid
    
    FROM tb_foo
)

SELECT
    t.id_,
    t.max_ctid
    FROM t
    WHERE t.count_id > 1  -- Filters which values repeat
    GROUP by id_, max_ctid;

 id_ | max_ctid 
-----+----------
   3 | (0,13)
   1 | (0,11)
   2 | (0,12)



Deixando a tabela com valores únicos para o campo id_, removendo as linhas mais antigas:
WITH

t1 AS (
SELECT
    id_,
    count(id_) OVER (PARTITION BY id_) AS count_id,
    ctid,
    max(ctid) OVER (PARTITION BY id_) AS max_ctid
    
    FROM tb_foo
),

t2 AS (  -- Virtual table that filters repeated values that will remain
SELECT t1.id_, t1.max_ctid
    FROM t1
    WHERE t1.count_id > 1
    GROUP by t1.id_, t1.max_ctid)

DELETE  -- DELETE with JOIN 
    FROM tb_foo AS f
    USING t2
    WHERE 
        f.id_ = t2.id_ AND  -- tb_foo has id_ equal to t2 (repeated values)
        f.ctid < t2.max_ctid;  -- ctid is less than the maximum (most current)

Verificando os valores da tabela sem valores duplicados para id_:
SELECT id_, letter FROM tb_foo;

 id_ | letter 
-----+--------
   4 | a
   5 | a
   6 | a
   7 | a
   8 | a
   9 | a
  10 | a
   1 | b
   2 | b
   3 | b



Agora você pode alterar a tabela para deixar o campo id_ como PRIMARY KEY:
ALTER TABLE tb_foo ADD CONSTRAINT tb_foo_pkey PRIMARY KEY (id_);