Files
aiData/Doc/5、统计表结构说明.md
HuangHai b66f683dfb 'commit'
2026-01-12 07:49:18 +08:00

172 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 充电桩数据仓库设计说明文档
本文档详细描述了充电桩爬虫项目的数据仓库设计思路、表结构定义、常见查询场景及应对策略。该设计基于 Doris 数据库,采用拉链表(SCD Type 2)思想,旨在完整保留历史数据变更轨迹,支持时间旅行查询(Time Travel Query)。
## 1. 设计思路:为何选择 SCD Type 2 拉链表?
在充电桩业务中,数据具有不同的更新频率和业务含义:
1. **基础信息(Profile)**:如场站名称、地址。这些信息相对稳定,偶尔变化(如更名、迁址)。
2. **实时状态(Status)**:如空闲桩数、总桩数。这些信息变化极快(分钟级),是分析热度的关键。
3. **价格策略(Price)**:如分时电价。通常按天或月调整,但一旦调整需要保留历史价格以核对账单或分析价格趋势。
如果仅保存“最新状态”,我们将丢失所有历史轨迹,无法回答“上个月该站点的平均空闲率是多少”或“价格调整前后的流量对比”等问题。
因此,我们采用 **SCD Type 2(Slowly Changing Dimension Type 2)** 拉链表设计:
* 每条记录都有 `valid_start_time`(生效时间)和 `valid_end_time`(失效时间)。
* 当前有效的记录,`valid_end_time` 通常设为极大值(如 `9999-12-31`)。
* 当数据发生变化时:
1. 将旧记录的 `valid_end_time` 更新为当前时间(闭链)。
2. 插入一条新记录,`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 写法
### 场景一:查询所有场站的“当前”最新状态
这是最常用的查询,用于实时展示。
```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** 这一刻,所有场站的状态。
这是拉链表的杀手锏功能。
```sql
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小时的空闲桩变化趋势
我们需要按时间顺序拉出该场站的所有状态变更记录。
```sql
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` 有多条记录的情况。
```sql
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)需要遵循以下逻辑:
1. **计算 Hash**:对抓取到的场站名称计算 MD5,作为 `station_hash`。
2. **查询最新状态**:根据 `station_hash` 查询 `is_current=1` 的记录。
3. **比对差异**:
* **Profile**: 比对名称、地址是否变化。
* **Status**: 比对总桩数、空闲数、价格是否变化。
* **Price**: 比对价格时段 JSON 是否变化。
4. **无变化**:跳过,不进行任何数据库操作。
5. **有变化**:
* **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 最好在一个事务中完成,或者保证最终一致性。*
## 5. 性能优化建议
1. **分桶 (Bucketing)**:建表时使用了 `DISTRIBUTED BY HASH(station_hash) BUCKETS 10`。这保证了同一个场站的所有历史记录都落在同一个 Tablet 上,极大地优化了基于 `station_hash` 的 Join 和聚合查询性能。
2. **分区 (Partitioning)**:当前未显式使用时间分区。如果数据量达到亿级,建议按 `valid_start_time` 进行 Range 分区(如按月),以便冷热数据分离和快速清理过期历史数据。
3. **索引**: Doris 自动为前缀列创建索引。由于我们常按 `station_hash` 查询,它被放在表定义的列首(或紧跟 ID),查询效率很高。
---
**文档维护人**: AI Assistant
**最后更新**: 2026-01-08