TDengine 一条 SQL 从客户端到执行完成的全链路

分类: 系统架构 > 端到端消息流

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

概述

本文追踪一条 SQL 语句从用户输入到结果返回的完整端到端路径,串联前 6 篇文章中讲解的各个组件(taosc、RPC、MNode、VNode、Raft、存储引擎),让读者建立全局视角。

我们分别追踪三类典型操作:

  1. 写入操作INSERT INTO 的完整链路
  2. 查询操作SELECT 的完整链路
  3. DDL 操作CREATE DATABASE 的完整链路

核心概念速查表

概念 说明
taosc 客户端驱动库,负责 SQL 解析、路由计算、请求分发
Catalog 客户端的元数据缓存(VGroup 路由、表 Schema 等)
Scheduler 客户端的查询调度器,将物理计划拆分为子任务并分发
VGroup 路由 根据表名 hash 确定数据所在 VGroup 及其 Leader 地址
消息类型 标识操作类型的枚举值(如 TDMT_VND_SUBMITTDMT_SCH_QUERY
QNode 可选的独立查询节点,分担 VNode 的查询计算负载

详细解析

1. 写入操作全链路

INSERT INTO d1001 VALUES (now, 10.3, 219, 0.31) 为例:

复制代码
写入全链路(14 步):

  ┌─────────────────────────────────────────────────────────────────┐
  │                         客户端 (taosc)                           │
  │                                                                   │
  │  ① 用户调用 API(如 taos_query)                                  │
  │  ② SQL 解析:识别为 INSERT 语句,提取数据库名、表名、列值          │
  │  ③ Catalog 查询:                                                 │
  │     - 表 d1001 的 Schema(列类型、列数)→ 用于数据编码              │
  │     - 表 d1001 所属 VGroup 及其 Leader 地址                        │
  │     (如果缓存命中则跳过网络请求,否则向 MNode 拉取)              │
  │  ④ 数据编码:按列类型将值序列化为二进制提交格式                    │
  │  ⑤ 构建 SUBMIT 消息,通过 RPC 发往 VGroup Leader                  │
  └───────────────────────────────────┬─────────────────────────────┘
                                      │
                                      ▼ RPC(TCP)
  ┌─────────────────────────────────────────────────────────────────┐
  │                    VNode (VGroup Leader)                          │
  │                                                                   │
  │  ⑥ RPC 层接收消息,入写队列                                       │
  │  ⑦ 写队列 Worker 取出消息                                         │
  │  ⑧ Raft 提议:                                                    │
  │     - 构建 Raft 日志条目                                           │
  │     - 写入本地 WAL                                                 │
  │     - 发送 AppendEntries 到 Follower                               │
  │  ⑨ 等待多数派确认(Follower WAL 写入成功后回复)                   │
  │  ⑩ Commit Index 推进                                              │
  │  ⑪ 应用到状态机:                                                  │
  │     - 解码提交数据                                                 │
  │     - 检查/自动创建子表(如果使用 USING 语法)                      │
  │     - 写入 MemTable(跳表结构)                                    │
  │     - 触发流计算(如果有关联 Stream)                               │
  │     - 触发 RSMA(如果数据库启用了 Rollup)                          │
  │  ⑫ 构建响应(成功 + 影响行数)                                     │
  └───────────────────────────────────┬─────────────────────────────┘
                                      │
                                      ▼ RPC 响应
  ┌─────────────────────────────────────────────────────────────────┐
  │                         客户端 (taosc)                           │
  │                                                                   │
  │  ⑬ 收到响应,检查状态码                                           │
  │     - 成功 → 返回影响行数给用户                                    │
  │     - VGroup 迁移/Leader 切换 → 刷新 Catalog,自动重试             │
  │     - 其他错误 → 返回错误码给用户                                  │
  │  ⑭ 用户收到结果                                                   │
  └─────────────────────────────────────────────────────────────────┘
1.1 批量写入的优化路径

当一条 INSERT 包含多张表的数据时:

sql 复制代码
INSERT INTO 
  d1001 VALUES (now, 10.3, 219, 0.31)
  d1002 VALUES (now, 12.6, 220, 0.28)
  d2001 VALUES (now, 9.8, 218, 0.35);

客户端会:

  1. 按表名 hash 将数据分组到不同 VGroup

  2. 为每个 VGroup 构建一条 SUBMIT 消息(包含该 VGroup 的所有表数据)

  3. 并行发送到各 VGroup 的 Leader

  4. 汇总所有响应后返回总影响行数

    批量写入分发:

    INSERT 包含 3 张表 → 按 VGroup 分组:
    VGroup 2: [d1001, d1002] → 发往 dnode 1
    VGroup 3: [d2001] → 发往 dnode 2

    两条 SUBMIT 消息并行发送,两个响应都回来后返回用户

1.2 自动建表写入
sql 复制代码
INSERT INTO d1001 USING meters TAGS ('California', 1) VALUES (now, 10.3, 219, 0.31);

当表不存在时的额外步骤:

  • VNode 收到 SUBMIT 后检查子表是否存在
  • 不存在 → 根据 USING 子句的超级表名和 TAGS 自动创建子表
  • 创建完成后继续写入数据
  • 整个过程在一次 SUBMIT 请求内完成,无需客户端额外操作

2. 查询操作全链路

SELECT avg(voltage) FROM power.meters WHERE location = 'California' INTERVAL(1h) 为例:

复制代码
查询全链路(18 步):

  ┌─────────────────────────────────────────────────────────────────┐
  │                         客户端 (taosc)                           │
  │                                                                   │
  │  ① SQL 文本发送到 taosc                                           │
  │  ② 词法分析 + 语法分析 → 生成 AST(抽象语法树)                   │
  │  ③ 语义分析:                                                     │
  │     - 从 Catalog 获取超级表 Schema(验证列名、类型)               │
  │     - 权限检查                                                    │
  │     - 类型推导和隐式转换                                          │
  │  ④ 生成逻辑计划(Scan → Filter → Interval → Aggregate)          │
  │  ⑤ 生成物理计划:                                                 │
  │     - 按 VGroup 拆分为子计划                                      │
  │     - VNode 层:每个 VGroup 执行 Scan + 本地聚合                  │
  │     - 合并层:汇总各 VGroup 的部分结果                            │
  │  ⑥ Scheduler 分发子计划:                                         │
  │     - 向每个 VGroup Leader 发送 QUERY 消息(携带子计划)          │
  │     - 在本地(或 QNode)准备合并任务                              │
  └───────────────────────────────────┬─────────────────────────────┘
                                      │
                          ┌───────────┼───────────┐
                          ▼           ▼           ▼
  ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
  │ VGroup 2      │ │ VGroup 3      │ │ VGroup 4      │
  │               │ │               │ │               │
  │ ⑦ 收到子计划  │ │ ⑦ 收到子计划  │ │ ⑦ 收到子计划  │
  │ ⑧ 反序列化    │ │               │ │               │
  │    物理计划   │ │               │ │               │
  │ ⑨ 执行算子链:│ │               │ │               │
  │   TableScan   │ │   TableScan   │ │   TableScan   │
  │   → Filter    │ │   → Filter    │ │   → Filter    │
  │   (location)  │ │   (location)  │ │   (location)  │
  │   → Interval  │ │   → Interval  │ │   → Interval  │
  │   → PartialAgg│ │   → PartialAgg│ │   → PartialAgg│
  │ ⑩ 生成部分结果│ │ ⑩            │ │ ⑩            │
  └───────┬───────┘ └───────┬───────┘ └───────┬───────┘
          │                  │                  │
          └──────────────────┼──────────────────┘
                             ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                    客户端 Merge 节点                              │
  │                                                                   │
  │  ⑪ 收到各 VGroup 的部分聚合结果                                   │
  │  ⑫ 排序合并(按时间窗口对齐)                                     │
  │  ⑬ 最终聚合(将 partial avg 合并为 final avg)                    │
  │  ⑭ 生成最终结果集                                                 │
  └───────────────────────────────────┬─────────────────────────────┘
                                      │
                                      ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  ⑮ 客户端通过 FETCH 逐批获取结果                                  │
  │  ⑯ 每次 FETCH 返回一批行(默认 4096 行/批)                       │
  │  ⑰ 直到所有结果返回完毕                                           │
  │  ⑱ 用户收到完整结果集                                             │
  └─────────────────────────────────────────────────────────────────┘
2.1 两阶段聚合

分布式查询采用两阶段聚合策略:

阶段 执行位置 操作
Partial(部分聚合) 各 VNode 本地 对本地数据执行初步聚合,输出中间结果
Final(最终聚合) 客户端或 QNode 合并各 VNode 的中间结果,计算最终值

例如 AVG 的两阶段:

  • Partial:计算本地的 SUMCOUNT
  • Final:SUM(所有 partial_sum) / SUM(所有 partial_count)
2.2 查询与写入的隔离

查询请求走查询队列 ,写入请求走写队列,两者互不阻塞:

  • 查询不持有写锁
  • 使用 MVCC(多版本并发控制)读取------查询看到的是查询开始时刻的一致性快照
  • 长时间查询不会阻塞写入

3. DDL 操作全链路

CREATE DATABASE power VGROUPS 4 REPLICA 3 为例:

复制代码
DDL 全链路(12 步):

  ┌─────────────────────────────────────────────────────────────────┐
  │                         客户端 (taosc)                           │
  │                                                                   │
  │  ① SQL 解析:识别为 DDL 语句(CREATE DATABASE)                   │
  │  ② DDL 不在客户端执行计划,直接封装为消息发往 MNode               │
  │  ③ 从 Catalog 获取 MNode Leader 地址                              │
  │  ④ 发送 CREATE_DB 消息到 MNode Leader                            │
  └───────────────────────────────────┬─────────────────────────────┘
                                      │
                                      ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                       MNode (Leader)                              │
  │                                                                   │
  │  ⑤ 验证请求:                                                     │
  │     - 数据库是否已存在                                            │
  │     - 用户权限检查                                                │
  │     - License/授权检查                                            │
  │     - 参数合法性校验                                              │
  │  ⑥ 资源分配:                                                     │
  │     - 计算各 DNode 负载评分                                       │
  │     - 为 4 个 VGroup 各选择 3 个最优 DNode                        │
  │     - 分配 Hash 范围                                              │
  │  ⑦ 创建事务(重试策略、DB 级冲突检测)                            │
  │  ⑧ PREPARE 阶段:通过 Raft 持久化事务 + 中间状态元数据            │
  └───────────────────────────────────┬─────────────────────────────┘
                                      │
                          ┌───────────┼───────────┐
                          ▼           ▼           ▼
  ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
  │   DNode 1     │ │   DNode 2     │ │   DNode 3     │
  │               │ │               │ │               │
  │ ⑨ 收到创建    │ │ ⑨ 收到创建    │ │ ⑨ 收到创建    │
  │    VNode 消息 │ │    VNode 消息 │ │    VNode 消息 │
  │    在本地创建 │ │    在本地创建 │ │    在本地创建 │
  │    数据目录   │ │    数据目录   │ │    数据目录   │
  │    初始化存储 │ │    初始化存储 │ │    初始化存储 │
  │    返回成功   │ │    返回成功   │ │    返回成功   │
  └───────┬───────┘ └───────┬───────┘ └───────┬───────┘
          │                  │                  │
          └──────────────────┼──────────────────┘
                             ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                       MNode (Leader)                              │
  │                                                                   │
  │  ⑩ 所有 DNode 确认成功                                            │
  │  ⑪ COMMIT 阶段:通过 Raft 将 DB 和 VGroup 状态更新为 READY       │
  │     事务完成,从事务表中删除                                       │
  └───────────────────────────────────┬─────────────────────────────┘
                                      │
                                      ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                         客户端 (taosc)                           │
  │                                                                   │
  │  ⑫ 收到成功响应,返回给用户                                       │
  │     客户端刷新 Catalog 缓存(新数据库的 VGroup 信息)             │
  └─────────────────────────────────────────────────────────────────┘

4. Catalog 缓存机制

Catalog 是客户端性能的关键------避免每次操作都向 MNode 查询元数据:

复制代码
Catalog 缓存层次:

  客户端请求
      │
      ▼
  Catalog 本地缓存查找
      │
      ├── 命中 → 直接使用(零网络开销)
      │
      └── 未命中 → 向 MNode 请求
            │
            ▼
          MNode 返回:
            - VGroup 列表(vgId, hashRange, EP Set)
            - 表 Schema(列定义、Tag 定义)
            - 版本号(用于失效检测)
            │
            ▼
          写入本地缓存,设置版本号

缓存失效场景

  • 操作返回 TSDB_CODE_SYN_NOT_LEADER → VGroup Leader 已切换,刷新 VGroup 路由
  • 操作返回 Schema 不匹配错误 → 表结构已变更,刷新表 Schema
  • 心跳检测到版本号变化 → 批量刷新过期缓存

5. 错误处理与自动重试

客户端对不同类型的错误采取不同的策略:

错误类型 行为
Leader 切换 刷新 Catalog,重新路由到新 Leader,自动重试
网络超时 RPC 层指数退避重试
VGroup 迁移 刷新 Catalog,重新路由,自动重试
Schema 不匹配 刷新表 Schema 缓存,重新编码数据,自动重试
权限不足 直接返回错误给用户
语法错误 直接返回错误给用户
磁盘满 直接返回错误给用户

6. 消息类型与路由总结

操作 消息类型 目标 路由方式
INSERT TDMT_VND_SUBMIT VNode Leader 表名 hash → VGroup → Leader EP
SELECT(分发子计划) TDMT_SCH_QUERY VNode/QNode 每个相关 VGroup
SELECT(获取结果) TDMT_SCH_FETCH VNode/QNode 查询 ID 关联
CREATE DATABASE TDMT_MND_CREATE_DB MNode Leader MNode EP Set
CREATE STABLE TDMT_MND_CREATE_STB MNode Leader MNode EP Set
DROP TABLE TDMT_MND_DROP_TB MNode Leader MNode EP Set
SHOW DNODES TDMT_MND_SHOW MNode Leader MNode EP Set
心跳 TDMT_MND_STATUS MNode Leader MNode EP Set

代码示例

观察查询执行计划

sql 复制代码
-- 使用 EXPLAIN 查看查询的物理执行计划
EXPLAIN SELECT avg(voltage) FROM power.meters 
  WHERE location = 'California' INTERVAL(1h);

-- 输出示例(简化):
-- -> Merge (columns=2)
--   -> Partial Interval [1h] on meters (columns=2) on VGroup 2
--   -> Partial Interval [1h] on meters (columns=2) on VGroup 3
--   -> Partial Interval [1h] on meters (columns=2) on VGroup 4

-- 可以看到查询被拆分为 3 个子计划(3 个 VGroup)
-- 各 VGroup 执行 Partial Interval,最后 Merge 汇总

观察写入路由

sql 复制代码
-- 查看表所属的 VGroup
SHOW power.VGROUPS;

-- 写入后通过 VGroup 统计确认数据分布
-- tables 列显示每个 VGroup 中的子表数量

连接后 Catalog 预热

sql 复制代码
-- 首次查询某数据库时,Catalog 需要从 MNode 拉取元数据
-- 第一次查询可能稍慢(多一次 MNode 往返)
SELECT * FROM power.meters LIMIT 1;  -- 触发 Catalog 缓存

-- 后续查询直接使用缓存,无额外延迟
SELECT avg(voltage) FROM power.meters INTERVAL(1h);

性能考量

各阶段耗时占比

阶段 典型耗时(同机房) 瓶颈因素
SQL 解析 < 0.1ms CPU
Catalog 查询(缓存命中) 0 ms ---
Catalog 查询(缓存未命中) 1-5 ms 网络 RTT
数据编码 < 0.1ms CPU
RPC 传输 0.1-0.5 ms 网络
WAL 写入 0.05-0.5 ms 磁盘 IOPS
Raft 复制确认 0.5-2 ms 网络 RTT
MemTable 写入 < 0.1ms CPU/内存

优化建议

场景 优化方式
批量写入 单条 INSERT 包含多行多表,减少 RPC 次数
高频写入 使用 STMT2 参数绑定,避免重复 SQL 解析
跨 VGroup 查询慢 增加 VGROUPS 数量提高并行度
DDL 慢 减少 VGROUPS 数量降低事务动作数
首次查询慢 连接后执行轻量查询预热 Catalog
Catalog 频繁失效 检查是否频繁执行 Schema 变更

FAQ

Q1: 写入时客户端需要知道数据发往哪个节点吗?

不需要用户关心。taosc 驱动自动完成路由:根据表名计算 hash 值 → 查找 Catalog 缓存确定 VGroup → 获取 VGroup Leader 的地址 → 直接发送。对用户完全透明。

Q2: 查询时数据从 Leader 还是 Follower 读取?

默认从 Leader 读取(保证强一致性)。这是因为查询需要获取最新的 commitIndex 数据。当前版本不支持 Follower 读。

Q3: 如果写入中途 Leader 切换了怎么办?

客户端会收到 TSDB_CODE_SYN_NOT_LEADER 错误,然后:

  1. 自动刷新 Catalog 获取新 Leader 地址
  2. 重新发送写入请求到新 Leader
  3. 对用户透明(重试在 RPC 层自动完成)

Q4: 一条 INSERT 包含 1000 张表的数据,是发 1000 次请求吗?

不是。客户端将 1000 张表按 VGroup 分组,每个 VGroup 只发 1 条 SUBMIT 消息。如果有 4 个 VGroup,则总共只发 4 条消息(并行发送)。这就是为什么批量 INSERT 比逐条 INSERT 效率高很多。

Q5: DDL 和 DML 走同一条路径吗?

不同:

  • DML(INSERT/SELECT):客户端直接与 VNode 通信
  • DDL(CREATE/ALTER/DROP):客户端与 MNode 通信,MNode 再通过事务机制协调各 DNode

DDL 操作经过 MNode 的事务引擎,有冲突检测和原子性保证。DML 操作通过 VNode 的 Raft 协议保证一致性。

Q6: 查询结果是一次性返回还是分批返回?

分批返回 。查询执行后,客户端通过 FETCH 消息逐批获取结果(每批默认 4096 行)。这避免了大结果集一次性占用过多内存。在 API 层面,taos_fetch_row / taos_fetch_block 封装了这个过程。

Q7: QNode 和 VNode 上的查询有什么区别?

  • VNode 查询:在数据所在节点执行,适合带 WHERE 条件的扫描(数据本地化)
  • QNode 查询:独立的计算节点,不存储数据,适合纯计算型的合并和聚合

默认情况下查询在 VNode 上执行。创建 QNode 后,合并层可以卸载到 QNode,减轻 VNode 的 CPU 负担。

Q8: 为什么有时候 INSERT 返回的行数比发送的少?

可能原因:

  1. 部分行的时间戳重复(同一子表同一时间戳视为更新,不增加行数计数)
  2. 部分表不存在且未使用 USING 自动建表语法
  3. 数据类型不匹配导致部分行被拒绝

检查返回的错误码可以了解具体原因。

参考

系统构架篇

关于 TDengine

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

相关推荐
それども6 小时前
怎么理解 LEFT JOIN 和 LEFT SEMI JOIN
java·数据库·mysql
2601_957786776 小时前
深度解析:星链引擎全域智能营销矩阵系统的技术架构与实践
大数据
qxwlcsdn6 小时前
CSS如何实现元素镜像翻转_使用transformscalex负值
jvm·数据库·python
2301_803934616 小时前
mysql如何处理大量重复值索引_mysql索引存储特征分析
jvm·数据库·python
jran-7 小时前
MySQL 用户与权限
数据库·mysql
無限進步D7 小时前
MySQL 排序与分页
数据库·mysql
大G的笔记本7 小时前
Redis 分布式锁自动续期机制
数据库·redis·分布式
Solis程序员7 小时前
跳出 CRUD:深入剖析 Redis 管道 Pipeline 底层通信机制
数据库·redis·缓存
夏贰四7 小时前
数据转换分哪些应用类型?数据转换如何做好规范管控?
大数据·数据库·数据转换