TDengine 排序、填充与投影 — Sort、Fill 与 Project 算子

分类:4.查询引擎 | 篇章:10 排序填充投影

在这里插入图片描述

适用版本:TDengine v3.x(v3.3.x / v3.4.x) | 最后更新:2026-06-18

排序(ORDER BY)、填充(FILL)和投影(Project)是查询后处理阶段的核心算子。本文聚焦于这三类算子的实现细节、与时序数据排序天然有序的关系、以及如何避免不必要的全局排序开销。

核心概念速查表

概念说明
ORDER BY ts按时间排序(天然有序,零开销)
ORDER BY 其他列需要实际排序
MergeSort多路有序输入合并
TopNORDER BY + LIMIT N 的优化
FILL时间窗口缺失值填充
Project投影:选择/计算/重命名列
Aliasing列别名(AS)

详细解析

1. ORDER BY 的执行策略

TDengine ORDER BY 的三种执行情况:

  ① ORDER BY ts ASC(默认)
     - 存储天然有序 → 零开销
     - Scan 顺序读 → 直接输出
     
  ② ORDER BY ts DESC
     - Scan 反向读取数据块
     - 同样零开销
     
  ③ ORDER BY 其他列
     - 必须实际排序
     - 单节点:内存排序(小数据)/ 外排(大数据)
     - 多节点:本地排序 + MergeExchange

2. MergeSort(多路归并)

跨 VGroup 的全局排序:

  SQL: SELECT * FROM meters WHERE ts > now-1h ORDER BY ts;
  
  执行:
  
  ┌─ Client/QNode ────────────────────┐
  │  MergeSort (3 路归并)              │
  │      ↑                             │
  │  Exchange                          │
  └────────┬───────────────────────────┘
           │
    ┌──────┼──────┐
    ▼      ▼      ▼
  ┌───┐  ┌───┐  ┌───┐
  │VN1│  │VN2│  │VN3│
  │   │  │   │  │   │
  │Scan│  │Scan│ │Scan│ (各自按 ts 有序)
  └───┘  └───┘  └───┘
  
  归并算法:
    维护 3 个指针,指向每个 VNode 的当前位置
    每次输出 ts 最小的那个,对应指针前进
    类似数据文件多路合并(详见 04-file-format)

3. ORDER BY + LIMIT(TopN 优化)

TopN 优化:

  SELECT * FROM meters ORDER BY ts DESC LIMIT 10;
  
  朴素实现:
    全表排序 → 取前 10
    复杂度 O(N log N)
    
  TopN 优化:
    维护大小为 10 的堆
    扫描中只保留 top 10
    复杂度 O(N log 10)
    
  
跨节点的 TopN:

  ① 每个 VNode 各取 top 10
  ② 共 K×10 行汇总到 QNode
  ③ QNode 再做 top 10
  
  网络传输:仅 K×10 行(不是全量)


ORDER BY 时间列 + LIMIT 特别优化:
  ORDER BY ts DESC LIMIT 10
  → 反向 Scan,读到 10 行立即停止
  → I/O 极小

4. 外排序(大数据排序)

当排序数据超出内存:

  ① 数据按内存大小分块读入
  ② 每块内存中排序后写入临时文件
  ③ 多路归并所有临时文件
  
  特点:
  - 磁盘 I/O 大(写临时文件 + 读取归并)
  - 性能远低于内存排序
  - 应尽量避免(用 LIMIT 限制)

避免外排的方法:
  - 加 WHERE 限制数据量
  - 用 LIMIT + TopN 算法
  - 增大查询内存配额

5. FILL 算子的执行

FILL 实现流程:

  INTERVAL(1h) FILL(LINEAR)
  
  窗口算子输出:
    12:00 → 25.0
    13:00 → (无数据,待填充)
    14:00 → (无数据,待填充)
    15:00 → 28.0
  
  FILL 算子处理:
    扫描连续 NULL 窗口
    找到边界值:前=25.0(12:00), 后=28.0(15:00)
    线性插值:
      13:00 → 25 + (28-25) × 1/3 = 26.0
      14:00 → 25 + (28-25) × 2/3 = 27.0
    
  输出:
    12:00 → 25.0
    13:00 → 26.0
    14:00 → 27.0
    15:00 → 28.0


FILL 模式对比:
  FILL(NULL)        → 13:00=NULL, 14:00=NULL
  FILL(VALUE, 0)    → 13:00=0, 14:00=0
  FILL(PREV)        → 13:00=25.0, 14:00=25.0
  FILL(NEXT)        → 13:00=28.0, 14:00=28.0
  FILL(LINEAR)      → 13:00=26.0, 14:00=27.0
  FILL(NONE)        → 跳过这两行

6. Project 算子

Project 算子的职责:

  ① 列选择(裁剪):
     SELECT ts, current FROM meters
     → 只保留 ts 和 current 列
     
  ② 表达式计算:
     SELECT ts, current * voltage AS power FROM meters
     → 计算 power = current × voltage
     
  ③ 列别名:
     SELECT current AS amperage FROM meters
     → 输出列名改为 amperage
     
  ④ 函数应用:
     SELECT ts, ROUND(current, 2) FROM meters
     → 应用 ROUND 函数
     
  ⑤ CASE WHEN:
     SELECT ts, CASE WHEN current > 10 THEN 'HIGH' ELSE 'LOW' END 
     FROM meters
     → 条件分支

7. 表达式向量化执行

表达式向量化:

  SELECT current * voltage AS power FROM meters
  
  传统行式:
    for each row:
      output[i] = row.current * row.voltage
    → 每行一次乘法
    
  向量化:
    一次性取 4096 行的 current 列 → 数组 a[4096]
    一次性取 4096 行的 voltage 列 → 数组 b[4096]
    向量乘法:c[i] = a[i] * b[i](可用 SIMD)
    → 一次操作处理多行
    
  优势:
  - SIMD 指令加速(AVX/SSE)
  - CPU 缓存友好
  - 函数调用开销摊薄

8. DISTINCT 与 SELECT 的关系

DISTINCT 实现:

  SELECT DISTINCT location FROM meters;
  
  执行:
    HashAgg (group by location)
    → 哈希表去重
    → 输出唯一值
  
  → 本质是 GROUP BY 但不带聚合函数


对比:
  SELECT DISTINCT a, b → 等价于 GROUP BY a, b
  SELECT COUNT(DISTINCT a) → 用 HyperLogLog(近似)或全去重

代码示例

排序场景

-- ✓ 高效:按 ts 排序(天然有序)
SELECT * FROM meters WHERE ts > now-1h ORDER BY ts DESC LIMIT 100;

-- ✓ 高效:TopN
SELECT * FROM meters ORDER BY current DESC LIMIT 10;

-- ✗ 低效:全表按非 ts 列排序
SELECT * FROM meters ORDER BY current;  -- 没有 LIMIT

FILL 应用

-- 监控图表绘图(必须连续点)
SELECT _wstart, AVG(current) 
FROM meters 
WHERE ts > now-1h 
INTERVAL(1m) FILL(LINEAR);

-- 数据归档(保留 NULL 标识)
SELECT _wstart, SUM(quantity) 
FROM orders 
WHERE ts > now-1d 
INTERVAL(1h) FILL(NULL);

-- 累计监控(缺失用前值)
SELECT _wstart, MAX(counter) 
FROM monotonic_counter 
INTERVAL(1m) FILL(PREV);

投影表达式

-- 计算列
SELECT ts, 
       current * voltage AS power,
       CASE WHEN current > 10 THEN 'HIGH' ELSE 'LOW' END AS level
FROM meters;

-- 时间格式化
SELECT TO_CHAR(ts, 'yyyy-mm-dd HH:MI'), current 
FROM meters;

-- 嵌套函数
SELECT ROUND(AVG(current), 2) AS avg_c 
FROM meters 
INTERVAL(1h);

性能考量

排序性能

场景复杂度备注
ORDER BY tsO(N)利用天然有序
ORDER BY ts + LIMITO(LIMIT)早停优化
ORDER BY 其他列O(N log N)需要实际排序
内存放不下O(N log N)外排,慢得多
跨 VGroupO(N log N)+ MergeExchange

FILL 性能

FILL 算子本身开销极低。但 FILL(LINEAR) 需要"先看到后续值"才能填充,会增加少量延迟。FILL(PREV) 是最快的(无需前瞻)。

FAQ

Q1: ORDER BY ts 为什么不算开销?

数据存储天然按 ts 有序,Scan 算子按顺序读取即可。ORDER BY ts ASC 是默认行为,无需额外算子。

Q2: 大表 SELECT * ORDER BY ts 会爆内存吗?

如果没有 LIMIT,会传输全量数据。本身不爆内存(流式),但客户端可能 OOM。务必加 LIMIT。

Q3: FILL 会拖慢查询吗?

正常情况几乎无感知。性能开销主要在窗口聚合阶段,FILL 只是后处理。

Q4: SELECT 中能用窗口聚合的别名吗?

SELECT AVG(current) AS avg_c FROM meters WHERE avg_c > 10;
-- ✗ 错误:WHERE 在聚合前执行,看不到 avg_c

SELECT AVG(current) AS avg_c FROM meters HAVING avg_c > 10;
-- ✓ 正确:HAVING 在聚合后执行

参考

系统构架篇

数据模型

存储引擎

查询引擎

关于 TDengine

TDengine 专为物联网IoT平台、工业大数据平台设计。其中,TDengine TSDB 是一款高性能、分布式的时序数据库(Time Series Database),同时它还带有内建的缓存、流式计算、数据订阅等系统功能;TDengine IDMP 是一款AI原生工业数据管理平台,它通过树状层次结构建立数据目录,对数据进行标准化、情景化,并通过 AI 提供实时分析、可视化、事件管理与报警等功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TDengine (老段)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值