clickhousedb SQLAlchemy 方言。同步方言支持 SQLAlchemy 1.4.40 及更高版本 (包括 SQLAlchemy 2.x) ,重点关注 Core 查询、ClickHouse DDL、反射以及简单的 ORM 插入。异步方言要求 SQLAlchemy 2.0.44 或更高版本。
通过包扩展安装 SQLAlchemy 依赖项:
使用 SQLAlchemy 进行连接
使用clickhousedb:// 或 clickhousedb+connect:// 这两种 URL 格式之一创建引擎:
ClickHouse 会话 ID
默认情况下,无论使用同步还是异步方言,每个池化连接都会生成一个独立的 ClickHouse 会话 ID。只要该连接的请求到达同一个 ClickHouse 服务器进程,通过SET 修改的设置以及临时表就会在该连接上持续保留。命名会话状态和同一会话的重叠检查仅在进程内生效。在同一个服务器进程上,针对相同用户和会话 ID 的重叠请求会被立即拒绝并返回服务器错误码 373,而不会进入队列等待。如果您配置了固定的 session_id,请使用 pool_size=1, max_overflow=0,或在请求到达 ClickHouse 之前将访问串行化。在 ClickHouse Cloud 或其他采用负载均衡的部署中,具有相同会话 ID 的请求可能会被路由到不同的服务器,因此请勿将固定的 session_id 用作分布式状态或分布式互斥锁。
异步连接
异步方言要求 SQLAlchemy 2.0.44 或更高版本,并使用 ClickHouse Connect 原生的AsyncClient。请先安装所需的依赖项,然后使用 clickhousedb+async:// URL 创建异步引擎:
AsyncConnection.stream() 会引发 InvalidRequestError。SQLAlchemy 虽然接受 AsyncSession.stream(),但该方言会先缓冲完整结果,然后再返回。对于大型结果集,请使用原生 AsyncClient 的流式方法。在对应的 SQLAlchemy 连接被签出期间,可通过 driver_connection 访问底层原生客户端:
client.close() 或其任何私有生命周期方法。连接并发由 SQLAlchemy 的连接池负责管理。每个池化连接拥有一个原生异步客户端,其 aiohttp 连接器默认限制为总共一个连接、每个主机一个连接。可在 URL 或 connect_args 中设置 connector_limit、connector_limit_per_host 或 keepalive_timeout 来覆盖这些传输设置。启用 pool_pre_ping=True 后,SQLAlchemy 会在签出池化连接时使用 SELECT 1 检查被复用的连接。
目前,异步 SQLAlchemy 的 executemany 插入会为每个参数集单独发送一个 HTTP 请求,而不会使用驱动程序的 Native 批量插入协议。此方式仅适用于小批量数据。对于大批量数据,请使用上文介绍的由连接池管理的 driver_connection 访问模式,并在将 SQLAlchemy 连接归还到连接池之前 await client.insert()。由于异步 executemany 使用查询参数绑定,不带时区的 datetime 值遵循 naive_datetime_binding,而非同步 Native executemany 所使用的 naive_datetime_insert 设置。带类型的 SQLAlchemy DateTime64 绑定无论使用客户端参数还是服务器端参数,均会保留小数秒。对于传递给 exec_driver_sql() 的无类型 %s 或 %(name)s 参数,不带时区的 datetime 值仍沿用默认的整秒格式。如需明确的时区行为,请使用带时区信息的值。如需 Native 批量语义,请使用 client.insert()。
请在使用异步引擎的事件循环中创建并释放该引擎。在关闭时,以及在其他事件循环中使用该引擎之前,请先归还所有已签出的连接,然后 await engine.dispose()。如果引擎所属的事件循环已经关闭,请在复用前于当前循环中 await engine.dispose()。如果清理工作在所属循环关闭之后才开始,aiohttp 仍可能报告存在未关闭的传输,因此请尽可能在转移之前完成释放。在事件循环之间迁移池化异步引擎时,pool_pre_ping=True 不能替代释放操作。若要在多个事件循环之间共享同一个引擎,且不保留绑定到特定循环的连接,请配置 poolclass=NullPool。如果执行释放时仍有连接处于签出状态,方言会在该连接被归还或被垃圾回收时将其关闭。请勿在同步代码中调用 engine.sync_engine.dispose(),因为 SQLAlchemy 在此无法 await 异步连接的清理,可能只会记录错误日志,而不会关闭池化的传输。
URL 查询参数可以包含 ClickHouse 设置、ClickHouse Connect 客户端选项 (例如 compression、query_limit 和各类超时) ,或 HTTP/TLS 选项 (例如 ca_cert) 。必要时,可为 ClickHouse 设置添加 ch_ 前缀,强制将其视为服务器设置,例如 ch_http_max_field_name_size=99999。
有关可用的客户端选项,请参阅连接参数和设置。
DDL、元数据检查等同步 SQLAlchemy 辅助操作需通过 AsyncConnection.run_sync() 运行:
每个查询的设置
通过 SQLAlchemy 的执行选项传递 ClickHouse 设置。可以在引擎、连接或语句上设置这些参数。对于相同的键,语句上的值优先于连接或引擎上的值。按查询设置读取格式
通过 SQLAlchemy 的执行选项query_formats,可为引擎、连接或语句设置 ClickHouse 读取格式。语句级格式会优先应用,并覆盖匹配的连接级或引擎级键及通配符。
错误处理
驱动通过 SQLAlchemy 连接引发的错误,使用的是从clickhouse_connect.dbapi 导出的 DB-API 类。这些类与 clickhouse_connect.driver.exceptions 中的对应类是同一个类对象,因此 SQLAlchemy 会将其包装为相应的 sqlalchemy.exc.DBAPIError 子类。StreamFailureError 属于 OperationalError,会被包装为 sqlalchemy.exc.OperationalError。
如果调用方的取消操作可能会中断显式调用的 AsyncConnection.invalidate(),请在自行管理的任务中执行失效操作,并等待该任务完成后再传播取消。这样 SQLAlchemy 便能完成连接记录的簿记工作:
await connection.invalidate() 被取消,且 connection.invalidated 仍为 false,请再次 await connection.invalidate() 以完成清理,之后再使用或关闭该连接。
服务器端参数
SQLAlchemy 通常会在客户端渲染参数。创建引擎时,可选择启用 ClickHouse 服务器端参数:create_async_engine() 中使用相同的 server_side_params=True 参数。
在此模式下,每个绑定值都必须具有与 ClickHouse 兼容的 SQLAlchemy 类型。受支持的 IN 列表会变为带类型的 ClickHouse Array 参数。如果编译器无法推导出兼容的类型,或无法安全地处理绑定值,就会引发 CompileError。
绑定名称必须是 ClickHouse ASCII BareWord 名称。以 $ 开头和结尾的名称会被拒绝,因为核心驱动程序将其保留用于原始二进制查询参数。
Core 查询
该方言支持 SQLAlchemy CoreSELECT 查询,可使用 JOIN、过滤器、排序、LIMIT 和 OFFSET 以及 DISTINCT 和复合 SELECT。
SQLAlchemy union()、intersect() 和 except_() 会编译为 ClickHouse UNION DISTINCT、INTERSECT DISTINCT 和 EXCEPT DISTINCT。对应的 union_all()、intersect_all() 和 except_all() 会编译为相应的 ALL 运算符。此显式映射可保留 SQLAlchemy 的重复项语义,而不受 ClickHouse 集合操作默认设置的影响。
DELETE,并且需要显式 WHERE 子句:
字面量渲染
当 SQLAlchemy 通过literal_binds 或 literal_execute 内联绑定值时,方言会针对通用 String 类型和 ClickHouse 类型采用 ClickHouse 的引用规则。这同样适用于 TypeDecorator 包装器以及 with_variant() 选择。即使其他绑定参数保持不变,String 值中的百分号和反斜杠仍会保留。
对于使用 ClickHouse DateTime64 SQLAlchemy 类型的 Python datetime 值,其微秒部分会在客户端参数和内联字面量中得到保留,包括 Nullable 值以及嵌套在数组和元组中的值。ClickHouse 会按所声明的精度进行处理。Python datetime 最多提供六位小数。普通 DateTime 值仍按整秒格式输出。对于 text() 语句,请通过 bindparam("ts", type_=DateTime64(6)) 显式指定类型,以保留小数秒。
SQLAlchemy 列类型必须与服务器 schema 保持一致。若在服务器端类型为 DateTime 的列上声明 DateTime64,渲染结果会带有小数秒,并可能在 insert 时以及 IN 比较中引发转换出错。
在 SQLAlchemy 2.x 中,若通用 sqlalchemy.ARRAY 类型包含 ClickHouse Tuple 元素,其内联字面量需要设置 dimensions=1 (嵌套数组则需设置相应更高的维数) ,以便 SQLAlchemy 将每个元组视为单个元素。SQLAlchemy 1.4 不支持通用 ARRAY 类型的内联字面量。
如果重复使用某个命名的 datetime 参数,则该参数每次出现时都需要兼容的 DateTime64 绑定类型,才能保留小数部分。只要有一处未指定类型或类型冲突,就会保持整秒格式。请在每个 bindparam 上设置 type_=DateTime64(6),或改用具有相应类型的不同参数名。
JSON type hints
使用typed_paths 映射声明带类型的 JSON 路径。路径类型可以是 ClickHouse SQLAlchemy 类型类、已配置的类型实例,或 ClickHouse 类型名称字符串。类型名称字符串支持没有 SQLAlchemy 构造函数的类型 (例如 Dynamic) ,也同样适用于复杂的已配置类型表达式,并能在命名 Tuple 中保留字段名称。
类型名称字符串中可以包含已配置的嵌套 JSON 类型,例如 Array(JSON(`child` UInt32))。在这些字符串中,可识别的 ClickHouse 类型名称不区分大小写,输出时会统一采用其规范大小写形式。字符串必须且只能包含一个完整的类型表达式,尾随文本以及格式错误的嵌套 JSON 参数都会被拒绝。
空的 Tuple() 不能用作 JSON 类型化路径,因为 ClickHouse 无法通过 JSON 列的 Native 格式对其进行序列化。核心驱动支持在查询列和插入列的任意位置使用 Tuple(),包括嵌套在位置元组或命名元组中、位于 Array 内,以及在服务端启用的情况下以 Nullable(Tuple()) 的形式使用。
typed_paths 的简写形式,例如 JSON(user_id=UInt32)。若路径包含点号、空格、反引号、%2E 编码的点号,或名称与构造函数选项重名,请使用 typed_paths。名为 SKIP 的类型化路径可通过该映射来指定。typed_paths 中的键和 skip_paths 中的值均为解码后的名称。开头或尾随的反引号和双引号会被视为路径中的字面字符,而非预先应用的 SQL 引用。而在原始类型字符串内部,反引号和双引号属于 ClickHouse 的标识符语法。
最多可配置 1000 个类型化路径。max_dynamic_paths 接受 0 至 10000,max_dynamic_types 接受 0 至 254。这些取值范围同样适用于原始嵌套 JSON 类型字符串内部。若显式指定的值与服务端默认值 1024 和 32 相同,则不会出现在生成的 DDL 中。普通的 skip 路径会被去重。由于 ClickHouse 使用 RE2 语法,Python 不会校验正则表达式字符串。重复的正则表达式会被保留。
普通 skip 路径的名称不能正好是 REGEXP,因为 ClickHouse 将该标记保留给 SKIP REGEXP 使用。而 REGEXP_foo 这类名称仍然有效。在原始 JSON 类型字符串中,普通 SKIP 的操作数必须是一个 ClickHouse 标识符,或以点号分隔的复合标识符。未加引号的复合标识符不能以 REGEXP 开头;当第一个组成部分本身就是路径数据时,请为其加上引号。SKIP REGEXP 必须带有一个单引号字符串字面量。当标识符各部分包含空格或标点符号时,请使用反引号或双引号引用。原始 JSON 类型提示支持 Variant(...);独立的 Variant 没有公开的 SQLAlchemy 构造函数。Variant 成员会按照 ClickHouse 所用的同一套规范名称进行排序和去重。
构造函数对参数的排序与 ClickHouse 返回的规范形式一致。反射得到的类型、SQLAlchemy 类型副本以及 Alembic 自动生成均会保留该配置。
JSON 子列
对于声明为或映射为 ClickHouseJSON 的列,请使用方括号逐段选择由存储支持的子列路径:
payload["severity"] 会被编译为 ClickHouse 的点分标识符语法。每个部分都会分别加引号,例如 `events`.`payload`.`severity`。它读取 ClickHouse 存储的 JSON 子列,不会调用 getSubcolumn。对路径中的每个分段依次使用 [] 或 .subcolumn()。每个分段都必须是非空字符串。
向 .subcolumn() 传入 type_ 会将点分路径包装为 SQL CAST,并将该类型赋予 SQLAlchemy 表达式。未传入 type_ 时,.subcolumn("segment") 的行为与 ["segment"] 相同。
未指定类型的路径具有 ClickHouse 的 Dynamic 类型。ClickHouse 不允许在 ORDER BY 或 GROUP BY 中直接使用 Dynamic 值。当在这些位置使用子列时,请传入 type_。
对于静态类型代码,请从 clickhouse_connect.cc_sqlalchemy 导入 json_subcolumn。该辅助函数同样每次只接受一个分段,并保留 type_ 指定的 Python 结果类型:
request_id 识别为 ColumnElement[int]。
每个片段都会分别加引号,包括包含空格或反引号的名称。对于 ClickHouse JSON 路径处理,反引号不会使点号成为字面量。启用 json_type_escape_dots_in_keys 后,键名中的字面点号应使用 ClickHouse 的 %2E 编码。对于名为 a.b 的键,应通过 payload["a%2Eb"] 而非 payload["a.b"] 访问。
ClickHouse 查询扩展
从clickhouse_connect.cc_sqlalchemy 导入 select,即可向静态类型检查器公开带类型的 ClickHouse 方法。标准的 sqlalchemy.select 在运行时也提供这些方法。
Select 方法如下:
SQLAlchemy’s
Select.with_hint() 是表提示 API。ClickHouse 方言不会渲染表提示。适用的通配符提示或 clickhousedb 提示会发出 SAWarning,并保持生成的 SQL 不变。对于这些 ClickHouse 子句,请使用 final()、sample()、prewhere() 或 limit_by()。
Select.with_statement_hint() 是原始尾部指令 API。它会将提供的文本附加到 SELECT 末尾,而不进行 ClickHouse 特有的验证。它仍可用于受信任的静态 SQL,例如 SETTINGS max_threads=1:
GLOBAL ANY LEFT JOIN 可以链式调用,无需嵌套自定义 FromClause:
Lambda 构造:
values() 构造会被编译为 ClickHouse 的 VALUES 表函数语法,包括在公共表表达式中使用时。CTE 形式需要 SQLAlchemy 2.0.42 或更高版本,其中新增了 Values.cte()。
Materialized CTE
默认情况下,ClickHouse 会内联公共表表达式,因此被多次引用的 CTE 的主体会针对每次引用执行一次。向.cte() 传入 materialized=True,即可生成 WITH <name> AS MATERIALIZED (...),使主体只计算一次:
enable_materialized_cte=1 且启用 analyzer 时,服务器 才会 materialize CTE。如每个查询的设置所示,可在语句、连接 或 引擎 上设置 enable_materialized_cte。在所有支持此功能的服务器 上,analyzer 默认启用,因此显式设置 enable_analyzer=1 是一种防御性措施。enable_materialized_cte 是一项 Experimental ClickHouse 设置。使用 enable_materialized_cte=0 或 enable_analyzer=0 时,查询仍会成功执行并返回相同的行。ClickHouse 会静默忽略 MATERIALIZED 并重新内联 CTE,因此漏设该选项只会影响性能,不会报错。Materialized CTEs 需要 ClickHouse 26.3 或更高版本。旧版服务器 会将该关键字视为语法错误而拒绝。
对于使用标准 sqlalchemy.select 构建的语句,请改用模块级的 cte()。它将语句 作为第一个 argument,其他行为与 Select.cte() 一致:
recursive=True 和 materialized=True,SQLAlchemy helpers 将引发 ValueError。
DDL 与反射
ClickHouse Connect 提供 ClickHouse 数据类型、表引擎、字典结构、数据库 DDL 和表反射功能。 独立的Variant 列通过 SQLAlchemy 内部类型进行反射,Alembic 自动生成会保留其规范的原始类型名称,不会反复产生类型变更。Geometry 和 MultiPoint 列则反射为公开的 SQLAlchemy 类型。
DEFAULT 表达式带上 server_default,并在存在时包含方言特有的属性,例如 clickhouse_codec、clickhouse_ttl、clickhouse_materialized 和 clickhouse_alias。
DEFAULT、MATERIALIZED、ALIAS 和 TTL 子句中的 String 值使用 ClickHouse 字符串转义。相同的转义规则也适用于表、字典和列注释,包括 Alembic 生成的注释。
MergeTree 键参数 (如 order_by、partition_by、primary_key、sample_by 和 ttl) 既接受 SQLAlchemy 列和 SQL 表达式,也接受普通字符串。
Memory()、Log()、StripeLog()、TinyLog()、Null() 和 Set() 支持无参数调用,且在经过 Alembic 自动生成后能够保持往返一致。原有的字典参数形式仍然受支持。如需提供引擎设置,请使用 settings={...}。
SummingMergeTree 和 ReplicatedSummingMergeTree 支持一个可选的 columns 参数,该参数只能以关键字形式传入。原有位置参数的含义保持不变,因此 SummingMergeTree("id") 仍会设置 ORDER BY id。
"delta" 或 "(delta, n_tx)"。服务器要求这些列以标识符形式指定。省略 columns 时,由 ClickHouse 自行选择要求和的列。反射和 Alembic 自动生成会保留显式指定的列列表。
插入和基本 ORM 用法
支持 Core 插入以及简单的 ORM 模型。对于同步方言,在兼容的批量数据路径中优先使用 Core executemany 插入。对于异步批量插入,请使用异步连接中介绍的原生AsyncClient.insert() 路径。
executemany 插入会通过一次 Native 批量插入完成。异步 executemany 则会为每个参数集各发送一个请求,详见 异步连接。原始 SQL,以及包含表达式或其他无法安全路由的语义的插入,会保留原始 SQL,并针对每个参数集各执行一次。如果后续某个参数集执行失败,之前的参数集已写入的行仍会保持已提交状态。
显式多行 insert(events).values([...]) 语句支持字典形式的行、按表列顺序排列的元组,以及逐行指定的 SQL 表达式。Pandas to_sql(method="multi") 采用的就是这种形式。它能够插入这些行,但会返回 0,因为文本形式的 INSERT 语句通过 DB-API 游标报告的行数始终为 0。SQLAlchemy 根据第一行确定列列表。后续行中多出的字典键,以及超出所选列列表范围的元组值,都会被忽略。如果后续某行缺少所选列的值,编译将会失败。请确保每一行都包含相同的列。
在 ClickHouse 26.4 及更高版本的默认 HTTP 表单限制下,server_side_params=True 仅适用于小型显式批次,即绑定值少于约 1000 个,同时还需为其他字段预留余量。可以通过服务器配置提高这一上限。对于同步方言下的大型普通批次,请将行作为 execute() 的第二个参数传入,以便驱动程序走其 Native 批量插入路径。对于异步批量数据,请 await 原生的 AsyncClient.insert() 方法。
Alembic 迁移
ClickHouse Connect 提供了适用于 ClickHouse schema 迁移的 Alembic 集成。使用以下命令安装:alembic.ini 使用 script_location = %(here)s/alembic。如果迁移目录名为 alembic,请保留该设置;否则,请将其改为传递给 alembic init 的目录。将 alembic/env.py 替换为仓库中提供的异步 Alembic env.py 示例,然后在 alembic.ini 中设置 sqlalchemy.url。
在 Alembic 的 env.py 中导入 clickhouse_connect.cc_sqlalchemy.alembic,以注册方言集成。自动生成支持常见的表结构变更,包括创建和删除表、添加/修改/删除列、默认值以及注释。表和列的重命名请使用手动操作。在应用每个生成的迁移之前,都应先进行审查。
Alembic 的迁移函数仍然是同步的。异步环境会创建 AsyncEngine,打开 AsyncConnection,并将同步迁移函数传给 await connection.run_sync(...)。离线迁移则直接调用 context.configure(url=..., literal_binds=True, dialect_opts={"paramstyle": "named"}),不会创建引擎。仓库中提供的异步 Alembic env.py 示例同时涵盖这两种路径,并通过 Alembic 标准的 sqlalchemy.url 配置读取连接 URL。该示例保留了完整示例中的 ClickHouse Alembic 钩子和选项,包括 include_object、make_include_name(...)、clickhouse_writer 和 version_table。请勿使用 engine.sync_engine 来运行异步迁移或释放其资源。
ClickHouse 特有的 op.* 辅助方法涵盖:
- 数据跳过索引,包括添加、物化和删除操作。
- 投影,包括添加、物化和删除操作。
- MergeTree 表设置的修改与重置。
- materialized view 的创建与删除。
- 字典的创建、删除和重新加载。
Index、Column(index=True)、op.create_index 和 op.drop_index 都会被拒绝,以避免生成不完整或不正确的 DDL。请使用 op.add_clickhouse_index 和 op.drop_clickhouse_index。
请参阅完整的 Alembic 示例。从 clickhouse-sqlalchemy 迁移的用户还应阅读迁移指南。
范围和限制
- ClickHouse 不通过此 HTTP 方言提供传统事务。
engine.begin()和Session.commit()用于组织 Python 端的工作,但 commit 和 rollback 在服务器端都是空操作。 - 该方言未实现
UPDATE、两阶段事务、序列、RETURNING以及高级隔离级别。需要执行服务器端变更时,请显式使用 ClickHouse SQL。 Column(..., primary_key=True)提供的是 SQLAlchemy 的对象标识。它不会创建服务器端的唯一性约束。请通过表引擎定义排序和可选的主键表达式。- 传统的外键、唯一约束以及标准索引元数据不可用,因为 ClickHouse 不会强制执行这些约束。
- ORM 关系管理、工作单元更新、级联,以及立即或延迟的关系加载,不属于受支持的 ORM 范围。