Поиск и работа с дублирующимися пользователями
В большой базе данных пользователей со следующим форматом и примерными данными мы пытаемся идентифицировать дублированных людей:
id first_name last_name email
---------------------------------------------------
1 chris baker
2 chris baker [email protected]
3 chris baker [email protected]
4 chris baker [email protected]
5 carl castle [email protected]
6 mike rotch [email protected]
Я использую следующий запрос:
SELECT
GROUP_CONCAT(id) AS "ids",
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
COUNT(*) AS "duplicate_count"
FROM
users
GROUP BY
name
HAVING
duplicate_count > 1
Это отлично работает; Я получаю список дубликатов с номерами идентификаторов входящих строк.
Мы повторно назначили бы связанные данные, привязанные к дубликату, фактическому человеку (set user_id = 2 where user_id = 3
), а затем удалим повторяющуюся строку пользователя.
Проблема возникает после того, как мы сделаем этот отчет в первый раз, так как мы очищаем список после проверки вручную, что они действительно дубликаты - некоторые из них НЕ дублируются. Есть 2 Chris Bakers, которые являются законными пользователями.
Мы не хотим видеть Криса Бейкера в последующих дублированных отчетах до конца времени, поэтому я ищу способ указать, что идентификатор пользователя 1 и идентификатор пользователя 4 НЕ являются дубликатами друг друга для будущих отчетов, но они могут быть дублированы новыми пользователями, добавленными позже.
Что я пробовал
Я добавил в таблицу пользователя поле is_not_duplicate
, но если в базу данных будет добавлен новый дубликат "Chris Baker", это приведет к тому, что эта ситуация не будет отображаться в дублированном отчете; is_not_duplicate
неправильно исключает одну из учетных записей. Оператор HAVING
не будет соответствовать порогу > 1
, пока не появятся -дво-дубликаты Криса Бейкера, плюс "реальный", отмеченный is_not_duplicate
.
Вопрос суммирован
Как я могу создать исключения в указанном выше запросе без результатов цикла или нескольких запросов?
Подзапросы в порядке, но размер набора данных делает каждый подсчет количества запросов, и я хотел бы, чтобы решение было максимально эффективным.
Ответы
Ответ 1
Попробуйте добавить логическое поле is_not_duplicate
и изменить код следующим образом:
SELECT
GROUP_CONCAT(id) AS "ids",
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
COUNT(*) AS "duplicate_count",
SUM(is_not_duplicate) AS "real_count"
FROM
users
GROUP BY
name
HAVING
duplicate_count > 1
AND
duplicate_count - real_count > 0
Добавленные дубликаты будут иметь is_not_duplicate=0
, поэтому real_count
для этого имени будет меньше duplicate_count
, и строка будет показана
Ответ 2
Мой мозг слишком обжарен, чтобы придумать фактический запрос для этого на данный момент, но я мог бы дать вам толчок в пути, который должен работать:)
Что делать, если вы добавили еще один столбец (может быть, таблица действительных дублированных пользователей вместо этого?... оба будут выполнять одно и то же) и выполнили подзапрос, который будет подсчитывать все допустимые дубликаты, а затем вы можете сравнить с счет в текущем запросе. Вы исключили бы всех пользователей, у которых есть соответствующие счетчики, и будут тянуть любые с более высокими значениями. Надеюсь, это имеет смысл; Я создам прецедент:
- Крис Бейкер с идентификаторами 1 и 4 помечены как valid_duplicates
- В системе есть 4 Chris Baker
- Вы получаете счет действительного Криса Бейкера
- Вы получаете кол-во всех вещей Криса Бейкера
- valid_count < > total_count, поэтому верните Chris Baker
* Возможно, вы даже можете изменить запрос таким образом, чтобы он даже не отображал дублированный идентификатор (даже если вы получили дублирующее обозначение всего 1 id). Вместо того, чтобы перепроверять, какие именно валиды. Это было бы немного сложнее. Без этого, по крайней мере, вы игнорируете Криса Бейкера, пока другой не войдет в систему.
Я написал основной запрос, касающийся исключения конкретного идентификатора, который я попытаюсь выполнить сегодня вечером. Но это, по крайней мере, решает вашу первоначальную потребность. Если вам не нужен более сложный запрос, сообщите мне, чтобы я не тратил время на это:)
SELECT
GROUP_CONCAT(id) AS "ids",
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
COUNT(*) AS "duplicate_count"
FROM
users
WHERE NOT EXISTS
(
SELECT 1
FROM
(
SELECT
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
COUNT(*) AS "valid_duplicate_count"
FROM
users
WHERE
is_valid_duplicate = 1 --true
GROUP BY
name
HAVING
valid_duplicate_count > 1
) AS duplicate_users
WHERE
duplicate_users.name = users.name
AND valid_duplicate_count = duplicate_count
)
GROUP BY
name
HAVING
duplicate_count > 1
Ниже приведен запрос, который должен делать то же самое, что и выше, но в конечном списке будет отображаться только идентификатор, который не находится в допустимом списке. На самом деле это оказалось намного проще, чем я думал. И это в основном то же самое, что и выше, но единственная причина, по которой я держал выше, - это сохранить два варианта, и в случае, если я испортил вышеописанное... он усложняется, так как много вложенных запросов. Если CTE доступны для вас или даже временные таблицы. Это может сделать запрос более выразительным, чтобы разбить его на временные таблицы:). Надеюсь, это поможет и является тем, что вы ищете.
SELECT GROUP_CONCAT(id) AS "ids",
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
COUNT(*) AS "final_duplicate_count"
--This count could actually be 1 due to the nature of the query
FROM
users
--get the list of duplicated user names
WHERE EXISTS
(
SELECT
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
COUNT(*) AS "total_duplicate_count"
FROM
users AS total_dup_users
--ignore valid_users whose count still matches
WHERE NOT EXISTS
(
SELECT 1
FROM
(
SELECT
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
COUNT(*) AS "valid_duplicate_count"
FROM
users AS valid_users
WHERE
is_valid_duplicate = 1 --true
GROUP BY
name
HAVING
valid_duplicate_count > 1
) AS duplicate_users
WHERE
--join inner table to outer table
duplicate_users.name = total_dup_users.name
--valid count check
AND valid_duplicate_count = total_duplicate_count
)
--join inner table to outer table
AND total_dup_users.Name = users.Name
GROUP BY
name
HAVING
duplicate_count > 1
)
--ignore users that are valid when doing the actual counts
AND NOT EXISTS
(
SELECT 1
FROM users AS valid
WHERE
--join inner table to outer table
users.name =
CONCAT(UPPER(valid.first_name), UPPER(valid.last_name))
--only valid users
AND valid.is_valid_duplicate = 1 --true
)
GROUP BY
FinalDuplicates.Name
Ответ 3
Так как это в основном отношение "многие ко многим", я бы добавил новую таблицу not_duplicate
с полями user1
и user2
.
Я бы добавил две строки для каждого отношения not_duplicate
, так что у меня есть одна строка для 2 -> 3
и симметричная строка для 3 -> 2
, чтобы облегчить запрос, но это может привести к несогласованности данных, поэтому убедитесь, что вы удаляете обе строки в то же время (или иметь только одну строку и сделать правильный запрос в script).
Ответ 4
Мне кажется, что столбец is_not_duplicate недостаточно сложный, чтобы хранить информацию, которую вы хотите сохранить, - из того, что, как я понимаю, вы хотите вручную сообщить своему детектору, что два разных пользователя не являются дубликатами друг друга. так что либо вы создаете столбец типа is_not_duplicate_of = other-user-id, либо если вы хотите сохранить возможность открывать, что один пользователь может быть вручную определен не дублировать более чем одного пользователя, вам нужна отдельная таблица с двумя столбцами идентификатора пользователя.
запрос, говорящий вам, что неперекрываемые дубликаты, вероятно, должны быть немного сложнее, чем тот, который вы предложили, я не могу думать о том, что работает с группой и имеет логику. Единственное, что мне пришло в голову, это что-то вроде
SELECT u1.* FROM users u1
INNER JOIN users u2
ON u1.id <> u2.id
AND u2.name = u1.name
WHERE NOT EXISTS (
SELECT *
FROM users_non_dups un
WHERE (un.id1 = u1.id AND un.id2 = u2.id)
OR (un.id1 = u2.id AND un.id2 = u1.id)
)
Ответ 5
Если вы будете исправлять все дубликаты при каждом запуске отчета, то очень простым решением может быть изменение запроса:
SELECT
GROUP_CONCAT(id) AS "ids",
MAX(id) AS "max_id",
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name",
COUNT(*) AS "duplicate_count"
FROM
users
GROUP BY
name
HAVING
duplicate_count > 1
AND
max_id > MAX_ID_LAST_TIME_DUPLICATE_REPORT_WAS_GENERATED;
Ответ 6
Я бы продолжил и сделал столбец "confirm_unique", по умолчанию "False".
Чтобы избежать проблем, о которых вы говорили,
Затем я выберу все элементы, которые могут выглядеть как дубликаты, и введите "False" для "confirm_unique".
Ответ 7
Я не уверен, что это сработает, но вы могли бы рассмотреть обратную логику добавления столбца * is_duplicate_of *? Таким образом, вы можете пометить дубликаты, введя идентификатор первой записи в этом столбце, который будет больше нуля. Записи, которые вы хотите сохранить, будут иметь значение 0 в этом поле. Вы можете установить значения по умолчанию (непроверенные записи) на -1, чтобы отслеживать статус проверки для каждой записи.
Впоследствии вы можете продолжать выполнять SQL, который будет сравнивать новые записи только с правильными записями с is_duplicate_of = 0.
Ответ 8
Если вы согласитесь внести небольшое изменение в формат отчета. Вы можете сделать это самостоятельно -
SELECT
CONCAT(u1.id,",", u2.id) AS "ids",
CONCAT(UPPER(u1.first_name), UPPER(u1.last_name)) AS "name"
FROM
users u1, users u2
WHERE
u1.id < u2.id AND
UPPER(u1.first_name) = UPPER(u2.first_name) AND
UPPER(u1.last_name) = UPPER(u2.last_name) AND
CONCAT(u1.id,",", u2.id) NOT IN (SELECT ids from not_dupe)
который сообщает о дубликатах следующим образом:
ids | name
----|--------
1,2 | CHRISBAKER
1,3 | CHRISBAKER
...
И таблица not_dupe имела бы строки, как показано ниже:
ids
------
1,2
3,4
...
Ответ 9
Я думаю, было бы разумно создать таблицу поиска, в которой хранятся идентификаторы тех, которые не дублируются. Таким образом, подтвержденные не дубликаты удаляются, и запрос будет иметь лишь небольшой поиск дубликатов, найденных в таблице поиска.
например, в этом примере мы имели бы
id 1 | id 2
2 4
если [email protected] и [email protected] являются разными людьми.
Ответ 10
Если бы я был вами, я добавлю некоторые таблицы/поля геолокализации в мою схему базы данных.
Вероятность, что два конечных пользователя имеют одинаковые имена И живут в одном и том же месте очень очень низкие - за исключением очень большого города - но вы также можете разделить геолокализацию на небольшие области - это о детализации.
Удачи.
Ответ 11
Я предлагаю вам создать пару вещей:
- Булевский столбец для подтверждения подтвержденных пользователей
- Строковый столбец для сохранения идентификаторов
- Триггер, который проверяет, находятся ли уже имя и фамилия, чтобы заполнить этот флаг, и сохраните в столбце строки все идентификаторы, для которых это возможно.
И затем создайте отчет, который ищет дублирующиеся истинные и декодирует поле строки, чтобы соответствовать возможному дублируемому
Ответ 12
Я дал Джастину Пихони +1 в качестве 1-го, чтобы предложить сравнить дублирующее количество с не дублирующимся счетом, а Грант Хачатрян +1 за то, что он первым показал эффективный способ сделать это.
Вот несколько другой метод, плюс некоторое переименование, чтобы сделать все более понятным для себя, плюс несколько дополнительных столбцов в запросе, чтобы было очевидно, какие записи нужно сравнивать как потенциальные дубликаты.
Я бы назвал новый столбец "CONFIRMED_UNIQUE" вместо "IS_NOT_DUPLICATE". Как и Грант, я бы сделал его логическим (tinyint (1) с 0 = FALSE и 1 = TRUE).
"потенциал_duplicate_count" - это максимальное количество записей, которые необходимо удалить.
select
group_concat(case when not confirmed_unique then id end) as potential_duplicate_ids,
group_concat(case when confirmed_unique then id end) as confirmed_unique_ids,
concat(upper(first_name), upper(last_name)) as name,
sum( case when not confirmed_unique then 1 end ) - (not max(confirmed_unique)) as potential_duplicate_count
from
users
group by
name
having
potential_duplicate_count > 0
Ответ 13
Я вижу, что кто-то еще проголосовал за предложение о слиянии, но ничего о вашем заявлении о проблемах не говорит о том, что данные должны быть на месте. OP следит за их решением, которое, оказывается, является SQL-кодом, что не означает, что каждое решение должно быть ограничено этим.
Проблема, как я понимаю, связана с контактами, имеющими несколько, похожих, но не обязательно идентичных записей в вашей базе данных, которые имеют стоимость и репутационные последствия, поэтому вы ищете дедупликацию этих записей.
Я бы написал пакетное задание, которое ищет потенциальные дубликаты (это может быть так сложно или просто, как вам нравится), а затем закрыть две записи, которые он находит, обманы и создать новую запись.
Чтобы включить это, вам понадобится четыре новых столбца:
- Статус, который будет либо открытым, объединенным, разделенным
- RelatedId, который будет содержать значение того, с кем запись была объединена с
- ChainId, новый идентификатор записи
- DateStatusChanged, достаточно очевидно
Открыть будет статус по умолчанию
Объединенный будет, когда запись будет объединена (эффективно закрыта и заменена)
Разделить будет, если слияние было отменено
Итак, в качестве примера, просмотрите все записи, которые, например, имеют одно и то же имя. Объедините их парами. Итак, если у вас есть три Chris Bakers, записи 1, 2 и 3, объедините 1 и 2, чтобы сделать запись 4, а затем 3 и 4, чтобы сделать запись 5. В вашей таблице будет что-то вроде:
ID NAME STATUS RELATEDID CHAINID DATESTATUSCHANGED [other rows omitted]
1 Chris Baker MERGED 2 4 27-AUG-2012
2 Chris Baker MERGED 1 4 27-AUG-2012
3 Chris Baker MERGED 4 5 28-AUG-2012
4 Chris Baker MERGED 3 5 28-AUG-2012
5 Chris Baker OPEN
Таким образом, у вас есть полная запись о том, что произошло с вашими данными, можно отменить любые изменения путем несмывания, если, например, контакты 1 и 2 не были одинаковыми, вы отменили слияние 3 и 4, измените слияние 1 и 2, вы получите следующее:
ID NAME STATUS RELATEDID CHAINID DATESTATUSCHANGED
1 Chris Baker SPLIT 2 4 29-AUG-2012
2 Chris Baker SPLIT 1 4 29-AUG-2012
3 Chris Baker SPLIT 4 5 29-AUG-2012
4 Chris Baker CLOSED 3 5 29-AUG-2012
5 Chris Baker CLOSED 29-AUG-2012
Затем вы можете вручную объединить, поскольку вы, вероятно, не хотите, чтобы ваша работа автоматически удаляла разделенные записи.
Ответ 14
Есть ли веская причина не объединять дубликаты учетных записей в одну учетную запись?
Из комментариев, похоже, что информация используется в основном для контактной информации, поэтому слияние должно быть относительно безболезненным и низким риском. После объединения пользователей они больше не будут отображаться в вашем дублированном отчете. Кроме того, таблица пользователей фактически уменьшится, что может помочь в производительности.
Ответ 15
Добавьте is_not_duplicate
по типу datatype в таблицу и используйте ниже запрос после значения is_not_duplicate
данных:
SELECT GROUP_CONCAT(id) AS "ids",
CONCAT(UPPER(first_name), UPPER(last_name)) AS "name"
FROM users
GROUP BY name
HAVING COUNT(*) > SUM(CAST(is_not_duplicate AS INT))
выше запрос сопоставляет полные повторяющиеся строки по полному количеству повторяющихся строк.
Ответ 16
Почему бы вам не сделать столбец электронной почты уникальным идентификатором в этом случае, и после того, как вы очистите свои записи один раз, вы не разрешаете дубликаты оттуда?