跳到主要内容

AsOf Join

什么是 AsOf Join?

时间序列数据并不总是完美对齐。 时钟可能有轻微偏差,或因果之间存在延迟。 这会让两组有序数据的关联变得困难。 AsOf join 正是用来解决这类问题的工具。

AsOf join 的一个典型用途是: 在某个时间点查找一个变化属性当时的值。 这个场景非常常见,名称也由此而来:

给我这个属性在该时刻的值(as of this time)

更一般地说,AsOf join 内置了一些常见的时序分析语义, 而这些语义用标准 SQL 实现通常会很繁琐且性能较差。

投资组合示例数据集

先看一个具体示例。 假设我们有一张带时间戳的股票 prices 表:

tickerwhenprice
APPL2001-01-01 00:00:001
APPL2001-01-01 00:01:002
APPL2001-01-01 00:02:003
MSFT2001-01-01 00:00:001
MSFT2001-01-01 00:01:002
MSFT2001-01-01 00:02:003
GOOG2001-01-01 00:00:001
GOOG2001-01-01 00:01:002
GOOG2001-01-01 00:02:003

还有另一张在不同时间点记录持仓的 holdings 表:

tickerwhenshares
APPL2000-12-31 23:59:305.16
APPL2001-01-01 00:00:302.94
APPL2001-01-01 00:01:3024.13
GOOG2000-12-31 23:59:309.33
GOOG2001-01-01 00:00:3023.45
GOOG2001-01-01 00:01:3010.58
DATA2000-12-31 23:59:306.65
DATA2001-01-01 00:00:3017.95
DATA2001-01-01 00:01:3018.37

将这些表加载到 Goose:

CREATE TABLE prices AS FROM 'https://pub.kumose.cc/goose/data/prices.csv';
CREATE TABLE holdings AS FROM 'https://pub.kumose.cc/goose/data/holdings.csv';

内连接 AsOf Join

我们可以通过 AsOf join 找到持仓时间点之前最近一次价格, 从而计算每条持仓在该时刻的价值:

SELECT h.ticker, h.when, price * shares AS value
FROM holdings h
ASOF JOIN prices p
ON h.ticker = p.ticker
AND h.when >= p.when;

这会将该时刻的持仓价值附加到每一行:

tickerwhenvalue
APPL2001-01-01 00:00:302.94
APPL2001-01-01 00:01:3048.26
GOOG2001-01-01 00:00:3023.45
GOOG2001-01-01 00:01:3021.16

本质上,它是通过在 prices 表中查找相邻时间值来计算结果。 另外,缺失 ticker 匹配的行不会出现在输出中。

外连接 AsOf Join

由于 AsOf 从右表最多匹配一行, 左表行数不会因为 join 而增加, 但当右表缺少对应时间时,结果可能变少。 要处理这种情况,可以使用 outer AsOf join:

SELECT h.ticker, h.when, price * shares AS value
FROM holdings h
ASOF LEFT JOIN prices p
ON h.ticker = p.ticker
AND h.when >= p.when
ORDER BY ALL;

如你所料,当没有 ticker 匹配或时间早于价格序列起点时, 该写法会返回 NULL 的价格和值,而不是丢弃左表行。

tickerwhenvalue
APPL2000-12-31 23:59:30
APPL2001-01-01 00:00:302.94
APPL2001-01-01 00:01:3048.26
GOOG2000-12-31 23:59:30
GOOG2001-01-01 00:00:3023.45
GOOG2001-01-01 00:01:3021.16
DATA2000-12-31 23:59:30
DATA2001-01-01 00:00:30
DATA2001-01-01 00:01:30

在 AsOf Join 中使用 USING 关键字

到目前为止,我们一直显式写出 AsOf 条件。 当两表列名相同时,SQL 还提供了更简化的 join 条件语法。 该语法通过 USING 关键字列出需要做相等比较的字段。 AsOf 同样支持此语法,但有两个限制:

  • 最后一个字段用于不等式比较
  • 不等式固定为 >=(最常见场景)

这样首个查询可以写为:

SELECT ticker, h.when, price * shares AS value
FROM holdings h
ASOF JOIN prices p USING (ticker, "when");

ASOF Join 中 USING 的列选择说明

在 join 中使用 USING 时,USING 子句中列会在结果集中被合并。 这意味着如果你执行:

SELECT *
FROM holdings h
ASOF JOIN prices p USING (ticker, "when");

返回的只有 h.ticker, h.when, h.shares, p.pricetickerwhen 只会出现一次,且来自左表(holdings)。

这种行为对 ticker 没问题,因为两表值相同。 但对 when 来说,由于 AsOf 使用 >= 条件,两表值可能不同。 AsOf join 的设计是:按 when 将左表(holdings)每行匹配到右表(prices)中最接近且不晚于它的那一行。

如果你想同时取回两表的 when(查看两个时间戳), 就要显式列出列名,而不是依赖 *,例如:

SELECT h.ticker, h.when AS holdings_when, p.when AS prices_when, h.shares, p.price
FROM holdings h
ASOF JOIN prices p USING (ticker, "when");

这样可以拿到两表完整信息,避免 USING 默认行为造成歧义。

另见

实现细节参见博客:“Goose's AsOf joins: Fuzzy Temporal Lookups”