Generally speaking, I want to group by one column OR by another one:
SELECT count(*)
FROM foo
GROUP BY column1 OR column2
In particular, I want to identify duplicates within a given table, below an example with minimal data.
Minimal example
| id | name | birth_date | email |
|---:|:-----------|:-------------|:-----------------------------|
| 1 | 'Gamow' | '1904-03-04' | '[email protected]' |
| 2 | 'Gamow' | NULL | NULL |
| 3 | 'Gamow' | '1904-03-04' | NULL |
| 4 | 'Gamow' | '1904-03-04' | '[email protected]' |
| 5 | 'Gamow' | NULL | '[email protected]' |
| 6 | 'Feynman' | '1918-05-11' | '[email protected]' |
| 7 | 'Feynman' | '1918-05-11' | NULL |
| 8 | 'Poincaré' | '1854-04-29' | '[email protected]' |
Two people are considered duplicates if they have the same name AND (the same birth date OR the same email).
I would like to find a query who gives
MIN(id)
among the duplicates: the line to keep- the 1+ other ids: the line(s) to delete
I wrote this query
SELECT
MIN(p.id) AS id_tokeep,
REPLACE(
GROUP_CONCAT(p.id ORDER BY p.id ASC SEPARATOR ','),
CONCAT(MIN(p.id), ','),
''
) AS ids_todelete,
MIN(p.name) AS name
FROM people AS p
WHERE p.birth_date IS NOT NULL OR p.email IS NOT NULL
GROUP BY p.name, (p.birth_date IS NOT NULL) OR (p.email IS NOT NULL)
HAVING COUNT(id) > 1
ORDER BY id_tokeep;
which works:
| id_tokeep | ids_todelete | name |
|----------:|--------------|-----------|
| 1 | '3,4,5' | 'Gamow' |
| 6 | '7' | 'Feynman' |
except:
- it feels like a hack (as I didn’t find anywhere the use of
OR
inside aGROUP BY
clause), - it sometimes generate a warning «
Truncated incorrect DOUBLE value: '[email protected]'
» - with real data, it returns « strange » results (which seems to confirm that it is a hack).
Hence, my questions are:
- is it « legal » to use OR within a GROUP BY clause?
- if not, how to re-write this query without the OR within the GROUP BY?
SQL code to generate the minimal example
DROP TABLE IF EXISTS people;
CREATE TABLE people (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
birth_date DATE DEFAULT NULL,
email VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO people (name, birth_date, email)
VALUES
('Gamow', '1904-03-04', '[email protected]'), -- 1
('Gamow', NULL, NULL), -- 2
('Gamow', '1904-03-04', NULL), -- 3
('Gamow', '1904-03-04', '[email protected]'), -- 4
('Gamow', NULL, '[email protected]'), -- 5
('Feynman', '1918-05-11', '[email protected]'), -- 6
('Feynman', '1918-05-11', NULL), -- 7
('Poincaré', '1854-04-29', '[email protected]') -- 8
;
Other trial query
I tried with two self-join, but I then struggle to group the duplicates:
SELECT
p1.id AS patient_id,
p2.id AS patient_match_birth_date_id,
p3.id AS patient_match_email_id
FROM people AS p1
JOIN people AS p2 ON p2.birth_date = p1.birth_date
JOIN people AS p3 ON p3.email = p1.email
ORDER BY p1.id, p2.id, p3.id;
I tried with a union between the duplicate on birth date and the duplicate on email, but it returns retundant data:
WITH patient_matches AS (
(
SELECT
p1.id AS patient_id,
p2.id AS patient_match_id
FROM people AS p1
JOIN people AS p2 ON p2.birth_date = p1.birth_date
ORDER BY p1.id ASC
)
UNION ALL
(
SELECT
p1.id AS patient_id,
p2.id AS patient_match_id
FROM people AS p1
JOIN people AS p2 ON p2.email = p1.email
ORDER BY p1.id ASC
)
ORDER BY patient_match_id
)
SELECT json_arrayagg(pm.patient_id)
FROM patient_matches AS pm
GROUP BY pm.patient_match_id;
OR
produces true/false value, wheretrue
is represented as1
andfalse
as '0. What do you _want_
OR` to mean insideGROUP BY
?OR
would be understand within theGROUP BY
context (i.e. semantically different from the basic logicalOR
), but it’s not. Hence it is not possible to do what I want with a simpleGROUP BY
and I find another way with mainly a self-JOIN
with specificON
clause.