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。