Civitai ClickHouse 即席查询实战指南:query.mjs 用法、安全防护与副本集群分析
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
导读
本文围绕 Civitai 仓库中.claude/skills/clickhouse-query这个 ClickHouse 查询技能展开,讲解如何通过内置的query.mjs脚本对 ClickHouse 执行即席(ad-hoc)分析查询,用于指标分析、事件数据探索、查询性能验证与线上排障。读完本文,你将掌握:query.mjs的全部命令行选项与默认行为、默认只读与--writable写权限的防护机制、views/modelEvents等核心事件表的结构与用途,以及在生产副本集群上必须使用clusterAllReplicas()查询系统表的正确姿势。
Skill 定位:什么是 clickhouse-query
clickhouse-query是仓库内置的一个 Claude Skill,其声明(frontmatter)位于 .claude/skills/clickhouse-query/SKILL.md,用途定义如下:
Run ClickHouse queries for analytics, metrics analysis, and event data exploration. Use when you need to query ClickHouse directly, analyze metrics, check event tracking data, or test query performance. Read-only by default.
也就是说,它面向三类典型场景:
- 分析类查询(analytics):统计页面浏览、模型事件、用户行为等业务指标;
- 指标与事件数据探索(metrics analysis / event data exploration):核查某类事件是否真的被记录、字段取值分布如何;
- 查询性能验证(test query performance):借助
--explain查看执行计划,借助超时参数避免长查询失控。
该 Skill 的关键特征是默认只读(Read-only by default),任何写操作都必须显式携带--writable并征得用户许可——这是贯穿整个脚本设计的安全底线。
快速开始:运行你的第一条查询
Skill 通过一个独立的 Node 脚本执行查询,不需要额外安装 CLI 工具。脚本位于 .claude/skills/clickhouse-query/query.mjs,直接以node运行并把 SQL 作为参数传入:
node .claude/skills/clickhouse-query/query.mjs "SELECT count() FROM views"该命令统计views表中的总行数(即页面/实体浏览量事件总量)。脚本内部流程(对应 query.mjs 的main())为:
- 从环境变量读取连接配置并创建客户端(
createClient); - 若传入
--explain,则把 SQL 包装为EXPLAIN <query>后下发; - 以
JSONEachRow格式拉取结果; - 按普通查询 / EXPLAIN / JSON 三种模式分别格式化输出,并统计耗时。
非--quiet模式下,脚本会先在 stderr 打印Connected to ClickHouse (timeout: 30s),随后输出Columns:列名列表与每一行结果(JSONEachRow单行对象),最后打印N row(s) in Xms。
命令行选项详解
SKILL.md 给出了完整的选项表,下表在保留原意的基础上补充了脚本源码中的实现细节(见 query.mjs 的参数解析部分):
| Flag | 说明 | 源码实现要点 |
|---|---|---|
--explain | 显示查询执行计划 | 把 SQL 前缀为EXPLAIN再发送;输出时逐行打印row.explain字段 |
--writable | 允许写操作(需用户许可) | 未携带时,脚本会对以INSERT/DELETE/DROP/ALTER/TRUNCATE/CREATE/RENAME/OPTIMIZE开头的语句直接报错退出 |
--timeout <s>/-t | 查询超时(秒),默认 30 | 同时作用于 HTTPrequest_timeout(毫秒)与 ClickHouse 服务端设置max_execution_time |
--file <path>/-f | 从文件读取 SQL | 以process.cwd()为基准解析路径并读取文件内容作为查询 |
--json | 以 JSON 输出结果 | 输出{ rows, rowCount, elapsed }结构,便于程序化处理 |
--quiet/-q | 最小化输出,只打印结果 | 跳过连接提示、列名行、分隔线与耗时统计 |
值得注意的两个细节:
- 超时是双层的:脚本创建客户端时把
timeoutSeconds * 1000传给request_timeout(客户端等待上限),同时写入clickhouse_settings.max_execution_time(服务端执行上限),见 query.mjs。任一先到都会终止查询,有效防止失控查询占用资源。 - 超时错误有专门提示:当错误消息包含
timeout或TIMEOUT时,脚本会明确提示Query timed out after N seconds并建议用--timeout调大,见 query.mjs。
典型查询示例
SKILL.md 提供了 6 个开箱即用的示例,全部可以直接复制运行:
# 统计表中的行数 node .claude/skills/clickhouse-query/query.mjs "SELECT count() FROM views" # 带过滤条件的查询 node .claude/skills/clickhouse-query/query.mjs "SELECT * FROM modelEvents WHERE modelId = 123 LIMIT 10" # 查看查询执行计划 node .claude/skills/clickhouse-query/query.mjs --explain "SELECT * FROM views WHERE userId = 1" # 覆盖默认 30s 超时,执行复杂聚合 node .claude/skills/clickhouse-query/query.mjs --timeout 60 "SELECT ... (complex aggregation)" # 从文件读取查询 node .claude/skills/clickhouse-query/query.mjs -f my-query.sql # JSON 输出,方便后续处理 node .claude/skills/clickhouse-query/query.mjs --json "SELECT type, count() FROM modelEvents GROUP BY type"安全特性:默认只读与写权限控制
SKILL.md 明确列出三条安全规则:
- 默认只读:未加
--writable时,脚本拦截INSERT/ALTER/DROP等写操作; - 30 秒超时:默认限制查询时长,防止失控查询,可通过
--timeout覆盖; - 显式许可:使用
--writable前,必须向用户征得许可。
这些规则在源码中有三重落地的实现(query.mjs):
- 环境校验:启动时要求
CLICKHOUSE_HOST与CLICKHOUSE_USERNAME必须存在,否则报错退出; - 语句拦截:未启用
--writable时,脚本将查询大写化后检查前缀,命中INSERT、DELETE、DROP、ALTER、TRUNCATE、CREATE、RENAME、OPTIMIZE任一即打印Error: Write operation detected (...) Use --writable flag to confirm.并以非零码退出; - 连接配置兜底:客户端创建时同时设置了服务端
max_execution_time,即使 SQL 本身无法被前缀匹配到(例如以注释开头),超时保护依然生效。
补充说明:该拦截只针对以写关键字开头的语句,属于快速防线而非完整 SQL 解析器;因此切勿依赖它做权限隔离,真正的写权限仍应由 ClickHouse 账号本身控制。
何时使用 --writable
只有在以下场景才应携带--writable:
- 用户明确请求写入访问;
- 需要插入测试数据(如验证某个新事件类型是否正常入库);
- 执行维护操作(如
OPTIMIZE合并分区)。
SKILL.md 对此给出了一条反复强调的警告:每次使用--writable前,都必须先询问用户获得许可(Always ask the user for permission before running with--writable)。
这一约束与仓库中 ClickHouse 事件写入链路的运维经验直接相关:仓库的迁移文档 src/server/clickhouse/migrations/README.md 记录过多次"迁移正确、却因 Tracker 未重启而静默零行"的事故(例如Announcement_Click类型上线 2.5 天收集 0 行),说明写入侧的任何变更都需要用真实事件验证,而不是依赖 DDL 或"看起来正常的 0"。因此在使用--writable插入测试数据验证新事件类型时,请参照该文档的验证流程:触发真实事件 → 检查 Tracker 日志poison=0 dlq=0→ 再查询确认行可读。
常用表与事件模型
SKILL.md 整理了 8 张分析常用表:
| 表名 | 用途 |
|---|---|
views | 页面/实体浏览事件 |
modelEvents | 模型创建/发布/更新事件 |
modelVersionEvents | 模型版本事件,含下载 |
userActivities | 用户注册、登录、订阅事件 |
images | 图片上传/删除事件 |
reactions | 点赞/点踩事件 |
reports | 内容举报事件 |
entityMetricEvents | 聚合指标事件 |
这 8 张表均能在本地开发容器的建表脚本 containers/clickhouse/docker-init/init.sh 中找到对应的CREATE TABLE定义,可作为理解字段语义的一手依据。以最常用的两张为例:
views(见 init.sh):type为Enum8,取值覆盖ProfileView/ImageView/PostView/ModelView/ModelVersionView/ArticleView/CollectionView/BountyView等;除常规的userId、entityId、ip、userAgent外,还包含ads(Member/Served/Blocked/Off 四态)、nsfw、browsingLevel、isMember等业务字段,按toYYYYMM(createdDate)按月分区。modelEvents(见 init.sh):type枚举Create/Publish/Update/Unpublish/Archive/Takedown/Delete/PermanentDelete,带nsfw布尔字段与deviceId。
此外,init.sh 中还定义了仓库事件体系中的其他表,如pageViews、impressions、buzzEvents(ReplacingMergeTree)、daily_views(SummingMergeTree 汇总表)等,可用于更深入的指标分析。
副本集群查询:clusterAllReplicas() 详解
SKILL.md 强调了一个生产环境的硬性规则:
Production uses a ClickHouse replica cluster. When querying system tables (logs, metrics, etc.), you must use
clusterAllReplicas()to get data from all nodes.
原因在于:ClickHouse 的system系统表(query_log、text_log、metric_log等)是每节点本地存储的。直接查询只会看到当前连接节点上的数据,而在副本集群中,各节点负载不均衡时,同一时刻的查询日志分散在不同节点上,单节点视角必然失真。
错误与正确写法对比
-- WRONG: 只查询当前连接节点的数据 SELECT * FROM system.query_log WHERE event_time > now() - INTERVAL 1 HOUR -- CORRECT: 查询集群内所有副本的数据 SELECT * FROM clusterAllReplicas(default, system.query_log) WHERE event_time > now() - INTERVAL 1 HOURclusterAllReplicas(default, system.query_log)中的default是集群名,第二个参数是系统表名。查询时会带上hostname()列即可区分数据来自哪个节点。
何时使用 clusterAllReplicas()
SKILL.md 给出了一张简洁的决策表:
| 场景 | 使用的函数 |
|---|---|
系统表(query_log、text_log等) | clusterAllReplicas(default, system.table_name) |
应用表(views、modelEvents等) | 直接查询(本身已是分布式表) |
| 跨多个系统表搜索 | clusterAllReplicas(default, merge('system', '^pattern*')) |
关键区分:业务事件表(如views、modelEvents)在集群中是分布式表,直接查询即可拿到全量数据;只有system.*这类本地系统表才必须走clusterAllReplicas()。这也是 SKILL.md 中"当查询系统表时必须使用 clusterAllReplicas()"这条规则的适用范围边界。
常用系统表排查查询
SKILL.md 提供了 4 个生产排障级示例,完整继承如下:
-- 查看最近 5 分钟所有节点上的查询 SELECT hostname(), event_time, query_duration_ms, formatReadableSize(memory_usage) AS memory, query FROM clusterAllReplicas(default, system.query_log) WHERE type = 'QueryFinish' AND event_time > now() - INTERVAL 5 MINUTE ORDER BY event_time DESC LIMIT 20 -- 按内存占用找出 24 小时内的"昂贵查询" SELECT count() as query_count, user, sum(memory_usage) AS total_memory, normalized_query_hash FROM clusterAllReplicas(default, system.query_log) WHERE event_time > now() - INTERVAL 1 DAY AND query_kind = 'Select' AND type = 'QueryFinish' GROUP BY normalized_query_hash, user ORDER BY total_memory DESC LIMIT 10 -- 按模式搜索查询日志(跨多个 query_log 分区表) SELECT event_time, query_id, query, type FROM clusterAllReplicas(default, merge('system', '^query_log*')) WHERE query ILIKE '%some_table%' AND event_time > now() - INTERVAL 5 MINUTE -- 跨节点调试某条具体查询(按 query_id 追溯) SELECT hostname(), message FROM clusterAllReplicas(default, system.text_log) WHERE query_id = 'your-query-id-here' ORDER BY event_time_microseconds ASC其中第二个示例用normalized_query_hash聚合"同形查询"(模板化 SQL 归一化后的哈希),能有效把同一类慢查询聚合到一起;formatReadableSize()则把原始字节数转成人类可读的单位。第四个示例是定位"某条线上查询为何失败/超时"的标准手段——先在query_log里按时间与特征找到query_id,再进text_log按微秒时间戳串起该查询在各节点上的日志行。
ClickHouse SQL 实用技巧
SKILL.md 汇总了 4 条高频实战技巧,可直接套用:
-- 用 count() 而不是 COUNT(*) SELECT count() FROM views -- 用 toDate() 做日期过滤(利用物化列 / 分区裁剪) SELECT * FROM views WHERE toDate(time) = today() -- 最近 7 天 SELECT * FROM modelEvents WHERE time > now() - INTERVAL 7 DAY -- 聚合排序 SELECT type, count() as cnt FROM modelEvents GROUP BY type ORDER BY cnt DESC结合 init.sh 的建表定义可以看到,views、modelEvents等多数表都带有createdDate Date materialized toDate(time)物化列,并且按toYYYYMM(createdDate)或toYear(createdDate)分区——因此用toDate(time)/toYYYYMM(createdDate)过滤能最大化利用分区裁剪,这也是技巧 2 推荐的直接原因。
环境变量与连接配置
query.mjs依赖三个环境变量连接 ClickHouse(query.mjs):
| 变量 | 必填 | 说明 |
|---|---|---|
CLICKHOUSE_HOST | 是 | 服务地址 |
CLICKHOUSE_USERNAME | 是 | 用户名 |
CLICKHOUSE_PASSWORD | 否(prod 必填) | 密码 |
脚本内置了一个零依赖的.env解析器(query.mjs),加载顺序为:
- 优先读取 Skill 目录下的
.claude/skills/clickhouse-query/.env; - 回退读取仓库根目录的
.env; - 两者都不存在时打印警告
Warning: Could not load any .env file(但不会立即退出,真正的退出发生在连接参数缺失校验时)。
加载规则是"已存在的环境变量不被覆盖"(if (!process.env[key])),即外部 shell 环境优先级最高。注意:连接凭据绝不能通过--writable绕过,写权限只受--writable标志与 ClickHouse 账号本身权限双重约束。
仓库主应用侧的连接配置与此同源:应用封装的 ClickHouse 客户端 shim 位于 src/server/clickhouse/client.ts,它引用包 packages/civitai-clickhouse 的createClickhouseClient,并做了IS_BUILD构建期跳过、生产直连 / 开发走 HMR 单例(globalClickhouse)的处理;环境变量 schema 由 packages/civitai-clickhouse/src/env.ts 用 zod 定义——生产环境三变量全部必填,开发环境可选。query.mjs使用的正是同一套变量名约定。
结合仓库源码:从查询到事件写入链路
掌握查询只是第一步,理解数据从哪来,查询才真正可解释。仓库的事件写入链路(Tracker)位于 src/server/clickhouse/tracker.ts,它与查询侧共享同一批表。几个与查询直接相关的源码事实:
- 异步插入:包级客户端默认开启
async_insert: 1与wait_for_async_insert: 0(见 packages/civitai-clickhouse/src/client.ts),写入是异步批量落盘的。因此刚触发的事件可能不会立刻出现在SELECT结果中——排查"事件缺失"时应留出落盘窗口。 - 列宽饱和哨兵:Tracker 对
pageViews.duration(UInt32)、windowWidth/windowHeight(Int16)做过客户端钳制,超过上限的值会被饱和为哨兵值(如duration = 4294967295),并且 tracker.ts 明确警告:统计这些列时务必排除哨兵值(如WHERE duration < 4294967295),否则均值/分位数会被少数极端行严重拉偏。 - 枚举漂移防护:仓库用测试钉住迁移与 Tracker 枚举的一致性,如 src/server/clickhouse/tests/tracker-enum-drift.test.ts 与 src/server/clickhouse/tests/action-type-enum-drift.test.ts。当你用
--writable插入测试数据验证新枚举类型时,应先确认对应迁移文件是否带POST-APPLY: restart civitai-clickhouse-tracker ...标记(见 src/server/clickhouse/migrations/README.md),否则新类型可能在 Tracker 侧被客户端静默拒绝。
这些背景解释了为什么 SKILL.md 反复强调"用真实事件验证":查询端看到干净的 0,可能与"事件真的没发生"完全无法区分。
结语
clickhouse-querySkill 把"安全地即席查询 ClickHouse"压缩成了一个可复制的命令:默认只读、默认 30s 超时、显式写许可,配合--explain、--json、--file等选项覆盖了指标分析、事件探索与性能验证的全部日常场景。在副本集群环境下,牢记clusterAllReplicas()是系统表查询的正确入口;结合仓库内的建表脚本与 Tracker 源码,你能从"会跑查询"进阶到"理解数据、查得对、排得掉"。
后续想深入,可继续阅读:SKILL.md 原始文档、query.mjs 完整实现、本地容器建表脚本、ClickHouse 迁移与运维手册、Tracker 事件写入实现 以及 ClickHouse 客户端封装包。
【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考