数据库基础体系 · 第 55/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
DuckDB 嵌入式分析:列式执行、Parquet、SQL 和本地数据工程
DuckDB 是一个面向分析型负载的嵌入式关系数据库管理系统。它与 SQLite 一样通常作为库链接到应用进程中,但目标不同:
- SQLite 主要面向事务型、点查询较多的应用数据;
- DuckDB 主要面向扫描大量数据、过滤、聚合、连接和导出结果的 OLAP 任务;
- ClickHouse 通常作为独立服务运行,面向持续写入、远程访问和大规模集群分析;
- DuckDB 则强调在本地进程内直接分析数据库文件、Parquet 文件、CSV 文件和其他数据源。
“嵌入式”描述的是部署和调用方式;“列式执行”描述的是执行引擎如何处理数据;“Parquet”是重要的外部列式文件格式;“SQL”是主要的数据操作接口;“本地数据工程”则是把这些能力组合成可复现的数据导入、清洗、分析和导出流程。
这几个概念不能混为一谈:DuckDB 可以对 Parquet 进行查询,但 Parquet 不是 DuckDB 的数据库文件格式;DuckDB 内部也可以持久化自己的表,而不只是读取外部文件。
一、DuckDB 的定位:数据库作为进程内分析组件
传统数据库通常由独立服务提供:
应用程序 ──网络协议──> 数据库服务器 ──> 数据文件
DuckDB 的典型结构是:
应用程序
└── DuckDB 库
├── SQL 解析器与绑定器
├── 优化器
├── 向量化执行引擎
├── 事务与存储管理
└── 文件、Parquet、CSV 等数据源
应用通过函数调用执行 SQL,而不是通过 TCP 连接远程服务:
import duckdb
con = duckdb.connect("analytics.duckdb")
result = con.execute("""
SELECT 42 AS answer
""").fetchall()
print(result)
con.close()
预期输出:
[(42,)]
duckdb.connect("analytics.duckdb") 打开或创建一个持久化数据库文件。如果使用:
con = duckdb.connect(":memory:")
则数据库主要存在于当前进程的内存中,连接关闭后数据通常不再保留。
1. 持久化数据库和内存数据库
两种连接方式的边界不同:
| 连接方式 | 数据生命周期 | 适合场景 |
|---|---|---|
:memory: |
当前连接或进程生命周期 | 临时分析、测试、单次转换 |
analytics.duckdb |
数据库文件生命周期 | 本地数据集、可重复分析、应用内分析 |
持久化数据库文件不是普通的 CSV 或 Parquet 文件。它包含 DuckDB 自己管理的表、元数据、事务状态和存储结构。应当通过 DuckDB 打开它,而不是直接用文本工具修改。
2. 嵌入式不等于只能单线程
DuckDB 可以在单个进程中使用多个线程执行查询,也可以由应用创建多个连接。线程数和并行执行属于运行时行为,不能简单地把“嵌入式”理解成“单线程”。
但是,嵌入式数据库与独立数据库服务的并发边界不同。生产设计至少要区分:
- 单进程内的多个连接:适合由同一个应用进程协调并发访问;
- 多个进程读取同一个数据库文件:需要遵守 DuckDB 对共享文件访问的支持边界,通常应使用只读方式;
- 多个进程同时写入同一个数据库文件:不能把它当作服务型数据库的多客户端写入模型,通常不支持这种架构。
如果需要多个独立进程持续并发写入,应该选择专门的服务型数据库,或者让一个进程负责写入、其他进程通过明确的数据交换边界读取。
二、为什么分析任务适合列式处理
1. 行式和列式
假设有如下订单表:
| order_id | customer_id | order_date | amount | status |
|---|---|---|---|---|
| 1 | 10 | 2025-01-01 | 100 | paid |
| 2 | 11 | 2025-01-02 | 200 | canceled |
| 3 | 10 | 2025-01-03 | 150 | paid |
行式存储倾向于按行组织:
(1, 10, 2025-01-01, 100, paid)
(2, 11, 2025-01-02, 200, canceled)
(3, 10, 2025-01-03, 150, paid)
列式存储倾向于按列组织:
order_id: 1, 2, 3
customer_id: 10, 11, 10
order_date: 2025-01-01, 2025-01-02, 2025-01-03
amount: 100, 200, 150
status: paid, canceled, paid
对于点查询:
SELECT *
FROM orders
WHERE order_id = 2;
行式布局可以快速得到完整的一行。
对于分析查询:
SELECT SUM(amount)
FROM orders
WHERE status = 'paid';
只需要 amount 和 status 两列。列式布局可以避免读取 order_id、customer_id 和 order_date,并且相同类型的数据更容易压缩和批量计算。
2. 投影和选择
关系代数中,查询通常可以拆成两个基础操作:
- 投影:选择需要的列,例如
amount; - 选择:筛选满足条件的行,例如
status = 'paid'。
对于查询:
SELECT SUM(amount)
FROM orders
WHERE status = 'paid';
逻辑过程可以写成:
其中:
- 表示选择;
- 表示聚合;
- 是过滤后的中间关系。
列式执行的关键收益是:执行过滤时主要读取 status,执行聚合时主要读取 amount。如果数据源还提供每个数据块的统计信息,则连某些不可能命中的数据块也可以跳过。
3. 向量化执行
DuckDB 不是一次只处理一行,也不是必须把整张表全部加载到内存。它通常以一批批向量化数据执行算子:
输入扫描
└── 一批 status 值
└── 生成布尔选择向量
└── 从 amount 中选择对应位置
└── 聚合器更新 SUM 状态
可以抽象为:
DataChunk
├── status: [paid, canceled, paid, paid]
└── amount: [100, 200, 150, 80]
过滤结果:
selection = [0, 2, 3]
聚合输入:
amount[0], amount[2], amount[3] = 100, 150, 80
SUM = 330
这种方式有几个效果:
- 减少逐行解释和函数调用开销;
- 更适合 CPU 缓存和 SIMD 批量计算;
- 中间结果可以按批次流过执行管线;
- 不必为整个查询结果建立一个完整的行式中间表。
但“列式执行”不表示所有操作都只使用简单的数组。连接、排序、窗口函数和复杂表达式仍可能需要哈希表、排序缓冲区或较大的中间状态。列式执行优化的是数据处理方式,而不是消除所有内存和计算成本。
4. 列式执行不等于列式持久化
这是一个常见误解:
- DuckDB 执行查询时采用向量化、列式友好的处理方式;
- DuckDB 自己的持久化存储也面向分析负载;
- Parquet 是外部文件格式;
- CSV 是文本交换格式。
因此,下面三件事要区分:
DuckDB 查询 Parquet
= DuckDB 作为执行引擎 + Parquet 作为外部数据源
DuckDB 保存表
= DuckDB 执行引擎 + DuckDB 自己的数据库存储
DuckDB 查询 CSV
= DuckDB 执行引擎 + CSV 扫描器
三、Parquet:DuckDB 的高效外部数据源
1. Parquet 的基本结构
Parquet 是一种面向分析的列式文件格式。一个文件通常包含:
文件
├── Row Group 1
│ ├── order_id 列块
│ ├── order_date 列块
│ └── amount 列块
├── Row Group 2
│ ├── order_id 列块
│ ├── order_date 列块
│ └── amount 列块
└── 文件元数据
这里有三个重要层次:
- Row group:一批行的逻辑分组;
- Column chunk:某个 row group 中的一列;
- Page:列块内部进一步组织和编码的数据单元。
Parquet 还可以保存列类型、压缩方式以及部分统计信息,例如某个 row group 的最小值和最大值。
2. 投影下推
查询:
SELECT SUM(amount)
FROM 'orders.parquet';
如果结果只依赖 amount,读取器通常可以只读取 amount 列,而不是把整个文件的所有列都解码出来。这称为投影下推。
正式地说,若查询表达式依赖的列集合为:
而文件包含:
那么理想情况下,读取列集合可以缩小为:
实际查询还可能需要读取过滤条件中的列。例如:
SELECT SUM(amount)
FROM 'orders.parquet'
WHERE status = 'paid';
此时:
order_id、customer_id 和 order_date 不参与这个查询,可以避免读取。
3. 谓词下推和 Row Group 跳过
如果 Parquet 的元数据表明某个 row group 中:
order_date 最小值 = 2025-01-01
order_date 最大值 = 2025-01-31
查询条件是:
WHERE order_date >= DATE '2025-03-01'
则该 row group 不可能包含满足条件的行,可以直接跳过。
其判定条件是:
其中:
- 是某个 row group 中
order_date的取值集合; - 是该组最大日期;
- 是查询下界;
- 若最大值都小于下界,则整个组都不可能命中。
对于范围查询:
WHERE order_date >= DATE '2025-03-01'
AND order_date < DATE '2025-04-01'
如果 row group 的最大值小于 2025-03-01,或者最小值大于等于 2025-04-01,也可以跳过。
但这不是无条件保证。能否跳过取决于:
- 文件是否保存了可用的统计信息;
- row group 的边界是否与查询条件有区分度;
- 谓词是否能够被读取器识别;
- 表达式是否对列进行了复杂变换。
例如:
WHERE year(order_date) = 2025
通常不如直接写成日期范围容易利用原始日期统计信息:
WHERE order_date >= DATE '2025-01-01'
AND order_date < DATE '2026-01-01'
这不是因为前一个条件语义错误,而是因为后一个形式更容易与存储层的最小值、最大值进行比较。
4. 直接查询 Parquet
DuckDB 支持把 Parquet 路径作为表源:
SELECT
order_date,
SUM(amount) AS total_amount
FROM 'data/orders.parquet'
WHERE order_date >= DATE '2025-01-01'
AND order_date < DATE '2025-02-01'
GROUP BY order_date
ORDER BY order_date;
也可以显式使用扫描函数:
SELECT COUNT(*)
FROM read_parquet('data/orders.parquet');
多个文件可以使用 glob:
SELECT *
FROM read_parquet('data/2025-01/*.parquet');
使用 glob 时,所有匹配文件最好具有兼容的列名和类型。否则可能出现列缺失、类型合并失败或结果与预期不一致。文件名匹配规则也属于文件系统行为,应在部署环境中验证,而不能假设开发机和生产机完全一致。
5. 写出 Parquet
从 DuckDB 表或查询结果写出 Parquet:
COPY (
SELECT
order_date,
status,
amount
FROM orders
WHERE amount > 0
)
TO 'out/orders_clean.parquet'
(FORMAT PARQUET);
这里的执行过程是:
- 执行子查询;
- 过滤
amount <= 0的行; - 只保留三个输出列;
- 将结果编码为 Parquet;
- 写入目标文件。
如果目标文件已存在,应明确处理覆盖策略,避免把“重新生成数据集”误当作“追加写入”。文件导出与数据库表更新也不是同一个事务边界:写 Parquet 的失败不会自动回滚已经提交到另一个数据库的业务操作。
四、用 SQL 完成一次本地数据工程流程
下面构造一个完整、可运行的例子。它不依赖外部 CSV 或 Parquet 文件,先创建订单表,再清洗、聚合并导出 Parquet。
1. 建立数据和表
CREATE OR REPLACE TABLE orders AS
SELECT *
FROM (
VALUES
(1, 101, DATE '2025-01-01', 'paid', 120.00),
(2, 101, DATE '2025-01-03', 'canceled', 80.00),
(3, 102, DATE '2025-01-04', 'paid', 250.00),
(4, 103, DATE '2025-01-08', 'paid', 100.00),
(5, 102, DATE '2025-02-01', 'paid', 300.00)
) AS t(order_id, customer_id, order_date, status, amount);
VALUES 提供常量关系,AS t(...) 显式定义列名。日期使用 DATE 'YYYY-MM-DD',使类型明确,而不是依赖字符串隐式转换。
查看数据:
SELECT * FROM orders ORDER BY order_id;
预期结果:
order_id | customer_id | order_date | status | amount
----------+-------------+------------+-----------+--------
1 | 101 | 2025-01-01 | paid | 120.00
2 | 101 | 2025-01-03 | canceled | 80.00
3 | 102 | 2025-01-04 | paid | 250.00
4 | 103 | 2025-01-08 | paid | 100.00
5 | 102 | 2025-02-01 | paid | 300.00
2. 清洗和聚合
SELECT
order_date,
COUNT(*) AS paid_orders,
SUM(amount) AS paid_amount
FROM orders
WHERE status = 'paid'
GROUP BY order_date
ORDER BY order_date;
预期结果:
order_date | paid_orders | paid_amount
------------+-------------+------------
2025-01-01 | 1 | 120.00
2025-01-04 | 1 | 250.00
2025-01-08 | 1 | 100.00
2025-02-01 | 1 | 300.00
逻辑上,数据库先筛出四条 paid 记录,再按 order_date 分组。物理执行时,优化器可能重排过滤、投影和聚合,但必须保持 SQL 的可观察语义。
一个重要边界是 NULL。例如:
SELECT SUM(amount), COUNT(amount), COUNT(*)
FROM orders;
SUM(amount)忽略amount为NULL的行;COUNT(amount)只计算非NULL的amount;COUNT(*)计算行数。
因此,不能把 COUNT(amount) 自动理解为“总行数”。
3. 导出分析结果
COPY (
SELECT
customer_id,
SUM(amount) AS paid_amount
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
ORDER BY customer_id
)
TO 'out/customer_paid_amount.parquet'
(FORMAT PARQUET);
结果逻辑上是:
customer_id | paid_amount
-------------+------------
101 | 120.00
102 | 550.00
103 | 100.00
随后可以重新读取:
SELECT *
FROM 'out/customer_paid_amount.parquet'
ORDER BY customer_id;
这个流程体现了 DuckDB 的典型用途:
本地原始数据
└── SQL 清洗
└── 聚合或连接
└── Parquet 结果
如果结果只是临时使用,可以不创建持久化表,直接把扫描、转换和导出写在一个 COPY (SELECT ...) TO ... 中。
五、CSV、Parquet 和 DuckDB 表的取舍
1. CSV 的优点和代价
CSV 适合作为交换格式,因为:
- 人可以阅读;
- 许多工具都能生成;
- 格式简单。
但 CSV 没有可靠的内建类型表达能力。以下值可能被解释为:
00123
2025-01-01
NULL
1,234.50
它们到底是字符串、整数、日期、空值还是带千位分隔符的数字,取决于读取配置和推断规则。
CSV 通常还具有:
- 解析成本较高;
- 无列级压缩;
- 无 row group 统计信息;
- 查询时常常需要扫描文本。
因此,CSV 更适合数据入口,不一定适合作为反复分析的中间格式。
2. Parquet 的优点和代价
Parquet 更适合:
- 分析型扫描;
- 多次重复读取;
- 只读取部分列;
- 按日期或其他字段过滤;
- 在本地数据湖中保存中间结果。
但 Parquet 不是事务数据库。它通常是不可变或追加生成的文件集合,而不是提供完整数据库表语义的单一存储系统。更新一部分记录往往意味着重写文件或生成新的文件版本。
3. DuckDB 表的优点和代价
将数据写入 DuckDB 表:
CREATE TABLE orders_copy AS
SELECT * FROM orders;
适合:
- 在同一个数据库文件中保存多个中间表;
- 需要事务控制;
- 需要反复执行 SQL;
- 不希望每次都重新解析外部文本文件。
代价是数据被放进 DuckDB 自己的存储格式,其他工具不能像读取 Parquet 那样直接读取其中的表。跨工具交换时,通常需要再导出 Parquet 或其他格式。
可以用下面的判断:
需要跨工具共享 -> Parquet
需要人类查看或简单交换 -> CSV
需要本地 SQL 状态和事务 -> DuckDB 数据库文件
需要一次性查询外部数据 -> 直接扫描外部文件
六、SQL 在 DuckDB 中扮演什么角色
SQL 不是简单的字符串接口,而是从声明式关系操作到物理执行计划的入口。
查询:
SELECT
customer_id,
SUM(amount) AS total
FROM orders
WHERE status = 'paid'
GROUP BY customer_id;
至少包含这些语义步骤:
- 从
orders读取关系; - 检查列名和类型;
- 过滤
status = 'paid'; - 按
customer_id分组; - 计算每组
SUM(amount); - 产生结果列。
优化器可以利用等价变换,例如先过滤再聚合、提前裁剪无用列。但优化器不能改变:
NULL的语义;- 聚合结果;
- 连接的匹配规则;
- 事务可见性;
- 显式排序要求。
1. 参数化查询
应用程序不要把用户输入直接拼接到 SQL 字符串:
customer_id = 102
rows = con.execute("""
SELECT order_id, amount
FROM orders
WHERE customer_id = ?
ORDER BY order_id
""", [customer_id]).fetchall()
print(rows)
预期输出:
[(3, 250.0), (5, 300.0)]
参数化主要用于值,不适用于任意表名或列名。如果表名来自外部输入,应采用白名单映射,而不是直接拼接。
2. 类型转换应显式
例如日期过滤:
WHERE order_date >= DATE '2025-01-01'
比依赖隐式字符串转换更清晰。对金额数据,应该在数据入口明确使用合适的数值类型;浮点数和精确十进制数在舍入语义上不同,不能把金额字段的类型选择留给自动推断。
七、事务、状态和故障路径
1. 事务的基本状态
事务可以抽象为:
未开始
└── BEGIN
└── 活跃事务
├── COMMIT -> 已提交
└── ROLLBACK -> 已回滚
示例:
BEGIN;
CREATE TABLE IF NOT EXISTS daily_summary AS
SELECT
order_date,
SUM(amount) AS total_amount
FROM orders
WHERE status = 'paid'
GROUP BY order_date;
COMMIT;
如果创建或查询过程中出错,应用应根据错误处理策略执行回滚:
BEGIN;
-- 可能失败的写入操作
INSERT INTO daily_summary
SELECT
order_date,
SUM(amount)
FROM orders
WHERE status = 'paid'
GROUP BY order_date;
ROLLBACK;
事务保护的是数据库内部操作。以下操作的边界需要单独判断:
数据库事务
├── 表创建和表更新:属于数据库事务
└── 外部 Parquet 文件写出:是文件系统操作
如果一个事务已经提交,之后导出 Parquet 失败,数据库不会自动恢复到导出之前。需要应用层采用临时文件、校验成功后重命名、版本目录或元数据提交等方法协调这两个系统。
2. 中断、磁盘空间和文件损坏
本地工程中常见故障包括:
- 目标路径无写权限;
- 磁盘空间不足;
- 进程被终止;
- 外部文件在读取过程中被其他程序替换;
- 多个进程违反数据库文件并发访问边界;
- 输入文件之间 schema 不一致。
诊断顺序通常应包括:
- 检查 SQL 是否能在较小数据集上运行;
- 检查输入文件是否能独立读取;
- 检查列名和数据类型;
- 检查目标目录权限和剩余空间;
- 检查是否有多个写入者;
- 检查输出文件是否只生成了一部分,并从临时路径重新导出。
不要把一个已经失败的 Parquet 输出文件直接当作成功产物。更稳妥的流程是:
写入 out/result.parquet.tmp
└── 成功关闭并验证
└── 原子地移动为 out/result.parquet
具体的原子替换语义仍取决于操作系统和文件系统,不能脱离部署环境假设。
八、查询外部文件时的性能边界
1. 过滤条件不一定都能减少读取量
下面的查询语义正确:
SELECT SUM(amount)
FROM 'orders.parquet'
WHERE status = 'paid';
但如果 status 的值在每个 row group 中都混杂,统计信息无法排除任何组,那么仍然可能读取大部分文件。
同样,下面的条件可能造成更多解码工作:
WHERE lower(status) = 'paid'
如果文件中的 status 已经规范化为小写,直接写:
WHERE status = 'paid'
更容易利用原始列和统计信息。
2. 文件布局会影响跳过效果
假设按月份生成文件,但每个文件内部日期完全混杂:
data/2025-01/*.parquet
data/2025-02/*.parquet
按月份的路径组织可以先减少文件集合;如果 row group 内的日期也具有局部有序性,则还可能进一步利用 row group 统计信息。
反过来,如果所有日期随机混合,文件名分区仍能提供文件级筛选,但 row group 跳过能力会变弱。
这与 ClickHouse 中排序键影响数据跳过的思想相似,但机制不同:
- DuckDB 查询 Parquet 时,主要依赖文件集合、Parquet row group 和列统计信息;
- ClickHouse 的 MergeTree 表由其自身存储引擎管理分区、排序键、稀疏索引和数据跳过;
- DuckDB 的外部 Parquet 文件不会自动获得 ClickHouse MergeTree 的索引语义。
3. 读取全部列会消除列式优势
下面的写法可能造成不必要的数据读取:
SELECT *
FROM 'orders.parquet'
WHERE order_date >= DATE '2025-01-01';
如果最终只需要金额汇总,应明确列:
SELECT SUM(amount)
FROM 'orders.parquet'
WHERE order_date >= DATE '2025-01-01';
SELECT * 在探索阶段方便,但在稳定的数据管道中会让输入 schema 变化直接影响结果列和读取成本。
九、Python 中的完整生命周期
下面的代码展示连接、事务、参数化查询和资源关闭:
import duckdb
from pathlib import Path
db_path = "local_analytics.duckdb"
output_path = Path("out/customer_summary.parquet")
output_path.parent.mkdir(parents=True, exist_ok=True)
con = duckdb.connect(db_path)
try:
con.execute("BEGIN")
con.execute("""
CREATE OR REPLACE TABLE orders AS
SELECT *
FROM (
VALUES
(1, 101, DATE '2025-01-01', 'paid', 120.00),
(2, 101, DATE '2025-01-03', 'canceled', 80.00),
(3, 102, DATE '2025-01-04', 'paid', 250.00),
(4, 103, DATE '2025-01-08', 'paid', 100.00)
) AS t(order_id, customer_id, order_date, status, amount)
""")
result = con.execute("""
SELECT customer_id, SUM(amount) AS paid_amount
FROM orders
WHERE status = ?
GROUP BY customer_id
ORDER BY customer_id
""", ["paid"]).fetchall()
print(result)
# [(101, Decimal('120.00')), (102, Decimal('250.00')),
# (103, Decimal('100.00'))]
#
# 实际数值展示形式可能随客户端类型映射而不同,
# 但逻辑结果应为上述三组。
temporary_output = str(output_path) + ".tmp"
con.execute("""
COPY (
SELECT customer_id, SUM(amount) AS paid_amount
FROM orders
WHERE status = 'paid'
GROUP BY customer_id
ORDER BY customer_id
)
TO ?
(FORMAT PARQUET)
""", [temporary_output])
con.execute("COMMIT")
# 真实工程中还应在这里检查临时文件存在、大小和可读取性,
# 验证通过后再替换正式输出文件。
Path(temporary_output).replace(output_path)
except Exception:
con.execute("ROLLBACK")
raise
finally:
con.close()
这个例子包含几个边界:
BEGIN和COMMIT管理数据库内部状态;COPY生成的是外部文件;ROLLBACK不能撤销已经完成的文件系统写入;- 临时文件替换属于应用层的发布策略;
- 参数化可以保护
status这个值,但路径处理仍需结合文件系统权限和白名单策略。
如果只是一次性内存转换,可以将连接改为:
con = duckdb.connect(":memory:")
如果需要跨脚本复用表,则必须使用持久化数据库文件,并明确谁负责写入。
十、嵌入式分析的适用边界
DuckDB 很适合以下工作:
- 在开发机或任务容器中分析本地数据;
- 将 CSV 转换为 Parquet;
- 对一组 Parquet 文件执行 SQL 聚合;
- 在 ETL、Notebook、测试和数据质量检查中嵌入查询引擎;
- 在应用中提供本地分析能力;
- 构建单进程或受控并发的批处理任务。
它不应被误解为:
- 任意多个进程共享写入的数据库服务;
- 自动提供分布式集群协调;
- 替代所有 OLTP 数据库;
- 仅仅是一个 Parquet 解析库;
- 只适合把数据全部加载进内存的工具。
选择 DuckDB 时,核心问题不是“它是不是数据库”,而是确认数据流边界:
数据在哪里?
由谁写入?
有多少并发写入者?
查询是一次性还是持续服务?
结果需要事务一致性还是文件版本发布?
是否需要远程客户端和权限管理?
如果数据主要位于本地文件或对象存储,分析以批处理为主,并且可以控制任务进程,DuckDB 能把数据库执行引擎直接放入数据工程脚本中。若需要长期运行的多租户服务、细粒度权限、跨进程高并发写入和统一运维入口,则应评估服务型数据库或专门的分析平台。
DuckDB 的价值在于缩短这条路径:
文件或本地表
-> SQL 关系操作
-> 列式、向量化执行
-> Parquet 或应用结果
理解执行方式、文件格式和事务边界之后,嵌入式并不意味着功能简化,而是意味着数据库的生命周期由应用程序和数据管道直接承担。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:InfluxDB 时序数据:时间模型、Schema、写入、查询和保留策略
- 下一篇:TiDB 分布式 SQL:计算存储分离、Raft、事务与扩缩容
- 延伸:ClickHouse 列式建模:MergeTree、排序键、分区和数据跳过
- 延伸:SQLite 架构与文件格式:嵌入式数据库、Pager、B-tree 和 VFS
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论