A customer 360 sounds like a product you buy. It isn't. It's every signal about a customer joined into one row per person, and on BigQuery you can build it with SQL, no CDP required.
What a customer 360 actually is
It's a single table with one row per customer, pulling together their behavior, orders, and profile: sessions and events from GA4, purchases and revenue from your store, and attributes from your CRM.
The goal is that any question about a customer, their LTV, last activity, acquisition channel, or lifecycle stage, is answerable from one place.
Pick the identity key first
Everything hinges on the key you join on. GA4's raw identifier is user_pseudo_id, which is device-level, so you need a stable person-level key, usually a user_id or a hashed email, mapped in where a user logged in.
Get this wrong and you either split one customer into many or merge several into one, and every metric downstream inherits the mistake.
Build it in layers
- Stage each source into a clean model: GA4 behavior, orders, CRM.
- Resolve identity, mapping user_pseudo_id to your user id where possible.
- Aggregate to one row per customer with the metrics you care about.
The final aggregation is where the 360 comes together:
SELECT c.user_id, c.email_sha256, MIN(s.first_seen) AS acquired_at, MAX(s.last_seen) AS last_active_at, ANY_VALUE(s.first_channel) AS acquisition_channel, COUNT(DISTINCT o.order_id) AS orders, SUM(o.revenue) AS lifetime_value FROM `project.marts.crm_customers` c LEFT JOIN `project.marts.sessions` s USING (user_id) LEFT JOIN `project.marts.orders` o USING (user_id) GROUP BY c.user_id, c.email_sha256
Decide what one row should hold
The join is easy; the discipline is deciding which attributes earn a column. A customer 360 tempts you to bolt on every field from every source until the table is unreadable and half the columns are never queried.
Start from the questions the business actually asks, LTV, recency, acquisition channel, lifecycle stage, and model those. You can always add a column later; ripping out fifty speculative ones nobody uses is the harder cleanup, so bias toward a lean table that answers real questions.
Keep it fresh
A customer 360 is only useful if it's current, so build it as an incremental, scheduled model in Dataform or dbt rather than a query you rerun by hand. It should update on the same cadence as your source data.
Freshness also protects trust, which is the table's real currency. The first time someone spots their own recent order missing, they stop believing the whole thing, and a distrusted 360 is worse than none because people quietly go back to pulling their own numbers.
Then actually use it
The table earns its keep when it drives something: feed segments to reverse ETL for activation, power dashboards, or seed lookalikes. A 360 nobody queries is just an expensive join.
This is also where the warehouse-native approach pays off. Because the 360 lives in BigQuery, the same table can answer an analyst's ad-hoc question, back a Looker dashboard, and sync an audience to Meta, all from one definition, instead of three teams maintaining three slightly different versions of who a customer is.
Where it goes wrong
- Identity collisions, where a shared device or recycled email fuses two people.
- PII sitting unhashed and ungoverned, which turns a useful table into a liability.
- Staleness, so it quietly drifts from reality until nobody trusts it.
- Over-joining every source in sight, when a few high-value ones would do.
A customer 360 on BigQuery is a join, not a purchase: one row per person, built in layers on a stable identity key. Resolve identity carefully, keep it fresh with a scheduled pipeline, and actually activate it, and you get most of what a CDP sells using the warehouse you already pay for.
Want a stronger data analyst role or a raise? Grab the FREE Product Analyst Playbook and get the exact roadmap to your next offer.
