Real-Time Market AnalyticsClickHouse Workshops

04 运行市场分析查询

七条针对 2650 万条 tick 的查询,每条都顶得上一整页 SQL:近似去重计数、topK、OHLC K 线、分位数、-If 组合器、argMax,以及压轴的 sort key 对比。

这些就是 ClickHouse 里"一个函数顶一整页 SQL"的那类函数。粘贴每个代码块、运行它, 再看绿色框里说明应该看到什么。它们跑的都是你刚加载好的 forex 表。

4.1 近似与精确:最亮眼的一招

2650 万条 tick 里有多少个不同的报价时间戳?三个函数回答同一个问题,取舍各不相同。 一次只跑一条,并留意查询统计里的耗时,分开跑才是重点所在。

-- Approximate count of distinct timestamps (HyperLogLog)
SELECT uniq(datetime) AS distinct_ts FROM forex;
-- Exact count — precise, but scans every value
SELECT uniqExact(datetime) AS distinct_ts FROM forex;
-- Adaptive + tunable accuracy vs memory
SELECT uniqCombined(datetime) AS distinct_ts FROM forex;

你应该看到

三者都给出约 2460 万个不同的时间戳。但 uniq 约 0.2 秒返回, uniqExact 要约 3 秒(慢约 17 倍)才能给出精确值,而 uniqCombined 在远不到一秒的时间里 把误差控制在约 0.4% 以内。到了几十亿行的规模,这个差距就是"瞬间返回"和"去喝杯咖啡"的区别。

4.2 一个函数搞定 Top-N

报价最活跃的货币对,不需要 GROUP BY / ORDER BY / LIMIT:

-- One row: an array of the 8 most actively quoted pairs (by tick count)
SELECT topK(8)(pair) AS most_active FROM forex;

你应该看到

单个单元格里装着一个含 8 个货币对的数组,报价最多的排在最前(黄金 XAU/USD 居首)。 topK 一次扫描就把整张表压缩成这份排名列表,它替代了 GROUP BY … ORDER BY count() DESC LIMIT 8。

4.3 一次扫描完成时序聚合:OHLC K 线

黄金的每日开盘 / 最高 / 最低 / 收盘,最基本的市场分析查询:

-- One row per day: gold (XAU/USD) open, high, low, close — a candlestick chart
SELECT
    toDate(datetime)      AS day,
    argMin(bid, datetime) AS open,    -- bid at the day's first tick
    max(bid)              AS high,
    min(bid)              AS low,
    argMax(bid, datetime) AS close    -- bid at the day's last tick
FROM forex
WHERE base = 'XAU' AND quote = 'USD'
GROUP BY day
ORDER BY day;

你应该看到

2020 年 1 月每天一行。argMin(bid, datetime) 是当天第一条 tick 的 bid(开盘价), argMax 是最后一条(收盘价),不用窗口函数,也不用自连接。看着黄金在这个月里 从约 1,520 涨到约 1,610。

4.4 不用绕窗口函数的分位数

bid/ask 价差是流动性的衡量指标,而平均值会掩盖尾部,所以要用分位数:

-- One row per pair: median and 99th-percentile bid/ask spread (tighter = more liquid)
SELECT
    pair,
    round(quantile(0.5)(ask - bid), 6)  AS median_spread,
    round(quantile(0.99)(ask - bid), 6) AS p99_spread
FROM forex
GROUP BY pair
ORDER BY median_spread ASC;

你应该看到

每个货币对一行,价差最窄的排在最前。EUR/USD 流动性最好(约 0.00002,不到一个 pip); 黄金最宽。quantile 是一次扫描内近似计算出来的,不需要对整列排序。

4.5 组合器:一次扫描,两个答案

活跃交易时段与清淡时段的平均价差,并排呈现,只需一次扫描:

-- One row per pair: avg spread during the active window vs quiet hours
SELECT
    pair,
    count()                                                           AS ticks,
    round(avgIf(ask - bid, toHour(datetime) BETWEEN 7 AND 20), 6)     AS spread_active,
    round(avgIf(ask - bid, toHour(datetime) NOT BETWEEN 7 AND 20), 6) AS spread_quiet
FROM forex
GROUP BY pair
ORDER BY ticks DESC;

你应该看到

活跃时段和清淡时段的价差出现在同一个结果里。-If 组合器可以给任何聚合函数挂上一个条件: avgIf(x, cond) 只对 cond 为真的地方求 x 的平均值,不用两遍扫描,也不用 CASE WHEN。 几乎每个聚合函数都支持它(countIf、sumIf、quantileIf,等等)。

4.6 取最新值:argMax

一次扫描拿到每个货币对最近的一次报价:

-- One row per pair: the latest quoted bid and the timestamp it was seen
SELECT
    pair,
    argMax(bid, datetime) AS last_bid,
    max(datetime)         AS as_of
FROM forex
GROUP BY pair
ORDER BY pair;

你应该看到

每个货币对最新的 bid。argMax(bid, datetime) 返回 datetime 最大的那一行的 bid, 这就是日常的"最新价格 / 最新状态"查询,不需要窗口函数,也不需要自连接。

4.7 压轴:有 sort key 与没有 sort key

查询形态相同,统计满足某个条件的 tick 数量,但一个过滤条件命中 sort key,另一个没有。 先把两个计数都跑一遍:

-- Optimized: base+quote ARE the leading sort key — the index skips to that pair
SELECT count() FROM forex WHERE base = 'XAU' AND quote = 'USD';

-- Non-optimized: bid is NOT in the sort key — no index, scans all 26.5M ticks.
-- (cache off so the full scan shows on every run)
SELECT count() FROM forex WHERE bid > 1.5
SETTINGS use_query_condition_cache = 0;

看不出差别?

两个计数都在几毫秒内返回,所以单看耗时几乎没有变化,count() 无论如何都很快。 真正的差别在于各自需要触碰多少数据。要真正看到它,用 EXPLAIN indexes = 1 让 ClickHouse 展示它的执行计划,其中会报告它将读取多少个 granule(每个约 8,192 行的数据块)。

-- Optimized: the primary index skips straight to the XAU/USD rows
EXPLAIN indexes = 1
SELECT count() FROM forex WHERE base = 'XAU' AND quote = 'USD';

-- Non-optimized: bid isn't in the sort key, so nothing can be skipped
EXPLAIN indexes = 1
SELECT count() FROM forex WHERE bid > 1.5;

你应该看到

在优化后的计划里,Indexes → PrimaryKey 部分显示只选中了一小部分 granule, 大约 492 / 3,234(约 1.6 万行)。在未优化的计划里没有索引可用, 所以它读取 3,234 / 3,234 个 granule,每一行都读。同样的查询形态,工作量差得离谱。 这里的教训是:把你最常用来过滤的列放在 ORDER BY 的最前面。

把这条留在剪贴板里

在下一个模块里,你要把那条未优化的 WHERE bid > 1.5 查询交给 AI。

本页内容

Track your progress?

Optional. We email a link to confirm your address; progress records once you open it.

Please use your work email address, not a personal one.

Progress tracking also requires accepting the current Terms of Service in Privacy settings.

ZH