快速上手数仓基础知识

快速上手数仓基础知识

    • 一、 数仓解决了一个什么问题?为什么要设计数仓?
      • 那数仓时怎么解决这个问题的呢?
    • 二、数仓的数据究竟是怎么存储的?
      • 2.1 Parquet存储格式
      • 2.2 HDFS分布式存储
        • 那我该怎么知道这些文件块存储在哪里了呢?
        • 只靠列存来优化查询速度吗?
    • 三、交互接口 Hive SQL
      • 3.1 支持原生复杂数据类型
      • 3.2 支持拆分数组
      • 3.3 窗口函数
      • 3.4 Join策略
      • 3.5 SQL执行顺序
    • 四、计算引擎Spark
      • 4.1 MapReduce
      • 4.2 为什么要有Spark
      • 4.3 DAG 是什么
      • 4.4 RDD 解决了什么问题
      • 4.5 shuffle 作用是什么
    • 五、 数仓分层模型

一、 数仓解决了一个什么问题?为什么要设计数仓?

传统数据库(OLTP)处理海量数据时耗时过长,当数据量到达TB甚至PB级的时候,MySQL的表现就极差,即使加上索引和分库分表也难以解决这个问题,因为MySQL的B+数本来就针对单点查询,不擅长做全表扫描

那数仓时怎么解决这个问题的呢?

基本上有这四个方面:

  1. 改用分布式存储
  2. 改变了存储方式,改行存储为列存储,便于后续聚合查询以及应用统计
  3. 引入分布式计算
  4. 分层冗余架构设计

那接下来我们就一层层的解析数仓的这四个方面

二、数仓的数据究竟是怎么存储的?

这里面其实分为两个部分

  1. 文件在服务器上怎么存
  2. 文件内部什么格式存
    这分别跟两个不同的概念有关------HDFS,Parquet

2.1 Parquet存储格式

传统的数据库存储的格式都是行存储,这样存储的优点在于能够快速读取数据,修改数据,但想一下,如果遇到某种情况需要统计某一列的总和,或者均值,这种情况下该怎么办?只能一行行的把数据读取出来再进行聚合,显然这样操作的效率非常低。

而Parquet存储格式改用了列存储,就是为了应对这样的统计场景,当需要单独查询某一列数据的时候,只需要单独将这一列拿出来即可,这样的IO消耗显然不是一个量级的。

在其尾部维护了一些信息,记录了每列的最大值和最小值,方便后续查询

2.2 HDFS分布式存储

即使用上了parquet列存格式,但是tb甚至pb级的文件也不能直接作为单文件存储,这样并不符合容灾要求,一旦出问题很难处理。所以HDFS采取了两个措施:

  • 文件分块,把整个数据文件切成一个个的128MB的Block,存放在不同的服务器上
  • 每块文件做备份处理,通常情况下,会把一个Block存储三份,其中两份再同一机架上的不同服务器,减少单服务器宕机时跨机架造成的延迟。另一份跨机架存储,用作最后保险,防止整个机柜出问题。
那我该怎么知道这些文件块存储在哪里了呢?

HDFS有一个"大脑",用来记忆那些文件存储在哪里了,这个就是NameNode,但这个存储并不是持久化在本地的,这个是动态维护的,由各个服务器定时心跳上报各自存储了哪些文件的哪几个block,在查询的时候就能精确定位。

在mysql层也维护了一张元数据的表 Hive Metastore 记录了要查询的表所在分区的物理位置与行列信息

只靠列存来优化查询速度吗?

当然不是,parquet设计的理念其实是"不读数据",也就是在查询的时候对数据进行最大程度的优化

  • 分区裁剪:在建表的时候就设立分区字段,parquet存储也会按照分区字段来存储在不同的路径下,常见的就是用日期做分区字段
  • 列裁剪:尽可能避免使用select * ,只检索需要的列
  • 谓词下推: 根据parquet footer的数据进行过滤,比如 parquet footer存储了这块的这列数据的最大值就是 500,那我查询 >700的数据肯定就不在这块中,就把这块过滤掉了,本质上是把where条件从SQL执行器推到了存储层
  • Bloom Filter: 一个哈希位数组,用来判断这个数据是否一定不在这个列中。

Bloom Fifter怎么工作的?

内部有一个位数组和K个哈希函数

这列的每个数据在插入时都会遍历这些哈希函数,并将命中的位数组下标对应的值置为1

要查询一个值是否在这列中,只需要遍历哈希函数,看看对应的位数组值是否都为1即可

都为1不一定在,可能会有hash碰撞,但如果有某个值不为1,则一定不在列中

三、交互接口 Hive SQL

虽然Hive SQL 看上去跟我们平常的SQL差不多,但是Hive表实际上跟普通的数据库有很大的区别。

Hive表的元数据存储在MySQL中(Hive Metastore),而真实的数据则是存储在HDFS中的,这就导致了很多问题,hive表没有外键,没有索引,也不支持行级修改,因为底部的parquet文件想要修改只能够重写一遍,所以修改代价极其昂贵,更新 = 全量覆盖。

总而言之,Hive表 = Hive Metastore + HDFS

3.1 支持原生复杂数据类型

  1. Array:有序列表
SQL 复制代码
CREATE TABLE user_tags (
    user_id BIGINT,
    tags    ARRAY<STRING>     -- 标签数组
)
STORED AS PARQUET;

-- 插入
INSERT INTO user_tags VALUES
    (1001, ARRAY('学生', '科技', '游戏')),
    (1002, ARRAY('宝妈', '美食'));

-- 查询
SELECT tags[0] FROM user_tags WHERE user_id = 1001;   -- 取第一个标签
SELECT SIZE(tags) FROM user_tags;                     -- 数组长度
SELECT * FROM user_tags WHERE ARRAY_CONTAINS(tags, '科技');
  1. Map: 键值对
SQL 复制代码
CREATE TABLE user_properties (
    user_id BIGINT,
    props   MAP<STRING, STRING>   -- 动态键值对
);

INSERT INTO user_properties VALUES
    (1001, MAP('city', '北京', 'vip_level', 'V3'));

-- 查询
SELECT props['city'] FROM user_properties WHERE user_id = 1001;  -- 取 'city'
SELECT MAP_KEYS(props), MAP_VALUES(props) FROM user_properties;
  1. Struct :嵌套对象
SQL 复制代码
CREATE TABLE orders (
    order_id BIGINT,
    address  STRUCT<province:STRING, city:STRING, street:STRING>
);

INSERT INTO orders VALUES
    (1, NAMED_STRUCT('province', '北京', 'city', '北京', 'street', '中关村'));

-- 查询(用点操作符,像访问对象字段)
SELECT address.city FROM orders;
SELECT address.province, address.street FROM orders WHERE order_id = 1;

3.2 支持拆分数组

因为原生支持数组,所以有时候需要将数组拆开,比如上面数组那个例子,要查询每个标签下有多少用户,那就必须把数组拆开。

SQL 复制代码
SELECT user_id, tag
FROM   user_tags
LATERAL VIEW EXPLODE(tags) t AS tag;

结果类似于这样

复制代码
user_id | tag
1001    | 学生
1001    | 科技
1001    | 游戏
1002    | 宝妈
1002    | 美食

多个列都需要展开的话,最终结果是所有列的笛卡尔积

3.3 窗口函数

最常用的窗口函数就是排序了,这也是统计场景中经常会出现的

分别是ROW_NUMBER,RANK,DENSE_RANK,举个例子

SQL 复制代码
SELECT user_id, amount,
       ROW_NUMBER() OVER(PARTITION BY city ORDER BY amount DESC) AS rn,
       RANK()       OVER(PARTITION BY city ORDER BY amount DESC) AS rk,
       DENSE_RANK() OVER(PARTITION BY city ORDER BY amount DESC) AS drk
FROM orders;

结果:

user_id amount ROW_NUMBER RANK DENSE_RANK
A 100 1 1 1
B 100 2 1 1
C 90 3 3 2
D 80 4 4 3

ROW_NUMBER:无并列,相同时随机排序

RANK:允许并列,不连续

DENSE_RANK:允许并列,且连续

OVER()的完整结构

复制代码
OVER (
    PARTITION BY <列表>       -- 分组:把数据切成几组
    ORDER BY <列表>           -- 排序:组内按什么排
    ROWS BETWEEN ... AND ...  -- 窗口框架:取哪几行
)

这三个部件都可以省略,简单来说就是

PARTITION BY:表示聚合口径

ORDER BY:表示排序口径,也可表示累计范围

ROWS BETWEEN ... AND ... :一般滑动窗口统计的时候才用

order表示统计范围可能不太好理解,举个例子

sql 复制代码
SELECT user_id, order_time, amount,
       SUM(amount) OVER(PARTITION BY user_id ORDER BY order_time) AS cum_amount
FROM orders;
user_id order_time amount cum_amount
1001 2026-04-20 10:00 100 100
1001 2026-04-21 15:30 200 300
1001 2026-04-22 09:15 50 350
1002 2026-04-20 11:00 80 80
1002 2026-04-21 14:00 120 200
1003 2026-04-22 20:00 500 500

3.4 Join策略

由于hive场景下有的表过于大,且数据都是分布式存储在不同的机器上,而mysql只需要单机内存即可完成

这时候就有三种场景:

  1. Broadcast Hash Join(BHJ)------ 大表+小表 将小表装进内存广播给大表(一般是小于10MB)
  2. Shuffle Hash Join(SHJ)------ 中等表 + 中等表 两张表都按照key shuffle到相同分区,分区内用小的表构建hashmap,大的查表
  3. Sort Merge Join(SMJ)------ 大表 + 大表(默认策略) 两张表都shuffle到相同分区,各自排序后用双指针归并

3.5 SQL执行顺序

复制代码
FROM → WHERE → GROUP BY → HAVING → 窗口函数 → SELECT → ORDER BY

这个顺序决定了上一步没法使用下一步定义的别名

四、计算引擎Spark

4.1 MapReduce

mapreduce是hive的上一代计算引擎,也是分布式计算的起点

他的流程大致分为三个阶段

  • Map 处理

  • Shuffle 分组

  • Reduce 聚合
    举个例子

    输入文件: "hello world hello spark"

    Map 阶段(每行独立处理):
    ("hello", 1), ("world", 1), ("hello", 1), ("spark", 1)

    Shuffle 阶段(按 key 分组):
    "hello" → [1, 1]
    "world" → [1]
    "spark" → [1]

    Reduce 阶段(聚合):
    ("hello", 2), ("world", 1), ("spark", 1)

这样做可以实现我们的目标,但是也存在很多问题:

  1. 磁盘IO巨大:其中的每一步都要落库,这三阶段需要至少落库三次
  2. 强制两阶段:所有计算必须套用上面的流程
  3. 性能差:每次迭代都需要读写HDFS,慢

4.2 为什么要有Spark

上一代的MapReduce存在太多问题需要优化,所以spark引擎应运而生

  • 磁盘IO问题,Spark优先使用内存,不会每步操作都去落盘
  • DAG 任意算子链构成有向无环图
  • RDD 缓存复用

可以大概看出来,Spark快在哪?快在把MR原先的"磁盘IO"瓶颈 变成了现在的"内存+shuffle优化"瓶颈

spark的执行流程是这样的

复制代码
SQL / DataFrame API
      ↓ (Catalyst 解析)
Unresolved Logical Plan
      ↓ (绑定 Hive Metastore 元数据)
Resolved Logical Plan
      ↓ (规则优化:谓词下推、列裁剪、常量折叠)
Optimized Logical Plan
      ↓ (Cost-Based Optimizer 选 Join 策略)
Physical Plan
      ↓ (切分 Stage)
DAG → Stage → Task
      ↓
Executor 执行

4.3 DAG 是什么

spark比起 MR 支持了更多的算子,而DAG就是负责把所有算子画成一张有向无环图

Spark 支持丰富的算子(Transformation):

单元素变换 : map 、 filter 、 flatMap

聚合类 : reduceByKey 、 groupByKey 、 aggregateByKey

Join 类 : join 、 leftOuterJoin 、 cogroup

集合类 : union 、 intersect 、 distinct

排序类 : sortBy 、 sortByKey

Spark SQL 拿到整张DAG之后,能在全局角度下做优化,就比如之前提到过的谓词下推,列裁剪...

4.4 RDD 解决了什么问题

DAG解决了shuffle优化问题,那RDD解决的就是内存问题。

那问题就来了,为什么RDD不怕内存数据丢失呢?万一宕机了怎么办?

因为RDD存储的不是数据本身,存的是"怎么算"的这条血缘记录,一旦宕机,可以沿着血缘链重新计算该分区,利用重算代替副本。

4.5 shuffle 作用是什么

shuffle 是造成整个过程的主要延迟来源,他主要的作用是跨节点按照key重新分发数据

上游task将自己的结果按照hash(key)分桶写到磁盘

下游task拉取属于自己的数据进行归并处理

为什么慢?

  • 磁盘 IO :Shuffle Write 必须落盘(容错 + 解耦上下游)
  • 网络 IO :M × R 的跨节点传输
  • 序列化 :对象 ↔ 字节流,CPU 开销大
  • 小文件 :分区数过多时产生海量小文件

优化

在spark3.0 引入了AQE概念,可以给程提供优化建议,比如:

  • 合并小分区
  • 拆分倾斜分区
  • 切换join策略

在不使用AQE的情况下,数据倾斜也可以通过 插入随机数进行分桶,单独处理倾斜分区等方法进行处理。

五、 数仓分层模型

当然了,数仓也是需要分层的,为了保证数据的 复用、隔离、可追溯

大概分为四层 ODS,DWD,DWS,ADS

  1. ODS - 原始数据仓库
    从数据源原封不动地把数据读取出来,保证数据原的可追溯性
  2. DWD层 - 明细数据
    对数据进行清洗:NULL、异常值统一处理,枚举值标准化
    字段冗余:把一些常用维度表的join数据冗余到事实表中(需要手动冗余)
    eg:对typeid字段冗余对应的名称
  3. DWS - 汇总层
    统一汇总口径,避免同一数据由于不同部门产生不同的统计口径
    将数据按照常用的维度汇总成半成品
    eg:按照日期x城市算出总销售额,订单数等
  4. ADS - 应用层
    面向BI报表,数据看板等直接应用场景

注:这四层每层都需要直接依赖上一层的数据,不允许同层依赖(DWS层可对不同粒度进行同层依赖,如7D数据依赖1D数据)

相关推荐
渣渣盟3 小时前
Spark 性能调优实战:从开发到生产落地
javascript·ajax·spark
渣渣盟4 小时前
数据仓库 vs 数据湖 vs 湖仓一体:架构演进与选型
数据仓库·架构
隐于花海,等待花开9 小时前
39.ROUND / FLOOR / CEIL 函数深度解析
hive·hadoop
juniperhan9 小时前
Flink 系列第22篇:Flink SQL 参数配置与性能调优指南:从 Checkpoint 到聚合优化
大数据·数据仓库·分布式·sql·flink
juniperhan18 小时前
Flink 系列第21篇:Flink SQL 函数与 UDF 全解读:类型推导、开发要点与 Module 扩展
java·大数据·数据仓库·分布式·sql·flink
看海的四叔1 天前
【SQL】SQL-管好你的字符串
大数据·数据库·hive·sql·数据分析·字符串
渣渣盟1 天前
大数据技术栈全景图:从零到一的入门路线(深度实战版)
大数据·hadoop·python·flink·spark
地球资源数据云1 天前
1960年-2024年中国棉花产量数据集
大数据·数据结构·数据仓库·人工智能
i建模1 天前
在数据仓库(数仓)中,给数据打标签(Tagging)
数据仓库