【DuckDB】在DuckDB中查询ORACLE的数据的三种方式
背景
DuckDB出来好多年了(2019~xxxx),早就有了查mysql和postgresql数据库的插件,但是对于查ORACLE,却一直只能通过odbc转,用着很不方便。我都想自己用AI搓一个出来。但最近瞅到德哥的数据库新闻,发现DuckDB的社区插件里有oracle_scanner了,就来看看是怎么回事。
注:DuckDB的插件有区分官方插件和社区插件。社区插件也是要官方评审后才能进入插件目录的。
(前一阵子我用AI搓了个 opengauss_scanner 出来了,当然和oracle相比,难度不可同日而语)
调研
首先我直接搜 DuckDB + oracle_scanner ,发现竟然有两个不同的插件:
- 一个是被社区收录的 https://github.com/krokozyab/quack-oracle (https://duckdb.org/community_extensions/extensions/oracle_scanner)
- 还有一个是没被社区收录的 https://github.com/cofin/duckdb-oracle
前者是自行模拟了ORACLE的原生协议,因此使用无需安装任何ORACLE客户端;
而后者是使用ORACLE INSTANCE CLIENT,所以需要用户自备ORACLE客户端。
再加上早就有的ODBC,也就是目前至少有3种方式在DuckDB里访问ORACLE数据库了。
之前(20260810)我让AI做过调研:
DuckDB 为什么没有 Oracle 原生 Scanner
调研结论
基于对 DuckDB 官方文档、扩展仓库、路线图以及相关技术资料的调研,以下是完整的分析。
现状概览
DuckDB 目前有三种数据库连接方式:
连接方式 类型 支持的数据库 原生 Scanner(核心扩展) 直连原生协议 PostgreSQL、MySQL、SQLite ODBC 扩展(核心扩展) 通过 ODBC 驱动 Oracle、SQL Server、DB2、PostgreSQL、MySQL 等 社区扩展(427 个) 各种方式 BigQuery、ClickHouse 等,无 Oracle DuckDB 的 ODBC 扩展已将 Oracle 列为 Tier 1 类型覆盖支持(最高级别),文档中直接提供了 Oracle 连接示例:
LOAD odbc; SET VARIABLE conn = odbc_connect('Driver={Oracle driver};DBQ=//127.0.0.1:1521/XE', 'scott', 'tiger'); FROM odbc_query(getvariable('conn'), 'SELECT SYSTIMESTAMP FROM DUAL');但始终没有
oracle_scanner这样的原生扩展。原因有多层。
原因一:Oracle 网络协议是闭源私有的
这是最根本的技术壁垒。
DuckDB 原生 Scanner 的工作原理是直接使用目标数据库的客户端/服务端协议进行高效数据传输。以 Postgres Scanner 为例,Hannes Mühleisen 在官方博客中详细说明了实现方式:
"We use the rarely-used binary transfer mode of the Postgres client-server protocol. This format is quite similar to the on-disk representation of Postgres data files and avoids some of the otherwise expensive to-string and from-string conversions."
PostgreSQL 和 MySQL 的网络协议都是公开文档化的开放协议:
- PostgreSQL 的前端/后端协议有完整的官方文档
- MySQL 的协议同样有公开规范
而 Oracle 使用的是 TNS(Transparent Network Substrate)/ Oracle Net 协议。这是一个专有闭源协议,Oracle 从未公开其完整规范。虽然可以通过 Wireshark 等工具进行抓包分析,但协议极其复杂,包含多版本兼容、安全握手、字符集协商、LOB 处理等大量细节,逆向工程的成本和风险极高。
原因二:Oracle 客户端库的许可证限制
DuckDB 原生 Scanner 的另一个关键设计是静态链接客户端库并随扩展一起分发预编译二进制文件:
Scanner 静态链接的库 库的许可证 可否自由分发 postgres_scannerlibpq PostgreSQL License (BSD-like) 可以 mysql_scannerlibmysqlclient GPL / LGPL 可以(LGPL 部分) sqlite_scanner无需(SQLite 是嵌入式) Public Domain 可以 oracle_scannerOCI / OCCI / ODP.NET Oracle 专有许可 不可以 Oracle 的客户端库(OCI - Oracle Call Interface、Oracle Instant Client)受 Oracle 的专有许可条款约束。关键限制包括:
- 不可自由再分发:Oracle Instant Client 的许可条款要求从 Oracle 官方渠道下载,不能随意打包到第三方项目中分发
- 出口管制条款:Oracle 许可包含美国出口管制限制
- 非开源许可:与 DuckDB 的 MIT 许可证不兼容
这意味着即使有人写出了 Oracle Scanner 的 C++ 代码,也无法像 Postgres Scanner 那样把 OCI 库静态链接进去、打包成
.duckdb_extension文件放到 DuckDB 的扩展仓库供人INSTALL。用户每次都需要自行安装 Oracle Instant Client 并配置环境,这大大削弱了原生 Scanner 相比 ODBC 方案的优势。
原因三:DuckDB 团队的优先级和定位
DuckDB 由非营利组织 DuckDB Foundation 和公司 DuckLabs 维护。查看其路线图(2026 年 6 月更新),完全没有提及 Oracle 支持——既不在"计划功能"中,也不在"未来工作/寻求资助"中。
DuckDB 的定位是嵌入式分析型数据库,核心场景是数据科学、数据分析、湖仓格式(Iceberg/Delta/Lance)等。核心团队的优先级明确:
- 湖仓格式持续改进(Iceberg、Delta、Lance、DuckLake)
- 异步 I/O、PEG 解析器、Quack 协议等核心引擎能力
- 连接开源数据库(PostgreSQL、MySQL、SQLite)
Oracle 作为商业企业级 OLTP 数据库,不是 DuckDB 的核心关注场景。而 ODBC 扩展已经"够用"了——虽然性能不如原生协议直连,但功能上已经覆盖了 Oracle 的类型系统(Tier 1 支持)。
原因四:社区无人填补空白
DuckDB 社区扩展仓库目前有 427 个社区扩展,涵盖从 BigQuery 到 ClickHouse 到 Google Sheets 的各种数据源连接器,但没有任何一个是 Oracle 数据库的原生 Scanner。
这并非因为没人有这个需求(实际上 Oracle 用户群体庞大),而是因为上述技术壁垒和许可证限制使得社区贡献者也面临同样的困境:
- 写原生协议实现 → 需要逆向 TNS 协议,工程量巨大且法律风险高
- 链接 OCI 库 → 无法作为预编译扩展分发,用户体验与 ODBC 方案无异
- 用 ODP.NET 或 ODPI-C 等封装 → 仍然依赖 Oracle 专有运行时
现有方案的局限性
目前通过 ODBC 连接 Oracle 的方案存在以下已知局限:
- 性能开销:ODBC 是通用接口,数据需要经过 ODBC Driver Manager → Oracle ODBC Driver → OCI → TNS 协议的多层转换,相比原生协议直连有明显开销
- 部署复杂:需要安装 unixODBC + Oracle Instant Client + 配置 ODBC DSN,环境依赖重
- 并发限制:DuckDB ODBC 扩展的连接在多线程环境下有局限,同一连接不能在多个线程中并发查询
- 无 ATTACH 语法:不像
ATTACH 'postgres:...' AS db那样可以直接挂载为数据库,需要通过odbc_query()函数式调用- 无谓词下推:Postgres Scanner 支持将过滤条件推到远端执行,ODBC 方案在这方面能力有限
可能的未来方向
虽然目前没有原生 Oracle Scanner,但有几个可能的技术演进方向:
- ADBC(Arrow Database Connectivity):DuckDB 已有 ADBC 支持,社区也有
adbc和adbc_scanner扩展。如果 Oracle 未来提供 ADBC 驱动(目前没有),这可以成为一个比 ODBC 更高效的替代方案- Oracle 开源客户端库:如果 Oracle 像近年开源某些组件一样,将客户端协议库开源(可能性很低),则可以像 Postgres Scanner 一样实现原生扩展
- 社区原生协议实现:理论上社区可以基于对 TNS 协议的逆向分析,纯从零实现一个不依赖 Oracle 库的 Oracle 协议客户端(类似 Python 的
cx_Oracle之前的纯 Python 实现尝试),但工程量和维护成本极高- 商业支持驱动:DuckLabs 提供商业支持和功能优先级服务,如果有企业客户出资赞助,可能会推动开发
总结
DuckDB 没有 Oracle 原生 Scanner,是 技术壁垒(闭源协议)、法律壁垒(许可证限制)和优先级选择(团队聚焦开源生态) 三者共同作用的结果。ODBC 方案虽然不完美,但在现有约束下是唯一可行的通用方案,DuckDB 团队也通过将 Oracle 列为 ODBC 扩展的 Tier 1 支持目标来弥补这一缺憾。
这个调研里,其实已经提前说明了目前已有的三种(ODBC/原生协议/链接OCI库)在 DuckDB 里访问ORACLE的优缺点。
艰难跑通cofin/duckdb-oracle
我先尝试把三个插件都跑通,对于ODBC和oracle_scanner(原生协议) 这两个插件, 按官方文档的用法,用起来都很顺利;
而cofin/duckdb-oracle 用不起来,一堆问题。
首先cofin/duckdb-oracle预编译版本要求 GLIBC 2.38 ,我当前操作系统是2.35,不能直接用,只能再编译一个了;
然后AI在我的ubuntu 22上编译,发现源码有BUG,会编译失败,又临时改了代码:
| 问题 | 位置 | 改动 |
|---|---|---|
| GCC 11 + DuckDB 1.5 unique_ptr 包装类无隐式派生→基类转换,编译失败 | oracle_secret.cpp:180、oracle_scan.cpp:47/209/401、oracle_schema_entry.cpp:212 | return x; → return std::move(x);(与 DuckDB 自身写法一致) |
| 入口点签名过时:DuckDB 1.5.5 以 ext_init_fun_t = void(*)(ExtensionLoader&) 调用,扩展声明的是 DatabaseInstance&,导致类型错认、在垃圾对象上加锁 → LOAD 永久死锁 | oracle_extension.cpp:37 | 改为接收 ExtensionLoader& 并直接调 OracleExtension::Load(loader) |
| ALTER SESSION 漏设 NLS_TIMESTAMP_TZ_FORMAT,带时区时间戳按 Oracle 默认 AMERICAN 格式返回,而解析器只认 ISO | oracle_connection_manager.cpp:314(注释本就写着 "Set NLS date/timestamp format to ISO") | 补上该参数;格式取 '...FF.TZHTZM' 且时区前不能有空格——实测 DuckDB 把 "... +00:00"(带空格)判为"非 UTC"而拒绝,"...+00:00" 才接受 |
最后编译好了终于能用了,但又发现了几个工程设计上的问题:
- RUNPATH 烧死了构建机绝对路径。readelf -d 显示扩展的 RUNPATH 是 /data/qoder-space/duckdb-oracle/oracle_sdk/instantclient_23_26,换机器后这个路径不存在 → libclntsh.so.23.1: cannot open shared object file。三选一:设 LD_LIBRARY_PATH;或把 Instant Client 装到同一路径;或用 patchelf --set-rpath '$ORIGIN' oracle.duckdb_extension 改成相对自身查找(配合把 so 和 instant client 放同级目录,最利于打包)。
- libaio:Ubuntu 24.04 上 apt install libaio1t64 后需 ln -sf /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/x86_64-linux-gnu/libaio.so.1,否则 libclntsh 加载失败。
- glibc 下限:如果目标机是 RHEL 8 / Ubuntu 20.04 这类老 glibc,别想着装兼容库——直接在那台机器(或同版本容器)里重新 make release,产物才能跑。
这意味着换环境很可能要重新编译插件才能用。
原作者恐怕是直接用便宜AI搓的,测试还不到位。
所以我暂时不推荐严格场景下使用这个未被DuckDB官方认可的插件。
至于我这边用AI修复的代码,我就不放出来了,我没精力去保证这个代码质量,本次仅仅只是为了做跑通验证。
大表扫描性能测试
脑海里突然冒出一个想法,让AI测试一下三种方式的性能(我未事先观察这三种方式是否有使用direct path,先测了再看),因为ODBC肯定会慢,但慢多少只有实测才能知道。
测试环境
Oracle 19c EE 19.13(PDB1,UTC,SGA 1.15GB / buffer cache 336MB);客户端 Ubuntu 22.04 4 核 3.9GB;DuckDB 1.5.5,
threads=2。数据用我新建的SYSTEM.BENCH_BIG(501万行 / 1571MB / 201,080 块,非分区,是缓存的 4.7 倍)+ 已有的SYSTEM.TEST01(160MB / 250万行)。每轮前ALTER SYSTEM FLUSH BUFFER_CACHE。所有查询都返回全 Table 列,行数逐轮校验(无!!报警)。一、direct path read:三种方式全部命中
表 方式 物理读 其中 direct table scans (direct read)elapsed BENCH_BIG odbc / oracle_scanner / cofin ~200,42x 200,412 1 7.5s TEST01 同上 ~20,03x 20,035 1 0.7–0.8s
physical reads里 99.99% 是 direct,table scans (direct read)每次 +1,direct path read等待约 200ms。两个推论:
- buffer cache 完全没参与:direct path 绕过缓存,所以"冷/热"两轮耗时一模一样(1571MB 表 7.48s vs 7.42s),重复查询不会变快。
- 这个量级下 direct path 是好事——它让扫描直接贴住存储上限(服务端纯扫描下限 7.39s ≈ 212 MB/s,三种方式 7.45–7.57s,差别 1–2%)。想让 Oracle 复用缓存得显式关掉(会话级
"_serial_direct_read"=NEVER或 event 10949),但这会让首轮变慢。二、性能:分两个口径,结论完全不同
口径 A:聚合成服务端下推(扫描成本 + 连接开销) — 三者基本无差别,都贴住 I/O 天花板:
表 odbc oracle_scanner cofin BENCH_BIG 7.45s 7.57s (1.02x) 7.52s (1.01x) TEST01 0.78s (1.09x) 0.80s (1.11x) 0.72s 口径 B:全量行传输(500万行真的过网络,DuckDB 侧算外层 COUNT) — 差距 5 倍以上,瓶颈转移到连接器:
表 odbc oracle_scanner cofin 默认 cofin 调优后 BENCH_BIG 1571MB 109.7s (46k 行/s) 28.6s (175k 行/s) 110.8s (45k 行/s) 19.4s (258k 行/s) TEST01 160MB 20.4s (122k 行/s) 40.4s (62k 行/s) 4.3s (581k 行/s) 3.5s (711k 行/s) 三、最有价值的一条:cofin 默认参数是性能陷阱
根因是
oracle_array_size=256、oracle_prefetch_rows=1024太小,宽表下往返次数爆炸。两条 SET 提速 5.5 倍:SET oracle_array_size = 2048; -- 上限就是 STANDARD_VECTOR_SIZE,实测封顶 SET oracle_prefetch_rows = 1000000; -- 最大 1,000,000,这才是真正的旋钮扫描后补充的验证:重复两轮 20.17s / 19.16s(稳定);不要设
oracle_prefetch_memory(显式给 256MB 反而退化到 25.3s);threads=4与 2 无差别(并行不是杠杆)。四、给你选型的建议
- 要快就读 cofin + 上面两条 SET:宽表 258k 行/s,比 oracle_scanner 快 1.5 倍、比 odbc 快 5.7 倍;窄表 711k 行/s 也是三者最快。
- odbc 无法通过 DuckDB 设置调优(它的设置里没有任何取数相关项),宽表下稳定垫底,且 TEST01 上还比 cofin 慢 4.75 倍。
- oracle_scanner 表现不稳:宽表第二、窄表最慢(9.4x),不可调;它还有个
oracle_session_pool_size=4。- 若查询本身能下推到 Oracle(聚合/过滤),三者差异可忽略——瓶颈永远先看 SQL 能不能让 Oracle 干活。
说明
- 剩下 81 MB/s 与 212 MB/s 存储上限的差距我没有继续追(已试 preload memory 与线程数,均无效),要更接近上限可能得改扩展的取数循环。
SYSTEM.BENCH_BIG(1.5GB)我留在库里方便你复测,不需要就DROP TABLE system.bench_big;。- 压测脚本都在本地工作区(
create_bench_table.py、bench_read_perf.py、bench_transfer.py、bench_tune_cofin.py、bench_tune_confirm.py),需要的话我推到服务器或提交到项目里。一句话:direct path read 三种方式都在用、且是性能正确选择;真正的性能差距在客户端取数实现,cofin 改两个参数即可从最慢变最快。
结果是,配合好参数的情况下,链接OCI的cofin/duckdb-oracle最快,可惜这个插件目前问题有点多。
总结
再等等看看吧,既然DuckDB社区已经放出基于模拟原生协议的独立oracle_scanner插件了,后面经过一段时间使用、反馈和优化,应该会慢慢变好的。至于cofin/duckdb-oracle这个插件,可以保持一定的关注度,没准后续能优化成生产级,但目前使用要慎重。而ODBC,算了吧,除非你不在乎性能也不在乎架构复杂度。
本次AI工具的使用情况:
- 调研使用了灵犀claw接GLM-5.2(灵犀claw最近转正式版开始订阅收费了,但目前和workbuddy一样,可以每日领取积分)
- 开发测试使用全新qoder(非IDE,可以自定义大模型api端点),接我本机部署的qwen3.8-flash-next模型(从25tok/s到最后满上下文7tok/s,复杂问题的确能打了,就是有点慢)

