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

PostgreSQL:cómo eliminar valores repetidos

Es posible que en una tabla, algún campo que tenga valores repetidos sea necesario dejarlo como único.
¿Y cómo proceder con valores repetidos sin eliminarlos todos?
Sería posible dejar solo los más actuales ?

Columna del sistema ctid

Cada tabla tiene unas columnas implícitamente definidas por el sistema, cuyos nombres están reservados.
Actualmente las columnas del sistema son:tableoid, xmin, cmin, xmax, cmax y ctid. Cada uno tiene metadatos de la tabla a la que pertenecen.
La columna del sistema ctid está destinada a almacenar la versión de la ubicación física de la fila. Esta versión puede cambiar si la fila
se actualiza (UPDATE) o la tabla pasa por un VACUUM FULL.
El tipo de datos de ctid es tid, que significa identificador de tupla (o identificador de fila), que es un par (número de bloque, índice de tupla dentro del bloque)
que identifica la ubicación física de la fila dentro de la tabla.
Esta columna siempre tiene su valor único en la tabla, por lo que cuando hay filas con valores repetidos se puede utilizar como criterio para su eliminación.

Creación de tabla de prueba:

CREATE TABLE tb_test_ctid (
    col1 int,
    col2 text);

Inserte algunos datos:

INSERT INTO tb_test_ctid VALUES 
(1, 'foo'),
(2, 'bar'),
(3, 'baz');

Comprobar las filas actuales:

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

Actualizar una fila:

UPDATE tb_test_ctid SET col2 = 'spam' WHERE col1 = 1;

Revisa la tabla nuevamente:

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

Podemos notar que la fila actualizada también cambió su ctid...

Una simple prueba de VACUUM FULL:

VACUUM FULL tb_test_ctid;

Comprobando la tabla después de VACUUM:

SELECT ctid, * FROM tb_test_ctid;

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

Actualice la misma fila nuevamente usando la cláusula RETURNING:

UPDATE tb_test_ctid
    SET col2 = 'eggs'
    WHERE col1 = 1
    RETURNING ctid;

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

Revisa la tabla nuevamente:

SELECT ctid, * FROM tb_test_ctid;

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

Eliminación de valores repetidos con ctid

Imagine una tabla que tiene valores repetidos en un campo y ese mismo campo se decide para hacerlo único más tarde.
Recuerde que un campo PRIMARY KEY también es único.
OK, se decidió que los valores repetidos en ese campo será eliminado.
Ahora es necesario establecer un criterio para decidir entre estos valores repetidos cuáles permanecerán.
En el siguiente caso, el criterio es la línea más actual, es decir, la que tiene el valor ctid más alto.

Nueva creación de tabla de prueba:

CREATE TABLE tb_foo(
    id_ int,  --This field will be the primary key in the future!
    letter char(1)
);

Insertar 10 registros:

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

Consulta la tabla:

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
Inserte 3 registros más:
INSERT INTO tb_foo (id_, letter) SELECT generate_series(1, 3), 'b';

Comprobar 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

Hay valores repetidos en el campo id_ de la tabla...

Intente convertir el campo id_ en una clave principal:

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 CTE y funciones de ventana, averigüe qué valores repetidos se mantendrán:

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)

Dejando la tabla con valores únicos para el campo id_, eliminando las filas más antiguas:

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)

Comprobación de los valores de la tabla sin 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

Ahora puede cambiar la tabla para dejar el campo id_ como CLAVE PRINCIPAL:

ALTER TABLE tb_foo ADD CONSTRAINT tb_foo_pkey PRIMARY KEY (id_);