Pergunta

Eu tenho que adicionar uma restrição única a uma tabela existente. Tudo bem, exceto que a tabela já possui milhões de linhas, e muitas das linhas violam a restrição única que preciso adicionar.

Qual é a abordagem mais rápida para remover as linhas ofensivas? Eu tenho uma declaração SQL que encontra os duplicados e os exclui, mas está levando uma eternidade para correr. Existe outra maneira de resolver esse problema? Talvez backup da mesa e depois restaurar após a adição da restrição?

Foi útil?

Solução

Por exemplo, você poderia:

CREATE TABLE tmp ...
INSERT INTO tmp SELECT DISTINCT * FROM t;
DROP TABLE t;
ALTER TABLE tmp RENAME TO t;

Outras dicas

Algumas dessas abordagens parecem um pouco complicadas, e eu geralmente faço isso como:

Tabela dada table, deseja exclusá -lo (Field1, Field2), mantendo a linha com o Max Field3:

DELETE FROM table USING table alias 
  WHERE table.field1 = alias.field1 AND table.field2 = alias.field2 AND
    table.max_field < alias.max_field

Por exemplo, eu tenho uma mesa, user_accounts, e quero adicionar uma restrição única no e -mail, mas tenho algumas duplicatas. Diga também que quero manter o mais recentemente criado (MAX ID entre duplicatas).

DELETE FROM user_accounts USING user_accounts ua2
  WHERE user_accounts.email = ua2.email AND user_account.id < ua2.id;
  • Observação - USING não é o SQL padrão, é uma extensão PostGresql (mas muito útil), mas a pergunta original menciona especificamente o PostGresql.

Em vez de criar uma nova tabela, você também pode reinserir linhas exclusivas para a mesma tabela depois de truncá-la. Faça tudo em uma transação. Opcionalmente, você pode soltar a tabela temporária no final da transação automaticamente com ON COMMIT DROP. Veja abaixo.

Essa abordagem só é útil, quando há muitas linhas para excluir de toda a mesa. Para apenas algumas duplicatas, use uma planície DELETE.

Você mencionou milhões de linhas. Para fazer a operação velozes você quer alocar o suficiente buffers temporários para a sessão. A configuração deve ser ajustada antes da Qualquer buffer temp é usado na sua sessão atual. Descubra o tamanho da sua mesa:

SELECT pg_size_pretty(pg_relation_size('tbl'));

Definir temp_buffers adequadamente. Recupere generosamente porque a representação na memória precisa de um pouco mais de RAM.

SET temp_buffers = 200MB;    -- example value

BEGIN;

-- CREATE TEMPORARY TABLE t_tmp ON COMMIT DROP AS -- drop temp table at commit
CREATE TEMPORARY TABLE t_tmp AS  -- retain temp table after commit
SELECT DISTINCT * FROM tbl;  -- DISTINCT folds duplicates

TRUNCATE tbl;

INSERT INTO tbl
SELECT * FROM t_tmp;
-- ORDER BY id; -- optionally "cluster" data while being at it.

COMMIT;

Este método pode ser superior a criar uma nova tabela E se dependendo de objetos existem. Visualizações, índices, chaves estrangeiras ou outros objetos referenciando a tabela. TRUNCATE faz você começar com uma lousa limpa de qualquer maneira (novo arquivo em segundo plano) e é Muito de mais rápido que DELETE FROM tbl com mesas grandes (DELETE pode realmente ser mais rápido com mesas pequenas).

Para mesas grandes, é regularmente mais rápido Para soltar índices e chaves estrangeiras, recarreva a tabela e recrie esses objetos. No que diz respeito às restrições da FK, você deve ter certeza de que os novos dados são válidos, é claro, ou você terá uma exceção ao tentar criar o FK.

Observe que TRUNCATE requer bloqueio mais agressivo do que DELETE. Isso pode ser um problema para tabelas com carga pesada e simultânea.

Se TRUNCATE não é uma opção ou geralmente para mesas pequenas a médias Existe uma técnica semelhante com um CTE modificadora de dados (Postgres 9.1+):

WITH del AS (DELETE FROM tbl RETURNING *)
INSERT INTO tbl
SELECT DISTINCT * FROM del;
-- ORDER BY id; -- optionally "cluster" data while being at it.

Mais lento para grandes mesas, porque TRUNCATE é mais rápido lá. Mas pode ser mais rápido (e mais simples!) Para mesas pequenas.

Se você não tiver objetos dependentes, pode criar uma nova tabela e excluir a antiga, mas quase não ganha nada sobre essa abordagem universal.

Para mesas muito grandes que não se encaixariam RAM disponível, criando um novo A tabela será consideravelmente mais rápida. Você terá que pesar isso contra possíveis problemas / despesas gerais com objetos dependentes.

Você pode usar OID ou CTID, que normalmente é uma coluna "não visível" na tabela:

DELETE FROM table
 WHERE ctid NOT IN
  (SELECT MAX(s.ctid)
    FROM table s
    GROUP BY s.column_has_be_distinct);

A função da janela PostGresql é útil para esse problema.

DELETE FROM tablename
WHERE id IN (SELECT id
              FROM (SELECT id,
                             row_number() over (partition BY column1, column2, column3 ORDER BY id) AS rnum
                     FROM tablename) t
              WHERE t.rnum > 1);

Ver Excluindo duplicatas.

Consulta generalizada para excluir duplicatas:

DELETE FROM table_name
WHERE ctid NOT IN (
  SELECT max(ctid) FROM table_name
  GROUP BY column1, [column 2, ...]
);

A coluna ctid é uma coluna especial disponível para cada tabela, mas não visível, a menos que mencionado especificamente. o ctid O valor da coluna é considerado exclusivo para cada linha em uma tabela.

A partir de Uma antiga lista de discussão postgreSql.org:

create table test ( a text, b text );

Valores únicos

insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );

Valores duplicados

insert into test values ( 'x', 'y');
insert into test values ( 'x', 'x');
insert into test values ( 'y', 'y' );
insert into test values ( 'y', 'x' );

Mais uma duplicata dupla

insert into test values ( 'x', 'y');

select oid, a, b from test;

Selecione linhas duplicadas

select o.oid, o.a, o.b from test o
    where exists ( select 'x'
                   from test i
                   where     i.a = o.a
                         and i.b = o.b
                         and i.oid < o.oid
                 );

Excluir linhas duplicadas

Nota: PostgreSql Dos não suporta aliases na tabela mencionada no from Cláusula de Excluir.

delete from test
    where exists ( select 'x'
                   from test i
                   where     i.a = test.a
                         and i.b = test.b
                         and i.oid < test.oid
             );

Eu apenas usei Resposta de Erwin Brandstetter Com sucesso para remover duplicatas em uma tabela de junção (uma tabela sem seus próprios IDs primários), mas descobriu que há uma ressalva importante.

Incluindo ON COMMIT DROP significa que a tabela temporária será descartada no final da transação. Para mim, isso significava que a tabela temporária era não está mais disponível Quando eu fui inserir!

eu apenas fiz CREATE TEMPORARY TABLE t_tmp AS SELECT DISTINCT * FROM tbl; E tudo funcionou bem.

A tabela temporária é descartada no final da sessão.

Esta função remove duplicata sem remover índices e faz isso em qualquer tabela.

Uso: select remove_duplicates('mytable');

---
--- remove_duplicates(tablename) removes duplicate records from a table (convert from set to unique set)
---
CREATE OR REPLACE FUNCTION remove_duplicates(text) RETURNS void AS $$
DECLARE
  tablename ALIAS FOR $1;
BEGIN
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || ' AS (SELECT DISTINCT * FROM ' || tablename || ');';
  EXECUTE 'DELETE FROM ' || tablename || ';';
  EXECUTE 'INSERT INTO ' || tablename || ' (SELECT * FROM _DISTINCT_' || tablename || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
  RETURN;
END;
$$ LANGUAGE plpgsql;
DELETE FROM table
  WHERE something NOT IN
    (SELECT     MAX(s.something)
      FROM      table As s
      GROUP BY  s.this_thing, s.that_thing);

Se você tem apenas uma ou algumas entradas duplicadas, e elas são de fato duplicado (isto é, eles aparecem duas vezes), você pode usar o "escondido" ctid coluna, como proposto acima, juntamente com LIMIT:

DELETE FROM mytable WHERE ctid=(SELECT ctid FROM mytable WHERE […] LIMIT 1);

Isso excluirá apenas a primeira das linhas selecionadas.

Primeiro, você precisa decidir sobre qual dos seus "duplicados" você manterá. Se todas as colunas forem iguais, ok, você pode excluir qualquer uma delas ... mas talvez você queira manter apenas o mais recente ou algum outro critério?

A maneira mais rápida depende da sua resposta para a pergunta acima e também da % de duplicatas da mesa. Se você jogar fora 50% de suas linhas, é melhor fazer CREATE TABLE ... AS SELECT DISTINCT ... FROM ... ;, e se você excluir 1% das linhas, o uso de delete é melhor.

Também para operações de manutenção como essa, geralmente é bom definir work_mem Para um bom pedaço da sua RAM: Explique, Explique, verifique o número N do tipo / hashes e defina Work_mem no seu RAM / 2 / N. Use muita RAM; É bom para a velocidade. Contanto que você tenha apenas uma conexão simultânea ...

Estou trabalhando com o PostgreSQL 8.4. Quando corri o código proposto, descobri que ele não estava realmente removendo as duplicatas. Ao executar alguns testes, descobri que adicionar o "distinto em (duplicate_column_name)" e a "ordem de duplicate_column_name" fez o truque. Não sou guru do SQL, encontrei isso no PostgreSQL 8.4 Selecione ... Doc distinto.

CREATE OR REPLACE FUNCTION remove_duplicates(text, text) RETURNS void AS $$
DECLARE
  tablename ALIAS FOR $1;
  duplicate_column ALIAS FOR $2;
BEGIN
  EXECUTE 'CREATE TEMPORARY TABLE _DISTINCT_' || tablename || ' AS (SELECT DISTINCT ON (' || duplicate_column || ') * FROM ' || tablename || ' ORDER BY ' || duplicate_column || ' ASC);';
  EXECUTE 'DELETE FROM ' || tablename || ';';
  EXECUTE 'INSERT INTO ' || tablename || ' (SELECT * FROM _DISTINCT_' || tablename || ');';
  EXECUTE 'DROP TABLE _DISTINCT_' || tablename || ';';
  RETURN;
END;
$$ LANGUAGE plpgsql;

Isso funciona muito bem e é muito rápido:

CREATE INDEX otherTable_idx ON otherTable( colName );
CREATE TABLE newTable AS select DISTINCT ON (colName) col1,colName,col2 FROM otherTable;
DELETE FROM tablename
WHERE id IN (SELECT id
    FROM (SELECT id,ROW_NUMBER() OVER (partition BY column1, column2, column3 ORDER BY id) AS rnum
                 FROM tablename) t
          WHERE t.rnum > 1);

Exclua duplicatas por coluna (s) e mantenha a linha com o menor ID. O padrão é retirado do Postgres Wiki

Usando CTEs, você pode alcançar uma versão mais legível do acima através disso

WITH duplicate_ids as (
    SELECT id, rnum 
    FROM num_of_rows
    WHERE rnum > 1
),
num_of_rows as (
    SELECT id, 
        ROW_NUMBER() over (partition BY column1, 
                                        column2, 
                                        column3 ORDER BY id) AS rnum
        FROM tablename
)
DELETE FROM tablename 
WHERE id IN (SELECT id from duplicate_ids)
CREATE TABLE test (col text);
INSERT INTO test VALUES
 ('1'),
 ('2'), ('2'),
 ('3'),
 ('4'), ('4'),
 ('5'),
 ('6'), ('6');
DELETE FROM test
 WHERE ctid in (
   SELECT t.ctid FROM (
     SELECT row_number() over (
               partition BY col
               ORDER BY col
               ) AS rnum,
            ctid FROM test
       ORDER BY col
     ) t
    WHERE t.rnum >1);
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top