Article content and detailed guides remain in English. The selected language applies to controls and quick instructions.

Back to articles

Processing Email Data in Data Lakes and Warehouses

On this page

When Email Data Outgrows Spreadsheets

Most teams start managing email lists in spreadsheets or their CRM. As data volume grows and sources multiply, limitations appear:

Limitation Spreadsheet/CRM impact Data warehouse solution
Row limits Excel: 1M rows; Google Sheets: 10M cells Billions of rows
Multiple sources Manual merge and dedup Automated ETL pipelines
Historical tracking Overwritten on update Full history with timestamps
Complex deduplication VLOOKUP or remove duplicates SQL-based fuzzy matching at scale
Cross-source analytics Manual pivot tables SQL queries across all sources
PII compliance Manual tracking of consent and opt-outs Automated PII handling and access controls
Audit trail No version control Full lineage tracking

Architecture Overview

Data flow for email data

Stage Component What happens
1. Ingestion ETL/ELT tool (Fivetran, Airbyte, custom scripts) Raw email data is extracted from sources and loaded into staging
2. Staging Raw tables in data lake/warehouse Data stored in original format; no transformations applied
3. Transformation dbt, SQL scripts, or Spark jobs Cleaning, deduplication, normalisation, enrichment
4. Modelling Dimensional model in warehouse Clean, queryable tables optimised for analysis
5. Serving BI tools, CRM sync, API endpoints Clean data accessible to downstream systems

Technology options

Component Options Considerations
Data warehouse Snowflake, BigQuery, Redshift, Databricks 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
);

Cleaned tables

-- Normalised and deduplicated contacts
CREATE TABLE analytics.dim_contact (
    contact_id          VARCHAR PRIMARY KEY,
    email_normalised    VARCHAR NOT NULL,
    email_domain        VARCHAR,
    first_name          VARCHAR,
    last_name           VARCHAR,
    company_name        VARCHAR,
    job_title           VARCHAR,
    phone_normalised    VARCHAR,
    is_valid_email      BOOLEAN,
    is_role_based       BOOLEAN,
    is_disposable       BOOLEAN,
    is_catch_all        BOOLEAN,
    verification_status VARCHAR,    -- 'verified', 'unverified',
                                    -- 'invalid', 'unknown'
    first_seen_at       TIMESTAMP_NTZ,
    last_seen_at        TIMESTAMP_NTZ,
    source_count        INTEGER,    -- Number of sources this
                                    -- contact appears in
    primary_source      VARCHAR,
    consent_status      VARCHAR,    -- 'opted_in', 'opted_out',
                                    -- 'unsubscribed', 'unknown'
    gdpr_basis          VARCHAR,    -- 'consent', 'legitimate_interest',
                                    -- 'contract'
    suppressed          BOOLEAN DEFAULT FALSE,
    created_at          TIMESTAMP_NTZ,
    updated_at          TIMESTAMP_NTZ
);

-- Track which sources each contact came from
CREATE TABLE analytics.bridge_contact_source (
    contact_id      VARCHAR,
    source_system   VARCHAR,
    source_id       VARCHAR,
    first_seen_at   TIMESTAMP_NTZ,
    last_seen_at    TIMESTAMP_NTZ,
    PRIMARY KEY (contact_id, source_system, source_id)
);

-- Email event history
CREATE TABLE analytics.fct_email_event (
    event_id        VARCHAR PRIMARY KEY,
    contact_id      VARCHAR,
    event_type      VARCHAR,    -- 'sent', 'delivered', 'opened',
                                -- 'clicked', 'bounced',
                                -- 'unsubscribed', 'complained'
    campaign_id     VARCHAR,
    event_timestamp TIMESTAMP_NTZ,
    metadata        VARIANT     -- Additional event data (JSON)
);

SQL Patterns for Email Data

Email normalisation

-- 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.

Extract emails

Explore tools

Verify emails

Check address validity before using your list.

ZeroBounce

Email Verification

Verifies email lists and provides tools for monitoring deliverability.

Useful when list cleaning and sender health belong in one workflow.

Explore ZeroBounce (opens in a new tab)