Показаны сообщения с ярлыком Как удалить дубликаты строк при наличии первичного ключа?. Показать все сообщения
Показаны сообщения с ярлыком Как удалить дубликаты строк при наличии первичного ключа?. Показать все сообщения

четверг, 23 февраля 2017 г.

Как удалить дубликаты строк при наличии первичного ключа?

Как удалить дубликаты строк при наличии первичного ключа?

    Моисеенко С.И. (25-07-2009)
В предыдущей статье мы рассмотрели решение проблемы с дубликатами, вызванной отсутствием первичного ключа. Рассмотрим теперь более тяжелый случай, когда ключ вроде бы есть, но он является синтетическим, что при неправильном проектировании тоже может привести к появлению дубликатов с точки зрения предметной области.
Странное дело, но, рассказывая на лекциях о недостатках синтетических ключей, я, тем не менее, постоянно сталкиваюсь с тем, что студенты в своих первых проектах с базами данных их неизменно используют. Видимо, в человеке заложена генетическая потребность все перенумеровывать, и помочь здесь может только психотерапевт. :-)
Я не хочу обсуждать здесь избитую проблему синтетических ключей. Скажу лишь, что если вы решили использовать их в качестве первичного ключа, то следует также создавать естественный уникальный ключ, чтобы избежать описанной ниже ситуации.
Итак, пусть имеется таблица с первичным ключом id и столбцом name, который в соответствии с ограничениями предметной области должен содержать уникальные значения. Однако если определить структуру таблицы следующим образом

CREATE TABLE T_pk (id INT IDENTITY PRIMARY KEY,
     name VARCHAR(50));
то появлению дубликатов ничто не препятствует. Следовало бы использовать следующую структуру таблицы:

CREATE TABLE T_pk (id INT IDENTITY PRIMARY KEY,
     name VARCHAR(50) UNIQUE);
Все знают, как нужно правильно поступить, однако зачастую приходится иметь с "унаследованной" структурой и данными, которые нарушают ограничения предметной области. Вот пример:
id    name  
1 John
2 Smith
3 John
4 Smith
5 Smith
6 Tom
Вы можете спросить: "А чем эта проблема отличается от предыдущей? Ведь здесь есть даже более простое решение - просто удалить все строки из каждой группы с одинаковыми значениями в столбце name, оставив лишь строку с минимальным/максимальным значением id. Например, так:"

DELETE
FROM T_pk
WHERE id > (SELECT MIN(id) FROM T_pk X WHERE X.name = T_pk.name);
Правильно, но я вам еще не все рассказал. :-) Представьте, что у нас имеется дочерняя таблица T_details, связанная с таблицей T_pk по внешнему ключу:

CREATE TABLE T_details (id_pk INT FOREIGN KEY REFERENCES
 T_pk ON DELETE CASCADE,
 color VARCHAR(10),
 PRIMARY KEY (id_pk, color);
Эта таблица может содержать такие данные:
id_pk    color  
1 blue
1 red
2 green
2 red
3 red
4 blue
6 red
Для большей наглядности воспользуемся запросом
SELECT id, name, color FROM T_pk JOIN T_details ON id= id_pk; 
чтобы увидеть имена:
id    name    color   
1 John blue
1 John red
2 Smith green
2 Smith red
3 John red
4 Smith blue
6 Tom red
Таким образом, оказывается, что данные, фактически относящиеся к одному лицу, ошибочно оказались разнесенными по разным родительским записям. Кроме того, дубликаты оказались и в этой таблице:
1 John red
3 John red
Очевидно, что подобные данные приведут к ошибочному анализу и отчетам. Более того, каскадное удаление приведет к потере данных. Например, если мы оставим только строки с минимальным идентификатором в каждой группе в таблице T_pk, то потеряем строку
4 Smith blue
в таблице T_details. Следовательно, мы должны при устранении дубликатов учитывать обе таблицы.
Процедуру "очистки" данных можно провести в два этапа:
  1. Выполнить обновление таблицы T_details, приписав данные, относящиеся к одному имени, к id с минимальным номером в группе.
  2. Удалить дубликаты из таблицы T_pk, оставив только строки с минимальным id в каждой группе с одинаковым значением в столбце name.

Обновление таблицы T_details

Запрос

SELECT id_pk, name, color
    , RANK() OVER(PARTITION BY name, color ORDER BY name, color, id_pk) dup
    ,(SELECT MIN(id)  FROM T_pk WHERE T_pk.name = X.name) min_id
FROM T_pk X JOIN T_details ON id=id_pk;
определяет наличие дубликатов (значение dup > 1) и минимальное значение id в группе одинаковых имен (min_id). Вот результат выполнения этого запроса:
id_pk   name    color   dup  min_id   
1 John blue 1 1
1 John red 1 1
3 John red 2 1
4 Smith blue 1 2
2 Smith green 1 2
2 Smith red 1 2
6 Tom red 1 6
Теперь нам нужно заменить значение id_pk значением min_pk для всех строк, кроме третьей, т.к. эта строка есть дубликат второй строки, о чем говорит значение dup=2. Запрос на обновление можно написать так:

UPDATE T_details
SET id_pk=min_id
FROM T_details T_d JOIN (
  SELECT id_pk, name, color
      , RANK() OVER(PARTITION BY name, color ORDER BY name, color, id_pk) dup
      ,(SELECT MIN(id)  FROM T_pk WHERE T_pk.name = X.name) min_id
  FROM T_pk X JOIN T_details ON id=id_pk
      ) Y ON Y.id_pk=T_d.id_pk
WHERE dup =1;
После обновления таблица T_details примет следующий вид:
id_pk   color
1 blue
1 red
2 blue
2 green
2 red
3 red
6 red
Как видно, осталась одна лишняя дубликатная строка:
3 red
Но о ней можно не беспокоиться, так она будет удалена автоматически при каскадном удалении дубликатов из таблицы T_pk:

DELETE
FROM T_pk
WHERE id > (SELECT MIN(id) FROM T_pk X WHERE X.name = T_pk.name);
Последний запрос и является вторым этапом процедуры, в результате выполнения которого мы получим:
Таблица T_pk
id    name  
1 John
2 Smith
6 Tom

Таблица T_details
id_pk   color 
1 blue
1 red
2 blue
2 green
2 red
6 red
Осталось только наложить ограничение, чтобы избежать появления дубликатов в дальнейшем:

ALTER TABLE T_pk
ADD CONSTRAINT unique_name UNIQUE(name);