BigQuery数据分析的底层逻辑:先适配架构再动手
很多人用BigQuery的第一步是直接导数据写SQL,但其实BigQuery的列存架构和分布式计算特性,决定了“先建模”是高效分析的前提——如果数据结构没适配BigQuery的脾气,后续查询再优化都是“治标不治本”。

1. 用“星型schema”适配BigQuery的列存优势
BigQuery是典型的列存储数据库,查询时只会扫描需要的列,因此建模要尽量让“常用查询的维度”和“事实数据”分离。比如电商场景中,我们通常会建:
– 事实表:存储订单、交易等业务动作(如orders_fact
),包含order_id
(订单ID)、user_id
(用户ID)、product_id
(商品ID)、order_time
(订单时间)、amount
(金额)等核心指标;
– 维度表:存储用户、商品、时间等描述性信息(如users_dim
含user_id
、age
、gender
;products_dim
含product_id
、category
)。
这种“星型schema”的好处是:查询时通过JOIN
关联事实表和维度表,既能保持数据的规范性,又能让BigQuery快速定位到需要的列。比如创建事实表时,一定要加分区(Partition)和聚类(Cluster):
CREATE TABLE `project.dataset.orders_fact` (
order_id STRING NOT NULL,
user_id STRING NOT NULL,
product_id STRING NOT NULL,
order_time TIMESTAMP NOT NULL,
amount FLOAT64 NOT NULL
)
-- 按订单时间分区(每天一个分区)
PARTITION BY DATE(order_time)
-- 按用户ID聚类(相同用户的订单存在一起)
CLUSTER BY user_id;
为什么要这么做? 比如查询“2025年8月的用户订单总额”,BigQuery会自动跳过其他月份的分区(数据修剪),再从聚类后的用户数据中快速找到目标记录——这能把扫描的数据量从“全表”降到“1/30”甚至更少。
2. 避开BigQuery的建模雷区
- 别用“雪花schema”:雪花schema会把维度表拆得太细(比如
users_dim
拆成users_basic
和users_extra
),导致多表JOIN,反而降低查询速度; - 别嵌套太复杂的结构:BigQuery支持嵌套和重复字段(如
order_items
作为RECORD
类型),但嵌套超过3层会让查询变复杂,尽量用“扁平化”结构; - 别存冗余数据:虽然BigQuery存储成本低,但冗余数据会增加查询时的扫描量,比如别把
user_name
存到订单表,用user_id
关联维度表就行。
写对BigQuery SQL:比“会写”更重要的是“写好”
BigQuery的SQL语法和标准SQL几乎一致,但要写出“快、省、准”的SQL,得掌握几个针对性技巧。
1. 用CTE代替子查询,让SQL更易读且高效
CTE(Common Table Expressions)是“临时结果集”,比子查询更直观,而且BigQuery会对CTE进行优化。比如计算“用户的累计订单额”,用CTE的写法:
WITH user_orders AS (
SELECT
user_id,
order_time,
amount
FROM `project.dataset.orders_fact`
WHERE DATE(order_time) BETWEEN '2025-08-01' AND '2025-08-24'
)
SELECT
user_id,
order_time,
amount,
-- 窗口函数计算累计额
SUM(amount) OVER (PARTITION BY user_id ORDER BY order_time) AS cumulative_amount
FROM user_orders;
对比子查询的写法:
SELECT
user_id,
order_time,
amount,
(SELECT SUM(amount) FROM `project.dataset.orders_fact` o2
WHERE o2.user_id = o1.user_id AND o2.order_time <= o1.order_time) AS cumulative_amount
FROM `project.dataset.orders_fact` o1
WHERE DATE(order_time) BETWEEN '2025-08-01' AND '2025-08-24';
显然CTE的写法更简洁,而且BigQuery会缓存CTE的结果,避免重复计算。
2. 避免“SELECT *”,只取需要的列
列存数据库的核心优势是“按需扫描列”,如果写SELECT *
,BigQuery会扫描表的所有列——比如订单表有20列,你只需要order_id
和amount
,那SELECT *
会多扫描18列,速度慢一倍以上。
3. 用近似函数代替精确函数(当不需要100%准确时)
如果分析的是“大致趋势”(比如日活用户数),用近似函数能大幅提高速度。比如:
– 用APPROX_COUNT_DISTINCT(user_id)
代替COUNT(DISTINCT user_id)
:前者用HyperLogLog算法估算,误差在1%以内,但速度是后者的5-10倍;
– 用APPROX_TOP_COUNT(product_id, 10)
代替TOP(product_id, 10)
:快速找到销量前10的商品,适合实时看板场景。
优化BigQuery性能:从“慢查询”到“秒级响应”
你有没有遇到过:写了一条SQL,等了5分钟还没结果?别慌,先看查询执行计划——BigQuery的UI里有个“Explain”按钮,点一下就能看到SQL的执行步骤。
1. 看“数据扫描量”:有没有做“数据修剪”
执行计划里的“Input metadata”会显示“Total bytes processed”(总扫描量),如果这个数值和表的总大小差不多,说明没用到分区或聚类。比如查询“2025年8月的订单”,如果扫描量是“100GB”,而表的总大小是“1TB”,那说明分区起作用了;如果扫描量是“1TB”,那肯定是没加分区。
2. 看“JOIN类型”:有没有“笛卡尔积”
如果执行计划里有“Cross join”(笛卡尔积),赶紧检查JOIN
的条件——比如ON a.user_id = b.user_id
写成了ON 1=1
,会导致所有行匹配,瞬间让查询变卡。
3. 用“预处理”减少重复计算
如果某个查询要每天跑(比如“日订单汇总”),可以用Materialized Views(物化视图)把结果缓存起来。比如创建物化视图:
CREATE MATERIALIZED VIEW `project.dataset.daily_orders_mv`
AS
SELECT
DATE(order_time) AS order_date,
COUNT(DISTINCT order_id) AS order_count,
SUM(amount) AS total_amount
FROM `project.dataset.orders_fact`
GROUP BY order_date;
之后查询“2025年8月的日订单额”,直接查物化视图就行,不用再扫全表——这能把查询时间从“1分钟”降到“1秒”。
控制BigQuery成本:别让分析变成“烧钱游戏”
BigQuery的收费方式是“按扫描的数据量收费”(默认是$5/TB),虽然不贵,但如果不注意,可能会出现“每月账单超预算”的情况。
1. 用“查询成本估算”提前避坑
BigQuery的UI里,写SQL的时候会实时显示“Estimated bytes processed”(预估扫描量)。比如写SELECT * FROM orders_fact
,预估扫描量是“1TB”,那成本是$5——如果只选order_id
和amount
,预估扫描量可能降到“100GB”,成本是$0.5。
2. 用“BI Engine”加速BI工具查询
如果用Tableau、Looker等BI工具连接BigQuery,开启BI Engine(按容量收费,$0.10/GB/月),能缓存常用查询的数据,减少重复扫描。比如Tableau的仪表盘每天被访问100次,开启BI Engine后,第一次查询扫描“100GB”,之后99次都从缓存取,成本从$5降到$0.5。
3. 用“存储桶”归档冷数据
如果有些数据很少查询(比如2023年的订单),可以把它们转到Google Cloud Storage(GCS)的“ Archive 类”存储($0.01/GB/月),需要的时候再用EXTERNAL TABLE
关联到BigQuery——这能把存储成本从“$0.02/GB/月”降到“$0.01/GB/月”。
真实场景:用BigQuery分析电商用户留存率
最后用一个真实案例,把前面的技巧串起来——假设你是电商数据分析师,要计算“用户的周留存率”(首单后第1周、第2周还下单的用户比例)。
步骤1:提取用户的首单时间
用CTE计算每个用户的首次订单时间:
WITH user_first_order AS (
SELECT
user_id,
MIN(order_time) AS first_order_time
FROM `project.dataset.orders_fact`
-- 只取2025年8月的首单用户
WHERE DATE(order_time) BETWEEN '2025-08-01' AND '2025-08-07'
GROUP BY user_id
)
步骤2:计算“周数差”
把每个用户的后续订单时间和首单时间对比,算出“差几周”:
, user_orders_with_week AS (
SELECT
o.user_id,
o.order_time,
-- 计算订单时间和首单时间的周数差
DATE_DIFF(DATE(o.order_time), DATE(u.first_order_time), WEEK) AS week_since_first
FROM `project.dataset.orders_fact` o
JOIN user_first_order u ON o.user_id = u.user_id
)
步骤3:计算留存率
用COUNT(DISTINCT)
统计每个周数的留存用户数:
SELECT
week_since_first,
COUNT(DISTINCT user_id) AS retained_users,
-- 首单周的用户数(基准)
MAX(CASE WHEN week_since_first = 0 THEN COUNT(DISTINCT user_id) END) OVER () AS total_first_users,
-- 留存率(保留两位小数)
ROUND(COUNT(DISTINCT user_id) / MAX(CASE WHEN week_since_first = 0 THEN COUNT(DISTINCT user_id) END) OVER (), 2) AS retention_rate
FROM user_orders_with_week
-- 只看首单后4周内的订单
WHERE week_since_first BETWEEN 0 AND 4
GROUP BY week_since_first
ORDER BY week_since_first;
结果解读
假设结果是:
| week_since_first | retained_users | total_first_users | retention_rate |
|——————-|—————-|——————–|—————–|
| 0 | 1000 | 1000 | 1.00 |
| 1 | 300 | 1000 | 0.30 |
| 2 | 150 | 1000 | 0.15 |
| 3 | 100 | 1000 | 0.10 |
| 4 | 80 | 1000 | 0.08 |
这说明:首单后第1周留存30%,第2周15%,第4周8%——接下来可以分析“留存低的原因”,比如首单后的优惠券有没有发放,或者商品质量有没有问题。
最后想说的话
BigQuery的核心优势是“无限扩展的计算能力”,但要把这个优势发挥出来,得“顺着它的脾气来”——先建模适配列存,再写好SQL,最后优化性能和成本。其实数据分析的本质,从来不是“用什么工具”,而是“能不能解决问题”——希望这篇指南能帮你用BigQuery更快地找到问题的答案。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/236