DuckDB是一个进程内嵌入式的分析型数据库,由荷兰CWI孵化,设计目标是在单机上高效处理分析查询。它常被拿来和SQLite对比,但两者方向不同:SQLite面向事务,DuckDB面向分析。由于列式存储和向量化执行,聚合、过滤、扫描等任务比传统行式数据库快很多。不过正是这种轻量、快速的表象,让不少人忽略了它在并发、事务、数据规模上的边界。

一、定位误区:DuckDB不是事务数据库
最常见的一个误区是把DuckDB当作MySQL、PostgreSQL或SQLite的替代品,直接用于业务系统的读写。DuckDB虽然支持SQL和事务,但它的存储引擎、锁机制和优化目标都围绕批量分析查询设计,而不是高频单行插入、更新和删除。就算能执行UPDATE、DELETE,性能也远不如行式数据库,而且写操作通常以文件级或表级粒度协调,并发一高就容易卡住。
举个例子,如果你把用户订单表放在DuckDB里,前端每下一单就插入一行,同时还有多个报表查询在扫描这张表,可能很快就会出现锁等待或者写入吞吐骤降。这不是DuckDB有bug,而是它本来就适合在数据已经落盘或模型固化的前提下做批量加载和复杂查询,不适合作为在线事务处理的主库。
另一个相关误区是以为只要数据量不超过内存,DuckDB就能当缓存层来用。实际上它的优化器会生成分析型执行计划,点查速度不如专门的KV存储,而且每次连接初始化、元数据加载也有开销。因此选型时要先分清OLTP和OLAP的边界。
二、内存与持久化误区
DuckDB常被描述为内存优化,但这不代表所有数据必须放在内存里。它支持内存模式,也支持持久化到单个文件或多个文件。默认情况下如果只使用:memory:连接,数据在进程结束后就消失;如果使用文件路径打开,则修改会写入磁盘。内存优化更多是指执行过程中充分利用内存做缓存、压缩和批量运算,同时具备溢出到磁盘的能力。
例如你可以设置PRAGMA memory_limit='4GB'来限制最大内存使用。当查询所需内存超过限制时,DuckDB会把部分中间结果写到临时目录,而不是直接报错。这个机制很像其他数据库的spill to disk,但需要确保临时目录有足够空间。否则会看到类似内存不足的错误。另外,persistent文件默认会采用WAL机制保证一致性,异常退出后可以恢复,但不能简单地像拷贝普通文件那样在数据库打开时复制,可能得到损坏副本。
| 项目 | 内存模式 | 文件模式 |
|---|---|---|
| 数据保存 | 进程结束即丢失 | 落盘持久化 |
| 适用场景 | 临时分析、测试 | 固定数据仓库、重复查询 |
| 并发写入 | 单进程内可多连接,跨进程无 | 单写入者,多读取者只读 |
| 恢复能力 | 无 | 支持崩溃恢复 |
很多人在容器环境使用DuckDB时,把数据文件放在网络挂载盘或分布式文件系统上,结果出现锁失败、性能抖动或文件损坏。DuckDB文件模式依赖本地文件系统语义和锁机制,网络文件系统不一定兼容。尽量将数据库文件放在本地磁盘,再用对象存储保存原始Parquet或CSV,需要时加载到本地DuckDB分析。
三、数据导入与类型推断误区
用DuckDB导入CSV时,很多人直接执行read_csv_auto然后发现日期变成字符串、数字变成VARCHAR、空值变成空字符串。这是因为自动推断基于样本数据,如果前几行没有代表性,或者字段混入异常值,推断结果就会偏离。解决方法是先查看推断出的schema,再显式指定类型。
例如可以用SELECT * FROM read_csv_auto('data.csv', sample_size=-1)让全量扫描,不过大文件会慢一些。更稳妥的是用read_csv函数并传入columns参数指定每个字段类型。Parquet文件通常带有schema,但也要注意不同引擎生成的Parquet在时间戳精度、嵌套结构上的差异。导入后最好做一次行数校验和关键列空值统计,避免后续分析踩坑。
另一个高频问题是文件路径中的反斜杠。在Windows环境下,路径写作C:\data\orders.csv时,如果通过字符串拼接或某些接口传递,反斜杠可能被当作转义字符处理。最稳妥的做法是在SQL字符串中使用正斜杠C:/data/orders.csv,或者按语言规则正确转义反斜杠,例如在Python中写r'C:\data\orders.csv'。DuckDB本身对路径中的反斜杠是接受的,但要看上层调用方式。
四、SQL兼容与查询写法注意点
DuckDB的SQL方言与PostgreSQL比较接近,但和MySQL、SQL Server差异明显。比如字符串必须使用单引号,双引号留给标识符。如果写SELECT "hello"会被当成列名,而不是字符串。很多人从MySQL迁移过来会习惯用双引号包字符串,结果报列不存在。大小写方面,DuckDB默认对标识符不区分大小写,但保留原始大小写,双引号包裹后则精确匹配。
空值处理也容易出错。在DuckDB中,NULL AND FALSE的结果是FALSE,NULL OR TRUE的结果是TRUE,这与SQL标准一致,但和部分编程语言直觉不同。写过滤条件时,如果字段可能为NULL,建议显式使用IS NULL或COALESCE,不要假设NOT IN会剔除空值。例如WHERE col NOT IN (1,2,3)在col为NULL时结果不是TRUE,而是NULL,导致该行被过滤掉。
窗口函数、ASOF JOIN、UNNEST、LATERAL JOIN等高级功能在DuckDB中支持得很好,但有一些函数名和参数顺序与其他数据库不同。比如日期格式化使用strftime,字符串拼接可以使用||或concat。遇到不熟悉的函数,最好先查官方文档或简单测试,不要直接照搬MySQL/Postgres写法。
五、并发、资源与部署注意事项
DuckDB的主要优势在单机分析,但并发控制是比较容易忽视的地方。同一个持久化文件在同一时间只允许一个进程以读写模式打开,其他进程只能只读。如果两个Python进程同时尝试向同一个库写入,后面一个会报锁错误。这个限制是为了降低复杂度、保证一致性,代价就是无法像服务器数据库那样支持高并发写入。
在单个进程内,DuckDB支持多个连接,写操作会通过内部锁协调,但吞吐仍然有限。对于需要周期性更新的场景,可以采用单写入者模式,把数据生成或ETL任务集中到一个进程,分析查询通过只读连接访问同一文件或从Parquet直接查询。内存方面通过PRAGMA memory_limit控制,线程数通过PRAGMA threads设置,通常保持默认或设为物理核数即可。
如果是在Jupyter、Airflow等环境使用,注意不要在每个task里都重新初始化数据库并重新装载数据。DuckDB加载Parquet和建立元数据是有成本的,最好复用连接,或者在内存中保留关系对象。对于跨任务共享数据,可以考虑将中间结果写成Parquet,再让下游用DuckDB直接查询文件,避免重复装载。
六、高频问题速查与避坑清单
- 不要拿DuckDB做OLTP主库,写入并发和点查性能不适合在线交易。
- CSV导入先明确schema,必要时关闭自动推断或全量采样。
- 持久化文件不要放在网络盘,尽量用本地磁盘或临时目录。
- 同一数据文件避免多进程写入,读取方也要注意锁版本兼容。
- 查询时留意NULL比较和字符串引号规则,跨数据库迁移要重新测试SQL。
- 设置合理的memory_limit和threads,防止单个查询拖垮整个机器。
- 定期升级DuckDB版本,新版本在类型推断、函数支持和稳定性上改进较快。
总的来说,DuckDB适合数据量在GB到百GB级别、以分析查询为主、单机或单进程部署的场景。理解它的定位和限制后,很多看似奇怪的问题都能提前避开。遇到报错时,先检查是否在错误场景下使用,再排查SQL类型和文件锁问题,通常能快速定位。