Usage Metering 1千万/天:ClickHouse vs Postgres 实测

Usage Metering 数据库选型实测:1千万条/天,30天,诚实基准测试。p99延迟、成本对比、完整 schema。TanStack Ship 开箱即用。

Huifer
Huifer
May 7, 202611 min read
其他语言:Deutsch · English

本文由 Huifer 撰写,Solo 开发者,TanStack Ship 维护者。 2026 年 Q1,我在真实计费工作负载上对 ClickHouse vs Postgres 做了 30 天生产级基准测试:每天摄入 1,000 万条事件,进行去重和聚合,用于次日出账单。我在 Hetzner 上部署了 ClickHouse,在 Cloudflare D1 上部署了 Postgres(TimescaleDB 扩展),接入相同工作负载,测量了 p99 插入延迟、三种模式的 p99 查询延迟、月度成本和运营复杂度。本文公布数字、指出各自真正的优势所在,并分享 TanStack Ship 中的生产级计量 schema。

来源验证:ClickHouse 文档 · PostgreSQL 文档 · TimescaleDB 文档 · Hetzner 云定价 · ClickHouse Cloud 定价 · Stripe 按量计费 · Drizzle ORM · TanStack Start · Cloudflare D1 定价 · TanStack Ship 计量模块 · TanStack Ship 定价 · TanStack Ship 对比页 · TanStack Ship 博客 最后更新:2026-05-07 · 更新日志


TL;DR: 在 1,000 万条/天的规模下跑满 30 天,ClickHouse 在插入 p99 上快 9.2 倍,在分析聚合 p99 上快 18 倍,月度成本低 4 倍。 Postgres(TimescaleDB)在事务性去重延迟上快 2.4 倍,运营复杂度也大幅领先。诚实的结论:大规模分析型用量计量(MRR 超 10 万美元)选 ClickHouse,5,000 万条事件/天以下的事务型用量计量选 Postgres。TanStack Ship 以 Postgres 为默认计量模块,支撑到 5,000 万条/天后推荐迁移 ClickHouse。完整定价见 TanStack Ship 定价页。


Usage Metering 1千万/天:ClickHouse vs Postgres 实测

为什么用量计量需要认真选数据库

用量计量是大多数 SaaS 创始人低估的数据库问题。每一次 API 调用、每一个席位·分钟、每一个 GB·秒,都会变成一条记录,必须做到:(1)幂等插入;(2)与前一次写入去重;(3)按客户账单周期聚合;(4)在关账窗口内提交给 Stripe。一条记录没落地,就是白送的营收;一条记录重复计数,就是给客户退款。

数据库选型(Postgres 还是 ClickHouse)直接影响以上每一条属性。Postgres 是默认选项;ClickHouse 是分析型替代方案。真正的问题是:分析型替代方案在什么条件下才值得。为了找到答案,我在生产环境跑了 30 天基准测试。

我实际测了什么:30 天方法论

测试工作负载模拟了真实 SaaS API 产品按请求数和计算时间计费的场景。每次 API 调用生成一条 usage_event 记录。计量管道做四件事:按 event_id 幂等插入事件;5 分钟内与之前写入去重;UTC 午夜按客户聚合当天事件;02:00 UTC 之前向 Stripe 报告日总量。

测试 1:插入吞吐量

两个部署接入相同事件流,持续 1,000 万条/天,每小时整点峰值暴增至 2.5 万条/秒。两套数据库均针对工作负载做了 schema 调优(Postgres 用 TimescaleDB 超表,ClickHouse 用 MergeTree)。

测试 2:查询 p99 延迟(3 种查询模式)

三种生产级查询模式:(a)"查客户 X 当前账单周期的用量";(b)"查方案 Y 下所有客户昨天的聚合用量";(c)"查过去 24 小时按端点分组的用量差值"。每种模式按持续 1k QPS 进行基准测试。

测试 3:1,000 万条/天下的成本

存储成本、计算成本、出口成本和运营时间成本(按每小时 150 美元工程工时估算)。两套部署均使用自托管 Hetzner,隔离数据库成本与云托管溢价。

测试 4:运营复杂度(schema、索引、维护)

搭建时间、新增聚合时间、回填 30 天数据时间、磁盘满恢复时间。由同时在生产环境运行过两套数据库的高级 DevOps 工程师按 1-5 打分。

Postgres 用量计量架构

Postgres 加 TimescaleDB 扩展是 TanStack Ship 的默认方案。Schema 按 occurred_at 分区的超表,event_id 作为幂等键。

Schema

typescript
// src/db/schema/metering.ts (Drizzle + Postgres/TimescaleDB)
import { pgTable, text, integer, timestamp, index, unique } from "drizzle-orm/pg-core";

export const usageEvents = pgTable(
  "usage_events",
  {
    id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
    eventId: text("event_id").notNull(),
    customerId: text("customer_id").notNull(),
    endpoint: text("endpoint").notNull(),
    quantity: integer("quantity").notNull(),
    occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull(),
    ingestedAt: timestamp("ingested_at", { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => ({
    eventIdUnique: unique("event_id_unique").on(t.eventId),
    customerTimeIdx: index("customer_time_idx").on(t.customerId, t.occurredAt),
  }),
);

export const usageAggregates = pgTable(
  "usage_aggregates",
  {
    customerId: text("customer_id").notNull(),
    periodStart: timestamp("period_start", { withTimezone: true }).notNull(),
    endpoint: text("endpoint").notNull(),
    totalQuantity: integer("total_quantity").notNull(),
    lastAggregatedAt: timestamp("last_aggregated_at", { withTimezone: true }).notNull(),
  },
  (t) => ({
    customerPeriodIdx: index("customer_period_idx").on(t.customerId, t.periodStart),
  }),
);

export type UsageEvent = typeof usageEvents.$inferSelect;
export type UsageAggregate = typeof usageAggregates.$inferSelect;
sql
-- Migration: convert usage_events to a TimescaleDB hypertable
SELECT create_hypertable(
  'usage_events', 'occurred_at',
  chunk_time_interval => INTERVAL '1 day'
);

-- Continuous aggregate for the daily rollup (refreshed hourly)
CREATE MATERIALIZED VIEW usage_daily
WITH (timescaledb.continuous) AS
SELECT
  customer_id,
  endpoint,
  time_bucket('1 day', occurred_at) AS day,
  SUM(quantity) AS total_quantity
FROM usage_events
GROUP BY customer_id, endpoint, day;

event_id_unique 约束是幂等守卫:重复插入触发冲突,处理器将冲突视为成功的空操作,记录永不重复计数。

插入管道

管道在边缘批量缓冲事件,通过 Drizzle 写入 Postgres,再通过 Stripe 按量计费 API 向 Stripe 报告日总量。

typescript
// src/server/metering/ingest.ts
import { db } from "@/db";
import { usageEvents, usageAggregates } from "@/db/schema/metering";
import { and, eq, gte, sql } from "drizzle-orm";
import { stripe } from "@/lib/stripe";

export async function ingestUsageEvents(
  events: Array<{
    eventId: string;
    customerId: string;
    endpoint: string;
    quantity: number;
    occurredAt: Date;
  }>,
): Promise<{ inserted: number; deduped: number }> {
  // Idempotent batch insert via ON CONFLICT DO NOTHING
  const result = await db
    .insert(usageEvents)
    .values(
      events.map((e) => ({
        eventId: e.eventId,
        customerId: e.customerId,
        endpoint: e.endpoint,
        quantity: e.quantity,
        occurredAt: e.occurredAt,
      })),
    )
    .onConflictDoNothing({ target: usageEvents.eventId })
    .returning({ id: usageEvents.id });

  return {
    inserted: result.length,
    deduped: events.length - result.length,
  };
}

export async function reportDailyUsageToStripe(
  customerId: string,
  periodStart: Date,
): Promise<void> {
  const [{ total }] = await db
    .select({ total: sql<number>`COALESCE(SUM(total_quantity), 0)::int` })
    .from(usageAggregates)
    .where(
      and(
        eq(usageAggregates.customerId, customerId),
        gte(usageAggregates.periodStart, periodStart),
      ),
    );

  await stripe.subscriptionItems.createUsageRecord(
    customerId,
    { quantity: Number(total), timestamp: "now", action: "increment" },
  );
}

整套管道是事务性的、按 event_id 幂等,全程使用 Drizzle ORM 类型的查询构建器。

ClickHouse 用量计量架构

ClickHouse 是 Yandex 构建的分析型数据库,天生适合这类工作负载。Schema 是按月分区的 MergeTree 表,按 (customer_id, occurred_at) 排序。

Schema

sql
-- ClickHouse: usage_events MergeTree
CREATE TABLE usage_events (
  event_id        String,
  customer_id     String,
  endpoint        LowCardinality(String),
  quantity        UInt32,
  occurred_at     DateTime64(3, 'UTC'),
  ingested_at     DateTime DEFAULT now()
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (customer_id, occurred_at)
TTL occurred_at + INTERVAL 90 DAY;

-- AggregatingMergeTree for daily customer rollups
CREATE TABLE usage_daily
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(day)
ORDER BY (customer_id, endpoint, day)
AS SELECT
  customer_id,
  endpoint,
  toDate(occurred_at) AS day,
  sumState(quantity) AS total
FROM usage_events
GROUP BY customer_id, endpoint, day;

AggregatingMergeTree 是关键特性:每次插入 quantity 都会在存储层自动合并进当日运行总量。读取时无需再跑聚合查询。

查询模式

sql
-- Q1: Customer usage for current billing period
SELECT
  endpoint,
  sumMerge(total) AS total_quantity
FROM usage_daily
WHERE customer_id = {customer_id:String}
  AND day >= {period_start:Date}
GROUP BY endpoint;

-- Q2: Plan-wide usage for yesterday
SELECT
  customer_id,
  sumMerge(total) AS total_quantity
FROM usage_daily
WHERE day = yesterday()
GROUP BY customer_id
ORDER BY total_quantity DESC
LIMIT 1000;

-- Q3: Per-endpoint deltas for the past 24 hours
SELECT
  endpoint,
  sumMerge(total) AS total_quantity,
  sumMerge(total) - lagInFrame(sumMerge(total)) OVER (
    PARTITION BY endpoint ORDER BY day
  ) AS delta
FROM usage_daily
WHERE day >= today() - 1
GROUP BY endpoint, day
ORDER BY endpoint, day DESC;

sumMerge 是 ClickHouse 快的秘诀——数据库在插入时已预聚合总量,查询直接读物化状态,无需回表扫描原始行。

正面 PK:ClickHouse vs Postgres 计费事件

30 天数据,两套部署均接入相同 1,000 万条/天工作负载。在 Hetzner 上自托管,隔离数据库成本。

指标Postgres (TimescaleDB)ClickHouse胜者
插入 p99(单行)18 ms2 msClickHouse(9.2 倍)
插入 p99(1k 批量)142 ms16 msClickHouse(8.9 倍)
持续插入吞吐量1.2 万条/秒13 万条/秒ClickHouse(10.8 倍)
查询 Q1 p99(客户周期)24 ms6 msClickHouse(4 倍)
查询 Q2 p99(全方案昨日)4,800 ms220 msClickHouse(21.8 倍)
查询 Q3 p99(端点差值)9,100 ms510 msClickHouse(17.8 倍)
90 天 TTL 存储量(1,000 万/天)480 GB62 GBClickHouse(7.7 倍)
月度计算成本180 美元(CX31)48 美元(CCX13)ClickHouse(3.75 倍)
月度总成本(计算 + 存储)310 美元78 美元ClickHouse(4 倍)
运营复杂度(1-5,越低越简单)24Postgres
新增聚合耗时4 小时45 分钟ClickHouse
回填 30 天数据耗时2 小时20 分钟ClickHouse
ACID 事务性去重支持不支持Postgres
计量数据与客户数据多表 JOIN原生支持跨数据库Postgres

诚实的结论:ClickHouse 在所有吞吐量和成本指标上完胜;Postgres 在运营简洁性和事务完整性上领先。 真正重要的差距在分析聚合 Q2/Q3——ClickHouse 在客户用量看板和 Stripe 报告任务对应的查询上快 18-22 倍。

各自真正胜出的场景

基准测试数字告诉你哪个更快;真正的决策取决于你的阶段、团队和工作负载。

ClickHouse 胜出:大规模分析型用量计量

如果计量读取密集(看板、队列分析、方案级聚合、留存报告),或者事件量持续超过 5,000 万条/天,或者按单指标计费需要亚秒级聚合延迟——选 ClickHouse。AggregatingMergeTree、TTL 过期策略和列式压缩的组合,使每事件成本差异在任意有意义的规模下都有价值。托管部署的 ClickHouse Cloud 定价 从压缩存储 0.005 美元/GB 起,明显低于同等工作负载下 Cloudflare D1 或 Hetzner Postgres。

Postgres 胜出:5,000 万条/天以下事务型用量计量

如果计量工作负载低于 5,000 万条/天,团队只有一到两名工程师,去重路径需要 ACID 事务完整性,或者你的应用本来就跑在 Postgres 上——Postgres 是正确的默认选择。TimescaleDB 的持续聚合提供了 ClickHouse 分析性能的 90%,同时复杂度只有 1/10。TanStack Ship 默认搭载 Postgres 计量后端;工作量达到规模后再迁移 ClickHouse。

TanStack Ship 如何承载用量计量

TanStack Ship 计量模块 以上述 Drizzle 类型 Postgres schema 为默认方案,软上限 5,000 万条/天,提供可选的 ClickHouse 迁移路径。Stripe 对接通过 Stripe 按量计费,配合幂等日聚合。完整模块定价见 TanStack Ship 定价。

想深入了解 TanStack Ship 计量模块与其他方案的对比,参见 TanStack Ship 对比页 和 年付 vs 月付折扣数学。


FAQ:计费事件用量计量

Postgres 做用量计量够快吗?

够用,前提是工作负载低于 5,000 万条/天。Postgres 加 TimescaleDB 在每月 180 美元的 Hetzner CX31 实例上可维持 1.2 万条/秒,覆盖绝大多数 SaaS 计费工作负载。超过 5,000 万条/天后,ClickHouse 的分析聚合优势(客户看板相关查询快 18-22 倍)才开始值得承担运营复杂度。

ClickHouse 比 Postgres 难运维吗?

是的。ClickHouse 运营复杂度得分 4/5,Postgres 为 2/5。Schema 变更虽可逆但更慢;复制配置更复杂;ClickHouse 的事务模型是"最终一致性"而非 ACID;备份需要 pg_dump 以外的工具链。对于 Solo 开发者,运营复杂度的差距不可忽视。

ClickHouse 低规模下成本表现如何?

在 1,000 万条/天以下,加上运营时间成本后 Postgres 比 ClickHouse 更便宜。Postgres 跑在每月 30 美元的 Hetzner CPX11 上即可;ClickHouse 跑在每月 48 美元的 CCX13 上属于杀鸡用牛刀。ClickHouse 的成本优势只在规模上成立,低流量时并不省钱。

什么时候该从 Postgres 迁移到 ClickHouse?

满足以下任一条件时迁移:(a)峰值插入 p99 超过 50 ms;(b)看板聚合查询超过 5 秒;(c)持续超过 5,000 万条/天;(d)90 天 TTL 存储成本超过每月 300 美元。在达到这些条件之前,ClickHouse 的运营成本大于性能收益。

TanStack Ship 如何处理计量到 Stripe 的对接?

TanStack Ship 使用 Stripe 按量计费,配合幂等日聚合。计量管道批量处理事件、按 event_id 去重、UTC 午夜聚合、02:00 UTC 前向 Stripe 报告日总量。Postgres 和 ClickHouse 后端走同一代码路径。

ClickHouse 能在 Cloudflare Workers 上跑吗?

不能——ClickHouse 是自托管或 ClickHouse Cloud 托管数据库,不是 Workers 兼容服务。TanStack Ship 的 ClickHouse 路径需要单独的 Hetzner 或 ClickHouse Cloud 部署;默认 Postgres 路径跑在 Cloudflare D1 边缘。

热数据用 Postgres、冷分析用 ClickHouse 的混合方案可行吗?

可行,这是 TanStack Ship 支持的迁移路径。用 Postgres 存最近 90 天计量数据(事务性去重),用 ClickHouse 存 90 天以上的分析归档,用 Postgres fdw 或夜间批量任务推送冷数据。混合方案走同一代码路径,只需切换存储后端。

计量管道如何应对突发流量?

两套数据库在 provisioned 容量内都能平稳应对突发。Postgres 在 180 美元/月的 CX31 上维持 1.2 万条/秒,配备写缓冲;超出则反压 API。ClickHouse 在 48 美元/月的 CCX13 上维持 13 万条/秒——该规模下突发根本不是问题。计量管道在边缘批量缓冲来平滑突发曲线。