Snowflake and BigQuery for ease of use; Databricks for Spark workloads
Data lake
S3, GCS, Azure Data Lake
For raw file storage before warehouse loading
ETL/ELT
Fivetran, Airbyte, Stitch, custom Python
Fivetran for CRM connectors; Airbyte for open source
Transformation
dbt, Spark, SQL scripts
dbt for SQL-based transformations; Spark for large-scale processing
Orchestration
Airflow, Dagster, Prefect, dbt Cloud
Airflow for flexibility; dbt Cloud for dbt-only workflows
BI / analytics
Looker, Tableau, Metabase, Superset
Looker for governed metrics; Metabase for self-service
Reverse ETL
Census, Hightouch, Polytomic
Sync clean data back to CRMs and marketing tools
Schema Design for Email Data
Staging tables (raw)
-- Raw email contacts from various sources
CREATE TABLE staging.raw_contacts (
id VARCHAR,
source_system VARCHAR, -- 'salesforce', 'hubspot',
-- 'csv_upload', etc.
source_id VARCHAR, -- ID in source system
email VARCHAR,
first_name VARCHAR,
last_name VARCHAR,
company VARCHAR,
title VARCHAR,
phone VARCHAR,
source_file VARCHAR, -- For file-based imports
raw_data VARIANT, -- Full raw record (JSON)
ingested_at TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP(),
batch_id VARCHAR
);
-- Normalise email addresses
SELECT
LOWER(TRIM(email)) AS email_normalised,
SPLIT_PART(LOWER(TRIM(email)), '@', 2) AS email_domain,
SPLIT_PART(LOWER(TRIM(email)), '@', 1) AS email_local_part
FROM staging.raw_contacts
WHERE email IS NOT NULL
AND email LIKE '%@%.%'
AND LENGTH(email) <= 254;
Deduplication with priority
-- Deduplicate contacts, keeping the most complete record
WITH ranked AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY LOWER(TRIM(email))
ORDER BY
-- Prefer records with more fields filled
(CASE WHEN first_name IS NOT NULL THEN 1 ELSE 0 END
+ CASE WHEN last_name IS NOT NULL THEN 1 ELSE 0 END
+ CASE WHEN company IS NOT NULL THEN 1 ELSE 0 END
+ CASE WHEN title IS NOT NULL THEN 1 ELSE 0 END
+ CASE WHEN phone IS NOT NULL THEN 1 ELSE 0 END
) DESC,
-- Then prefer newer records
ingested_at DESC
) AS rn
FROM staging.raw_contacts
WHERE email IS NOT NULL
)
SELECT * FROM ranked WHERE rn = 1;
Role-based address detection
-- Flag role-based email addresses
SELECT
email_normalised,
CASE
WHEN email_local_part IN (
'info', 'admin', 'support', 'sales', 'contact',
'help', 'billing', 'abuse', 'postmaster',
'webmaster', 'noreply', 'no-reply',
'marketing', 'security', 'legal', 'hr',
'office', 'team', 'hello'
) THEN TRUE
ELSE FALSE
END AS is_role_based
FROM analytics.dim_contact;
Domain analysis
-- Analyse email domains for list composition
SELECT
email_domain,
COUNT(*) AS contact_count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2)
AS percentage,
COUNT(CASE WHEN is_valid_email THEN 1 END)
AS valid_count,
COUNT(CASE WHEN is_role_based THEN 1 END)
AS role_based_count,
COUNT(CASE WHEN is_disposable THEN 1 END)
AS disposable_count
FROM analytics.dim_contact
GROUP BY email_domain
ORDER BY contact_count DESC
LIMIT 50;
Cross-source deduplication
-- Find contacts that appear in multiple sources
SELECT
c.email_normalised,
c.source_count,
LISTAGG(DISTINCT bs.source_system, ', ')
WITHIN GROUP (ORDER BY bs.source_system) AS sources,
MIN(bs.first_seen_at) AS earliest_seen,
MAX(bs.last_seen_at) AS latest_seen
FROM analytics.dim_contact c
JOIN analytics.bridge_contact_source bs
ON c.contact_id = bs.contact_id
GROUP BY c.email_normalised, c.source_count
HAVING c.source_count > 1
ORDER BY c.source_count DESC;
ETL Pipeline Design
Ingestion patterns by source
Source
Ingestion method
Frequency
Notes
Salesforce
Fivetran, Airbyte or Salesforce API
Every 15 min to hourly
Use change data capture (CDC) for efficiency
HubSpot
Fivetran, Airbyte or HubSpot API
Hourly
Pull contacts, companies and email events
Mailchimp / email ESP
API or Fivetran connector
Daily
Pull subscriber data and email events
CSV file uploads
Cloud function triggered by file drop
On upload
Validate schema before loading
Web form submissions
Webhook to staging table
Real-time
Validate email format on receipt
Event/conference lists
Manual upload pipeline
Ad hoc
Standardise schema before loading
Purchased lists
Manual upload with provenance tracking
Ad hoc
Flag with acquisition source and date
dbt model structure
Layer
dbt model
Purpose
Staging
stg_salesforce_contacts
Clean and type-cast raw Salesforce data
Staging
stg_hubspot_contacts
Clean and type-cast raw HubSpot data
Staging
stg_csv_uploads
Clean and type-cast uploaded CSV data
Intermediate
int_contacts_unioned
Union all source contact tables
Intermediate
int_contacts_deduplicated
Dedup across sources
Mart
dim_contact
Final clean, enriched contact dimension
Mart
fct_email_event
Email event facts
Mart
rpt_email_domain_analysis
Domain-level analytics
Mart
rpt_list_health
List quality metrics
PII Handling
Access controls
Data element
Sensitivity
Access level
Email address
PII
Restricted; need-to-know basis
First/last name
PII
Restricted
Phone number
PII
Restricted
Company name
Business data
Broadly accessible
Job title
Business data
Broadly accessible
Email domain
Derived / non-PII
Broadly accessible
Engagement metrics
Behavioural data
Marketing and sales teams
Consent/opt-out status
Compliance data
All teams that handle contact data
Data masking
-- Create a masked view for analysts who do not need
-- to see individual email addresses
CREATE VIEW analytics.v_contact_masked AS
SELECT
contact_id,
-- Hash the email for joining without exposing it
SHA2(email_normalised) AS email_hash,
email_domain,
-- Mask name
LEFT(first_name, 1) || '***' AS first_name_masked,
LEFT(last_name, 1) || '***' AS last_name_masked,
company_name,
job_title,
is_valid_email,
is_role_based,
verification_status,
consent_status,
source_count,
primary_source
FROM analytics.dim_contact;
Retention and deletion
Requirement
Implementation
GDPR right to erasure
DELETE from all tables where email matches; log the deletion
CCPA opt-out
Set suppressed = TRUE; exclude from all marketing queries
Retention policy
Automated deletion of records not seen in N months
Audit log
Track all access and modifications to PII
Data minimisation
Store only fields you actively use
Reverse ETL: Syncing Clean Data Back
Tool
What it does
Use case
Census
Syncs warehouse data to SaaS tools
Push clean contact data back to Salesforce, HubSpot
Hightouch
Syncs warehouse data to 100+ destinations
Push enriched contacts to marketing tools
Polytomic
Bi-directional sync between warehouse and SaaS
Keep CRM and warehouse in sync
Custom API scripts
Direct API calls to update CRM
Full control over sync logic
Sync patterns
Pattern
How it works
When to use
Full sync
Replace entire dataset in destination
Small datasets; infrequent updates
Incremental sync
Send only changed records
Large datasets; frequent updates
Event-triggered sync
Sync when a record changes in the warehouse
Real-time requirements
Scheduled sync
Run at fixed intervals
Batch processing; daily updates
Pre-Processing with Email Extractor
Before loading email data into a data warehouse, the data often exists in unstructured formats. Upload raw files (PDF reports, HTML pages, XLSX exports, CSV dumps) to Email Extractor to extract email addresses. Download the results as CSV, which can then be loaded into a staging table through a standard CSV ingestion pipeline. This pre-processing step is especially useful for ad hoc data sources like conference attendee lists, business card scans and exported documents that do not have a structured API.