-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path01-duplicate-contacts.sql
More file actions
126 lines (107 loc) · 4.81 KB
/
Copy path01-duplicate-contacts.sql
File metadata and controls
126 lines (107 loc) · 4.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/*
Query Name: Duplicate Contacts
Category: Hygiene
Purpose:
Identify contacts sharing the same email address. Duplicates inflate
list size metrics, cause contacts to receive the same message multiple
times, and corrupt engagement scoring by splitting a single person's
event history across multiple records.
Use Case:
Run before any large import or campaign send. The output provides the
full set of duplicate groups with each contact's creation date so you
can determine which record to keep (typically the oldest, or the one
with the most complete data). Export to a spreadsheet for manual review
or pipe into a deduplication script that merges activity history before
deleting the duplicate.
Tables Used:
contacts — email field as the deduplication key
*/
WITH duplicate_emails AS (
-- Identify email addresses that appear more than once.
-- NULL emails are excluded — they represent a separate data quality
-- issue covered by the Missing Required Fields query.
SELECT
LOWER(TRIM(email)) AS normalized_email,
COUNT(contact_id) AS duplicate_count
FROM contacts
WHERE email IS NOT NULL
AND TRIM(email) != ''
GROUP BY LOWER(TRIM(email))
HAVING COUNT(contact_id) > 1
),
duplicate_contacts AS (
-- Return all contacts belonging to a duplicate email group.
-- Ordered by email then created_at so the oldest record appears first
-- within each group — a common heuristic for which record to retain.
SELECT
c.contact_id,
c.email,
LOWER(TRIM(c.email)) AS normalized_email,
c.first_name,
c.last_name,
c.lifecycle_stage,
c.lead_source,
c.created_at,
c.updated_at,
de.duplicate_count,
-- Rank within duplicate group: rank 1 = oldest record (retain candidate)
ROW_NUMBER() OVER (
PARTITION BY LOWER(TRIM(c.email))
ORDER BY c.created_at ASC
) AS rank_in_group
-- BigQuery: ROW_NUMBER() OVER (...) is supported with identical syntax
FROM contacts c
INNER JOIN duplicate_emails de
ON LOWER(TRIM(c.email)) = de.normalized_email
)
SELECT
contact_id,
email,
first_name,
last_name,
lifecycle_stage,
lead_source,
created_at,
updated_at,
duplicate_count,
rank_in_group,
CASE
WHEN rank_in_group = 1 THEN 'Keep — oldest record'
ELSE 'Review — potential duplicate'
END AS recommendation
FROM duplicate_contacts
ORDER BY normalized_email, rank_in_group;
/*
Sample Output:
contact_id | email | created_at | duplicate_count | rank_in_group | recommendation
-----------|--------------------|---------------------|-----------------|---------------|-------------------------
c_012 | ana@example.com | 2023-01-15 09:00:00 | 2 | 1 | Keep — oldest record
c_847 | ana@example.com | 2024-03-22 14:30:00 | 2 | 2 | Review — potential duplicate
c_204 | rui@example.com | 2023-04-10 11:00:00 | 3 | 1 | Keep — oldest record
c_521 | rui@example.com | 2023-09-01 08:15:00 | 3 | 2 | Review — potential duplicate
c_903 | rui@example.com | 2024-01-05 16:45:00 | 3 | 3 | Review — potential duplicate
Summary Query (run separately):
SELECT duplicate_count, COUNT(DISTINCT normalized_email) AS email_groups,
COUNT(contact_id) AS total_contacts
FROM duplicate_contacts
GROUP BY duplicate_count ORDER BY duplicate_count;
Logic Notes:
- LOWER(TRIM(email)) normalizes casing and whitespace before comparison.
'Ana@Example.com' and 'ana@example.com' will be treated as duplicates.
- rank_in_group = 1 is a starting heuristic only. Before deleting, verify
that the oldest record has the most complete event history. If a newer
record has more purchases or engagements, it may be the better primary.
- Do not delete duplicates directly from this query output without first
merging the event, order, and campaign history from all duplicate records
onto the record you are retaining.
Adapting to Other Platforms:
HubSpot: Use the Duplicates Management tool (Contacts > Actions > Manage
Duplicates) for UI-based merging. For bulk deduplication, use
the Contacts API filtered by email to find and merge duplicates.
Salesforce: Use the Duplicate Management rules and Matching Rules on the
Contact object. The DataLoader can be used for bulk merges.
RD Station: Duplicates are surfaced in the contact list view. Use the
RD Station API to merge contacts programmatically.
BigQuery: ROW_NUMBER() OVER (...) is supported with identical syntax.
LOWER() and TRIM() are both available.
*/