7.3 KiB
7.3 KiB
充电桩数据仓库设计说明文档
本文档详细描述了充电桩爬虫项目的数据仓库设计思路、表结构定义、常见查询场景及应对策略。该设计基于 Doris 数据库,采用拉链表(SCD Type 2)思想,旨在完整保留历史数据变更轨迹,支持时间旅行查询(Time Travel Query)。
1. 设计思路:为何选择 SCD Type 2 拉链表?
在充电桩业务中,数据具有不同的更新频率和业务含义:
- 基础信息(Profile):如场站名称、地址。这些信息相对稳定,偶尔变化(如更名、迁址)。
- 实时状态(Status):如空闲桩数、总桩数。这些信息变化极快(分钟级),是分析热度的关键。
- 价格策略(Price):如分时电价。通常按天或月调整,但一旦调整需要保留历史价格以核对账单或分析价格趋势。
如果仅保存“最新状态”,我们将丢失所有历史轨迹,无法回答“上个月该站点的平均空闲率是多少”或“价格调整前后的流量对比”等问题。
因此,我们采用 SCD Type 2(Slowly Changing Dimension Type 2) 拉链表设计:
- 每条记录都有
valid_start_time(生效时间)和valid_end_time(失效时间)。 - 当前有效的记录,
valid_end_time通常设为极大值(如9999-12-31)。 - 当数据发生变化时:
- 将旧记录的
valid_end_time更新为当前时间(闭链)。 - 插入一条新记录,
valid_start_time为当前时间,valid_end_time为极大值(开链)。
- 将旧记录的
2. 表结构定义
2.1 充电站基础信息表 (station_profile_scd)
- 用途:存储场站名称、地址等相对稳定的属性。
- 业务主键:
station_hash(名称的MD5,确保唯一性)。
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
VARCHAR(50) | 物理主键 (UUID),每条历史记录唯一 |
station_hash |
VARCHAR(32) | 业务主键,同一场站的所有历史记录此值相同 |
station_name |
VARCHAR(255) | 场站名称 |
address |
VARCHAR(500) | 详细地址 |
valid_start_time |
DATETIME | 记录生效时间 |
valid_end_time |
DATETIME | 记录失效时间 (默认 9999-12-31) |
is_current |
TINYINT | 是否为当前最新记录 (1=是, 0=否) |
2.2 充电站实时状态表 (station_status_scd)
- 用途:高频存储场站的桩数、空闲情况。
- 特点:数据量大,通过拉链表仅记录“状态发生变化”的时刻,节省空间。
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
VARCHAR(50) | 物理主键 (UUID) |
station_hash |
VARCHAR(32) | 业务主键 |
total_piles |
INT | 总桩数 |
free_piles |
INT | 空闲桩数 |
piles_detail_json |
TEXT | 桩详情JSON (如 {"快": {"free": 2, "total": 10}, "慢": ...}) |
current_price |
DECIMAL | 抓取时的当前电价快照 |
valid_start_time |
DATETIME | 生效时间 |
valid_end_time |
DATETIME | 失效时间 |
is_current |
TINYINT | 是否最新 |
2.3 充电站价格时段表 (station_price_schedule_scd)
- 用途:存储全天的分时电价表。
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
VARCHAR(50) | 物理主键 (UUID) |
station_hash |
VARCHAR(32) | 业务主键 |
schedule_json |
TEXT | 价格时段JSON数组 |
valid_start_time |
DATETIME | 生效时间 |
valid_end_time |
DATETIME | 失效时间 |
is_current |
TINYINT | 是否最新 |
3. 常见查询场景及 SQL 写法
场景一:查询所有场站的“当前”最新状态
这是最常用的查询,用于实时展示。
SELECT
p.station_name,
p.address,
s.total_piles,
s.free_piles,
s.current_price
FROM
station_profile_scd p
JOIN
station_status_scd s ON p.station_hash = s.station_hash
WHERE
p.is_current = 1
AND s.is_current = 1;
场景二:查询“指定时间点”的历史快照
例如:查询 2023-10-01 14:00:00 这一刻,所有场站的状态。 这是拉链表的杀手锏功能。
SET @query_time = '2023-10-01 14:00:00';
SELECT
p.station_name,
s.free_piles,
s.current_price
FROM
station_profile_scd p
JOIN
station_status_scd s ON p.station_hash = s.station_hash
WHERE
-- 核心逻辑:查询时间落在生效区间内
(@query_time >= p.valid_start_time AND @query_time < p.valid_end_time)
AND (@query_time >= s.valid_start_time AND @query_time < s.valid_end_time);
场景三:分析某场站过去24小时的空闲桩变化趋势
我们需要按时间顺序拉出该场站的所有状态变更记录。
SELECT
valid_start_time AS change_time,
free_piles,
total_piles,
ROUND(free_piles / total_piles * 100, 2) AS free_rate_percent
FROM
station_status_scd
WHERE
station_hash = 'TARGET_STATION_MD5'
AND valid_start_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY
valid_start_time ASC;
场景四:统计过去一个月,价格发生过变动的场站
通过查找 station_price_schedule_scd 表中同一个 station_hash 有多条记录的情况。
SELECT
p.station_name,
COUNT(pr.id) AS price_change_count
FROM
station_price_schedule_scd pr
JOIN
station_profile_scd p ON pr.station_hash = p.station_hash AND p.is_current = 1
WHERE
pr.valid_start_time >= DATE_SUB(NOW(), INTERVAL 1 MONTH)
GROUP BY
p.station_name
HAVING
COUNT(pr.id) > 1
ORDER BY
price_change_count DESC;
4. 数据写入与维护策略
为了维护 SCD2 结构,写入程序(Crawler/ETL)需要遵循以下逻辑:
- 计算 Hash:对抓取到的场站名称计算 MD5,作为
station_hash。 - 查询最新状态:根据
station_hash查询is_current=1的记录。 - 比对差异:
- Profile: 比对名称、地址是否变化。
- Status: 比对总桩数、空闲数、价格是否变化。
- Price: 比对价格时段 JSON 是否变化。
- 无变化:跳过,不进行任何数据库操作。
- 有变化:
- Step 1 (Close): 更新旧记录:
UPDATE table SET valid_end_time = NOW(), is_current = 0 WHERE id = 'old_uuid'。 - Step 2 (Insert): 插入新记录:
INSERT INTO table (..., valid_start_time=NOW(), is_current=1)。 - 注意:Step 1 和 Step 2 最好在一个事务中完成,或者保证最终一致性。
- Step 1 (Close): 更新旧记录:
5. 性能优化建议
- 分桶 (Bucketing):建表时使用了
DISTRIBUTED BY HASH(station_hash) BUCKETS 10。这保证了同一个场站的所有历史记录都落在同一个 Tablet 上,极大地优化了基于station_hash的 Join 和聚合查询性能。 - 分区 (Partitioning):当前未显式使用时间分区。如果数据量达到亿级,建议按
valid_start_time进行 Range 分区(如按月),以便冷热数据分离和快速清理过期历史数据。 - 索引: Doris 自动为前缀列创建索引。由于我们常按
station_hash查询,它被放在表定义的列首(或紧跟 ID),查询效率很高。
文档维护人: AI Assistant 最后更新: 2026-01-08