# 第 4 部分导论 · PromQL 的数据模型

> PromQL 不是 SQL，它的心智模型是「向量」。这一章讲清即时向量与区间向量的区别——这个区分是理解整门语言的钥匙，也是 90% 报错的来源——以及 lookback 这个隐形但关键的机制。

- 作者：David（道雾轩）
- 专栏：指标采集与观测（https://daiw.org/manual/observability.md）
- 最后更新：2026-07-15
- 原文：https://daiw.org/manual/observability/promql-model
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 第 4 部分导论 · PromQL 的数据模型

数据[存好了](https://daiw.org/manual/observability/query-path)，用户怎么提问？用 **PromQL**。

很多人学 PromQL 的方式是“背几个常用查询”，然后一改就报错。根因是**没建立心智模型**。这一章补上——**PromQL 的一切都建立在“向量”这个概念上**。

## 两种向量：整门语言的钥匙

<Callout type="info">
  **PromQL 有两种核心数据类型，搞混它们是 90% 报错的来源：**

  - **即时向量（instant vector）** —— **每条 series 在某一时刻的一个值**。
    ```text
    http_requests_total          →  {job="a"} 1027
                                    {job="b"} 3421      ← 一堆 series,每条一个值
    ```
  - **区间向量（range vector）** —— **每条 series 在一段时间内的一串值**。
    ```text
    http_requests_total[5m]      →  {job="a"} [1001, 1010, 1020, 1027]
                                    {job="b"} [3400, 3408, 3415, 3421]   ← 每条一串值
    ```

  **区别就是那个 `[5m]`。** 加了它就是区间向量，不加就是即时向量。

  **关键规则：**
  - **区间向量不能直接画图、不能直接比较** —— 你写 `http_requests_total[5m] > 100` 会报错，因为“一串值”没法和一个数比。
  - **区间向量必须先被“降维”成即时向量** —— 靠 `rate()`、`increase()`、`avg_over_time()` 这类**接收区间向量、返回即时向量**的函数。
  - **反过来，`rate()` 只接受区间向量** —— 写 `rate(http_requests_total)` 也报错，因为 `rate` 需要“一段时间”才能算速率。

  **这个“降维”关系就是 PromQL 的核心语法约束**：
  ```
  区间向量 --[rate/increase/xxx_over_time]--> 即时向量 --[聚合/比较/画图]--> 结果
  ```
  记住它，你就再也不会写错 `[5m]` 该加在哪。
</Callout>

## 为什么要分这两种

<Callout type="info">
  **因为时序数据的两类问题**本质不同**：**

  - **“现在是多少？”** —— 内存用量、队列长度、当前副本数。这是**即时向量**：每条 series 取一个值就够。
  - **“这段时间变化了多少？”** —— [counter 涨了多少](https://daiw.org/manual/observability/metric-types)、平均值是多少、最大值是多少。这需要**一段时间的所有点**，即**区间向量**。

  [第 1 部分说过 counter 的绝对值几乎没意义](https://daiw.org/manual/observability/metric-types)，你要的是变化率——而**变化率天然需要两个以上的点**。所以 counter 几乎总是 `rate(x[5m])` 这个形态：**先取一段（区间向量），再算速率（降回即时向量）**。

  **理解了这个，PromQL 里那些看起来奇怪的写法就都顺了。**
</Callout>

## lookback：那个隐形的 5 分钟

<Callout type="warn">
  **一个几乎所有人都被坑过、但很少有人真正理解的机制：**当你查询一个即时向量时，Prometheus 会往回找最多 **5 分钟**（默认 `lookbackDelta`，源码 `promql/engine.go:64`）。**

  ```go
  // promql/engine.go:64
  defaultLookbackDelta = 5 * time.Minute
  ```

  为什么需要它？因为[抓取是离散的](https://daiw.org/manual/observability/scrape-loop)（每 15 秒一个点），而你查询的时刻**几乎不可能正好落在某个采样点上**：

  ```
  采样点:    ●--------●--------●--------●
             10:00:00  10:00:15  10:00:30  10:00:45
  你查询:              ↑ 10:00:22 (中间!这里没有点)
  ```

  **lookback 的规则：往回找，找到最近的那个点就用它。** 于是查 10:00:22 会返回 10:00:15 那个点的值。**这让“任意时刻查询”成为可能**，否则你只能查在采样点上。

  **但它带来两个陷阱：**

  - **死掉的 series 会“续命”5 分钟** —— 目标挂了、不再有新数据，但接下来 5 分钟内查询**仍会返回最后那个值**。图上是一条平线，你以为服务还活着。**这就是[第 1 部分讲的 staleness marker](https://daiw.org/manual/observability/scrape-loop) 要解决的问题**——它显式标记“这条 series 结束了”，让 lookback 立刻停止返回旧值。
  - **抓取间隔 > 5 分钟的指标查不到** —— 如果你的抓取间隔是 10 分钟，那么大部分时刻往回找 5 分钟**什么都找不到**，查询返回空。**这是“我的指标时有时无”这类怪问题的常见根因。** 规则：**抓取间隔必须明显小于 lookback**（建议 ≤ 2 分钟）。
</Callout>

## 一个查询的完整形态

把这些串起来，看一个真实的 [RED 查询](https://daiw.org/manual/observability/red-method)：

```text
sum by (service) (
  rate( http_requests_total{status=~"5.."}[5m] )
)
```

逐层拆解：

```mermaid
flowchart TD
    A["http_requests_total{status=~'5..'}<br/>① 选择器 → 即时向量<br/>(靠倒排索引挑 series)"] --> B["[5m]<br/>② 加区间 → 区间向量<br/>(每条 series 一串值)"]
    B --> C["rate(...)<br/>③ 降维 → 即时向量<br/>(每条 series 一个速率)"]
    C --> D["sum by (service)<br/>④ 聚合 → 即时向量<br/>(按 service 合并)"]
    style B fill:#f59e0b,color:#000
    style C fill:#22c55e,color:#fff
```

<Callout type="info">
  **注意第 ① 步的 `{status=~"5.."}`——它直接对应[上一部分的倒排索引](https://daiw.org/manual/observability/index-format)。** 而且这是个**正则匹配**，[比等值匹配慢](https://daiw.org/manual/observability/index-format)——如果 `status` 取值不多（通常就几个），影响有限；但**如果你在高基数标签上用正则**，这一步就会成为瓶颈。**PromQL 的性能，一大半取决于第 ① 步选中了多少 series。**

  第 ④ 步的 `by (service)` 也值得注意：它**丢弃了除 `service` 外的所有标签**，把原本可能几千条 series 合并成几条。**聚合是降基数最有效的手段**——这也是[下一部分 recording rules](https://daiw.org/manual/observability/recording-rules) 的价值所在。
</Callout>

## PromQL 不是 SQL

<Callout type="warn">
  **几个 SQL 直觉会害你的地方：**

  - **没有 JOIN，只有向量匹配** —— PromQL 的“关联”是靠**标签匹配**自动完成的（[后面讲](https://daiw.org/manual/observability/promql-aggregation)）。`a / b` 会自动把标签相同的 series 配对，而不是像 SQL 那样写 `ON` 条件。
  - **没有“行”，只有 series** —— 你不能“取出第 3 行”。一切都是“一组 series”。
  - **时间是隐式的** —— SQL 里时间是个普通列，你要 `WHERE time > ...`；PromQL 里**时间是查询的固有维度**，由查询范围和 `[5m]` 决定，**你几乎从不显式写时间**。
  - **不是关系代数** —— 没有子查询嵌套那套（虽然有个 `[5m:1m]` 的[子查询语法](https://daiw.org/manual/observability/promql-performance)，但语义完全不同）。

  **把 PromQL 当成“对一组时间序列做向量运算”来理解，而不是“查表”**——这个心智切换是学会它的关键。
</Callout>

## 小结与下一章

PromQL 的钥匙是**两种向量**：**即时向量**（[每条 series 一个值](https://daiw.org/manual/observability/promql-model)，答“现在是多少”）和**区间向量**（[加 `[5m]`，每条 series 一串值](https://daiw.org/manual/observability/promql-model)，答“这段时间变化多少”）。**核心约束**：[区间向量不能直接比较 / 画图，必须先被 `rate()`/`xxx_over_time()` 这类函数**降维**成即时向量；而 `rate()` 又只接受区间向量](https://daiw.org/manual/observability/promql-model)——**记住这条降维关系，就不会写错 `[5m]` 的位置**。分两种是因为[两类问题本质不同](https://daiw.org/manual/observability/metric-types)（counter 的绝对值无意义，变化率天然需要多个点）。**lookback**（默认 **5 分钟**，`engine.go:64`）让“任意时刻查询”成为可能（[采样是离散的，你查的时刻几乎不在采样点上](https://daiw.org/manual/observability/scrape-loop)），但有两个陷阱：[**死 series 会续命 5 分钟**（靠 staleness marker 解决）、**抓取间隔 > 5 分钟的指标查不到**（建议间隔 ≤ 2 分钟）](https://daiw.org/manual/observability/scrape-loop)。**PromQL 不是 SQL**：[没有 JOIN（靠标签自动匹配）、没有“行”、时间是隐式维度](https://daiw.org/manual/observability/promql-model)。

心智模型有了。下一章看这门语言怎么被**解析**成可执行的东西、以及引擎怎么跑它。👉 [解析与执行](https://daiw.org/manual/observability/promql-engine)。

## 参考资料

- [1] Prometheus 源码 `promql/engine.go:64`（`defaultLookbackDelta`）、`promql/value.go`（Vector/Matrix 类型）。
- [2] Prometheus 官方文档：Querying basics、Query examples、staleness。
- [3] 关联[指标类型](https://daiw.org/manual/observability/metric-types)、[抓取](https://daiw.org/manual/observability/scrape-loop)、[倒排索引](https://daiw.org/manual/observability/index-format)。
