Группировать строки базы данных по одному из двух столбцов - PullRequest
0 голосов
/ 30 января 2020

Цель

Я пытаюсь написать запрос для поиска повторяющихся строк. Строка дублируется, если столбец A или столбец B одинаковы.

Запись так, что оба должны быть одинаковыми, проста; просто GROUP BY A, B.

Однако фильтрация по одному из двух оказывается немного сложнее. Как можно go сделать это?

Я пробовал следующее:

select distinct a as col_a,
                b as col_b,
                (
                    select count(*)
                    from table_name
                    where a = col_a
                       or b = col_b
                ) as duplicate_count
from table_name
having duplicate_count > 1;

, но это не похоже на правильный путь к go об этом и с 84 000 строк это тоже очень медленно.

Пример

Со следующей таблицей:

+----+------------------------+---+---------+
| id | name                   | a | b       |
+----+------------------------+---+---------+
| 1  | Lorem ipsum            | 1 | Donec   |
+----+------------------------+---+---------+
| 2  | dolor sit              | 2 | rhoncus |
+----+------------------------+---+---------+
| 3  | amet                   | 3 | rhoncus |
+----+------------------------+---+---------+
| 4  | consectetur adipiscing | 1 | primis  |
+----+------------------------+---+---------+
| 5  | vulputate cursus       | 4 | Aliquam |
+----+------------------------+---+---------+

Либо результат 1, либо 4 (тот же A) и результат 2 или 3 (тот же B) должны быть возвращены, оба с duplicate_count из 2. Какой из двух «дубликатов» будет возвращен, не имеет значения.

Версии

На моей локальной машине я использую MySQL 5.7.24. Я только что проверил живой сервер, он использует 10.1.43-MariaDB.

Ответы [ 2 ]

2 голосов
/ 30 января 2020

Вы уже знаете, что этот запрос:

select a, b
from tablename
group by a, b
having count(*) > 1

возвращает дубликаты с одинаковыми значениями a и b. Вы можете получить остальные дубликаты по вашему требованию с EXISTS:

select t.a, t.b
from tablename t
where exists (
  select 1 from tablename
  where (a = t.a and b <> t.b) or (a <> t.a and b = t.b)
)

Или, если хотите, все они используют UNION ALL:

select a, b
from tablename
group by a, b
having count(*) > 1
union all
select t.a, t.b
from tablename t
where exists (
  select 1 from tablename
  where (a = t.a and b <> t.b) or (a <> t.a and b = t.b)
)

Обновить: Если у вас есть столбец ID, используйте EXISTS следующим образом:

select t.*
from tablename t
where exists (
  select 1 from tablename
  where id <> t.id and (a = t.a or b = t.b)
)

Или, если вы хотите, чтобы только один из дубликатов использовал id > t.id вместо id <> t.id. Смотрите демо . Или с самостоятельным присоединением:

select t.*
from tablename t inner join tablename tt
on (tt.a = t.a or tt.b = t.b) and tt.id <> t.id
0 голосов
/ 31 января 2020

Следующее решение работает:

Еще одна демонстрация со строкой, которая имеет дублирование в a и b

CREATE TEMPORARY TABLE ab_duplicates (
a INTEGER
) AS 
SELECT  a, count(*) as cnt
FROM tablename
group by a, b
Having cnt > 1;
ALTER TABLE ab_duplicates ADD INDEX (a);

-- Select duplicates for a, but not for a and b
SELECT id, name, a, b
FROM   (SELECT x.*, t.id, t.name, t.a, t.b, 
               @rn := IF(t.a = @a, @rn + 1, 1) rn,
               @a := t.a,
               ab.a as ab_exists
          FROM (select @a := null, @rn := 0) x, 
               tablename t
                LEFT JOIN ab_duplicates ab on ab.a = t.a 
       ORDER BY a
    ) a_duplicates
where rn = 2 and ab_exists is null
UNION
-- union duplicates for b, including duplicates for a and b
SELECT id, name, a, b
FROM   (SELECT x.*, t.id, t.name, t.a, t.b, 
               @rn := IF(t.b = @b, @rn + 1, 1) rn,
               @b := t.b
          FROM (select @b := null, @rn := 0) x, 
               tablename t
       ORDER BY b
    ) b_and_ab_duplicates
where rn = 2;

Предыдущие решения, которые работали только в некоторых крайних случаях

Использование group by и count ():

Первый поиск идентификаторов с дубликатами для a:

SELECT min(id) id, count(*) cnt from tablename t group by a having cnt > 1
-- this will work better if you have an index starting with a

То же самое с b:

SELECT min(id) id, count(*) cnt from tablename t group by b having cnt > 1
-- this will work better if you have an index starting with b

Первый решение:

Union дает идентификаторы, в которых есть дубликаты для a или b, требуется 2 индекса)

SELECT min(id) id, count(*) cnt from tablename t group by a having cnt > 1
UNION
SELECT min(id) id, count(*) cnt from tablename t group by b having cnt > 1

Используйте идентификаторы для фильтрации таблицы, если вам нужно больше данных из таблицы:

SELECT tablename.*
FROM (
SELECT min(id) id, count(*) cnt from tablename t group by a having cnt > 1
UNION
SELECT min(id) id, count(*) cnt from tablename t group by b having cnt > 1
) as ids
JOIN tablename on tablename.id = ids.id

Теперь это может не использовать индекс, но вы можете использовать временную таблицу, чтобы иметь ее:

Первое решение с использованием временной таблицы (может быть быстрее):

-- using a temporary table to set an index
CREATE TEMPORARY TABLE ids (
-- adds an index on id, for the JOIN in the result query
`id` INTEGER PRIMARY KEY 
) as 
SELECT id 
FROM (
-- duplicates on a, requires an index (a) on tablename
SELECT min(id) id, count(*) cnt from tablename t group by a having cnt > 1
-- removes duplicates between both part of the UNION : this might be slow
-- if there cannot be duplicates on a and b at the same time, consider using UNION ALL
UNION 
-- duplicates on b, requires an index (b) on tablename
SELECT min(id) id, count(*) cnt from tablename t group by b having cnt > 1
) tempids;

SELECT tablename.*
FROM ids -- using the temporary table, MUST be in the same database connection, will filter duplicates
JOIN tablename on tablename.id = ids.id;

Я не знаю, лучше ли устанавливать индекс для временной таблицы, чем устанавливать индекс после заполнения данных:

-- you might want to postpone the index after the ids are set
-- using a temporary table to set an index
CREATE TEMPORARY TABLE ids2 (
`id` INTEGER
) as 
SELECT id 
FROM (
-- duplicates on a, requires an index (a) on tablename
SELECT min(id) id, count(*) cnt from tablename t group by a having cnt > 1
-- removes duplicates between both part of the UNION : this might be slow
-- if there cannot be duplicates on a and b at the same time, consider using UNION ALL
UNION 
-- duplicates on b, requires an index (b) on tablename
SELECT min(id) id, count(*) cnt from tablename t group by b having cnt > 1
) tempids;

ALTER TABLE ids2 ADD INDEX (id);

SELECT tablename.*
FROM ids2 -- using the temporary table, MUST be in the same database connection, will filter duplicates
JOIN tablename on tablename.id = ids2.id;

С mariadb 10.2 или mysql 8 вы можете использовать окно функция (я думаю).

Другое решение: использование vars:

SELECT id, name, a, b, rn
FROM   (SELECT *, 
               @rn := IF(a = @a, @rn + 1, 1) rn,
               @a := a
          FROM (select @a := null, @rn := 0) x, 
               tablename
       ORDER BY a
    ) a_duplicates
where rn = 2
UNION 
SELECT id, name, a, b, rn
FROM   (SELECT *, 
               @rn := IF(b = @b, @rn + 1, 1) rn,
               @b := b
          FROM (select @b := null, @rn := 0) x, 
               tablename
       ORDER BY b
    ) b_duplicates
where rn = 2

Демо: с некоторыми дополнительными шагами для понимания

Редактировать: это работает, только если вы не есть строки, где а и б дубликаты. Который имеет место в примере.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...