# 结果与图表

> 处理 DuckLocal 查询结果：过滤与复制、逐列概览、柱状图/折线图/散点图/饼图/地图，并导出为 CSV 或 Parquet。

Source: https://ducklocal.app/docs/zh/results-and-charts

**[English](../results-and-charts.md)**

编辑器下方的面板有三个标签页：**结果**、**图表** 和 **概览**。标签页右侧是结果的规模（`N 行 · M 列`）以及 **导出 CSV** 和 **导出 Parquet** 两个按钮。

编辑器工具栏上 **执行计划** 和 **性能分析** 按钮的输出也显示在 **结果** 标签页里，见[执行计划与性能分析的输出](#explain-and-profile)。

## 结果表格

结果表格最左侧是 `#` 行号列，其后是查询结果的各列，数值列右对齐。每列的初始宽度根据表头和前 200 行内容计算，中日文字符按两个字宽计。在默认[界面大小](settings-and-data.md#interface-size)下，列宽介于 80 到 360 像素之间，其他大小按比例缩放。可以拖动调整列宽，但 `#` 列宽度固定。列的总宽度超过面板时，下方会一直显示横向滚动条。

点击列标题上的箭头，按这一列给已加载的行排序：先降序，再升序，第三次恢复查询原来的顺序。数值列按数值排，其余按文本排（ISO 格式的日期和时间因此也能排对），`NULL` 排在最后，值相同的行保持查询原来的顺序。[过滤](#filtering)不会打乱排序，下一次的结果从未排序开始。排序和过滤一样只在本地进行：要对超过已加载行数的结果排序，请在 SQL 里用 `ORDER BY`。

### 单元格显示

| 值 | 显示为 |
| --- | --- |
| `NULL` | `NULL` 字样，颜色较浅 |
| BLOB | `<blob N bytes>` |
| GEOMETRY | `<geometry N bytes>` |
| 值为整数的浮点数 | `3.0` |
| 时间戳 | `2026-09-17 04:21:09.5`——**UTC 时间，不是本地时间**；小数部分有几位就显示几位（TIMESTAMP_NS 最多九位） |
| Interval | DuckDB 的写法，如 `1 day 02:00:00` |
| List / struct / map | 格式化显示 |

在 SQL 里比较时间戳不受影响；直接看表格里的时间时，别忘了这是 UTC。

### 过滤 {#filtering}

过滤框在已加载行的**所有单元格**中查找包含关键词的内容，不区分大小写。过滤只在本地进行，不会重新查询数据库。因此，它只作用于已加载的行，不代表完整查询结果。

输入关键词后按 Enter 才会过滤，打字时不会逐字刷新；清空输入框即恢复全部行。过滤生效时，过滤框旁边显示 `显示行数 / 总行数 行`，例如 `120 / 5000 行`。

## 行数上限 {#row-limits}

结果达到以下任一上限，就会被截断：

- **100,000 行**
- **2,000,000 个单元格**：400 列的结果最多只生成约 5,000 行

只限行数，控制不住计算量，所以另设了单元格上限。对高基数列做 `PIVOT` 时，每个不同的值都会生成一列，要格式化的单元格可能多达上千万，界面看上去就像卡死了。

结果被截断时会出现一条横幅，注明实际加载了多少行。**没有分页，也没有“加载更多”**，需要更多数据时请缩小查询范围。

## 复制

鼠标移到单元格上会出现复制按钮。右键点击单元格，还可以选择 **复制单元格**、**复制整行**（用制表符分隔）或 **复制整列**（每行一个值，按显示顺序）。右键点击行号可复制整行。复制遵循当前的过滤和排序；仪表盘的表格也提供这三个复制操作。不能框选多个单元格，也没有“复制为 SQL”；要复制更多内容，请用[导出](#export)。

## 导出 {#export}

点 `导出 CSV` 或 `导出 Parquet` 会弹出一个小对话框，路径框里默认填好了 `~/Desktop/export.csv` 或 `~/Desktop/export.parquet`。

有两点需要注意：

- 导出时会**重新运行当前结果对应的 SQL**，所以文件里是完整的查询结果，既不受过滤影响，也不受表格截断影响。
- 没有文件选择窗口，同名文件也不会提示覆盖。输入路径，就直接写入。

## 图表 {#charts}

在你选列之前，图表直接用表格里已有的行来画，顺序与查询返回的一致，不做聚合。用 [**X**、**Y** 和 **拆分** 菜单](#pick-columns)选列后，则由 DuckDB 对完整结果分组。

图表上方的类型选择默认是 **自动**，按结果的列来选：

| 结果里有 | 自动画出 |
| --- | --- |
| 一个纬度列和一个经度列 | 地图 |
| 时间类型的第一列（date / timestamp） | 面积图；第 0 列为 x 轴，每个数值列画一条序列 |
| 其他 | 柱状图；第 0 列为标签，第一个数值列为数值 |

注意，“数值列”也包括第一列。如果第 0 列是数字，比如小时、年份或 ID，柱状图会把它当作数值，而不是标签。要让它做标签，先转成文本：`year::VARCHAR`，或 `format('{:02d}:00', hour(ts))`。

**柱状图**、**折线图**、**面积图** 和 **散点图** 对任何结果都能直接切换；第一列不是时间的结果还可以选 **饼图**，有坐标的结果可以选 **地图**。选择会跨次执行保留；新结果画不了所选类型时（比如没有坐标却选了地图），图表退回自动的选择，但不会忘记你选的类型。

### 选择列 {#pick-columns}

图表上方的 **X**、**Y** 和 **拆分** 菜单决定画什么。**Y** 可以选行数，或者数值列的求和、平均、最小值、最大值。之后由 DuckDB 对完整结果（而不只是已加载的行）分组，保留最大的 200 个类别（或前 50,000 个有序值）和最大的 12 条序列，并用提示说明略去了什么。

- 选了 **拆分** 后，图表类型里会多出 **堆叠柱状图** 和 **热力图**，**饼图** 则不再提供。
- 有可分箱的数值时会提供 **直方图**，由 DuckDB 对完整结果分箱。
- **复制为 .dash** 把你选的内容复制成 `query` 和 `plot` block，在[仪表盘](dashboards.md)里能画出同一张图。

没有选列时，如果结果被截断，图表会注明 **仅基于表格已加载的前 N 行绘制，并非完整结果**。被截断的时间序列会在后台重新运行，并对全部行取均值。

### 图表类型与限制

**饼图** 按标签画第一个数值列，从大到小。零、负数和缺失的值不画成扇区，会有提示说明有几行；超过 8 个扇区时，最大的 7 个保留，其余合并成"其他"。

**地图** 按列名识别坐标：

- **纬度**：`lat`、`latitude`，或任何包含这些词的名字（`geo_lat`、`start_lat`）；`纬度` 也可以。
- **经度**：`lng`、`lon`、`long`、`longitude`，同理；`经度` 也可以。

每一行是一个点，用 Web Mercator 投影，范围自动贴合这些点，背景是标了度数的经纬网；有取值不多的文本列时，按它给点着色。鼠标移到点上可以看到这一行。

点的下面可以画一层 OpenStreetMap 底图，在 **显示 → 在线底图（OpenStreetMap）** 里打开。它默认关闭，因为加载瓦片需要联网：瓦片请求大致能看出你在看哪片区域，但不会带上任何一行数据。瓦片缓存一周；离线时只画经纬网。

数值位置不是数字的行（例如 `NULL`）不参与绘图。

超出以下限制时，图表标题下方会有一行提示：

| 限制 | 处理方式 |
| --- | --- |
| 12 条序列 | 多出的数值列不画 |
| 1,500 个数据点 | 数据分桶，每桶取**平均值** |
| 200 根柱子 | 超出的柱子直接截掉，不合并 |
| 8 个饼图扇区 | 最小的几个合并成"其他" |
| 20,000 个地图点 | 超出的点不画 |

标题上总会写明画了多少个数据点。判断曲线走势之前，先看一眼这个数。

## 列概览 {#overview}

**概览** 标签页一次描述当前结果的每一列：填充了多少、有多少种取值、取值范围，以及取值的分布。

顶部一行是 `当前查询结果 · N 行 · M 列 · 耗时 …`。之后每列占一行：

| 部分 | 显示内容 |
| --- | --- |
| 名称和类型 | 列名及其 DuckDB 类型 |
| 空值 | `NULL` 所占的比例，附一条小进度条；大于 0% 时高亮 |
| 不同值 | `约 N 个不同值`，是 DuckDB 估算的不同值个数 |
| 范围 | 最小值 – 最大值；两者相等时只显示这一个值 |
| 中位数 | 数值、日期和时间戳列才有 |
| 分布 | 一张小图，见下文 |

分布图按列的类型而定：

- **数值、日期和时间戳**：画一张约 12 根等宽柱子的直方图。只有一个取值的列改为列出这个值。
- **文本和布尔值**：列出最常见的 5 个值，各自附上占总行数的比例；鼠标移到某个值上可以看到确切的行数。不同值超过约 10,000 个时，这些值是估算的，列在 *高频值（估算）* 下面，不显示比例。
- **List、struct、map 和 union**：不画图。

工作方式：

- **概览会把查询再完整运行一次**，分两遍扫描：先用 `SUMMARIZE` 算统计值，再用一条 `SELECT` 算出每一列的分布。所以它覆盖完整的结果，而不只是表格在[行数上限](#row-limits)内保留的那些行；慢查询也会再慢一次。结果不确定的查询（`random()`、`USING SAMPLE`）描述的是第二次运行的结果。
- 打开这个标签页时计算；标签页开着时，每来一个新结果就重新计算。
- **只有能放进 `FROM ( … )` 的查询才能概览**，即以 `SELECT`、`WITH`、`FROM`、`VALUES`、`TABLE`、`PIVOT` 或 `UNPIVOT` 开头的查询（开头的 `--` 注释会跳过）。其他语句，如 `SHOW`、`DESCRIBE`、`PRAGMA`、`CALL`、`SUMMARIZE`，会提示 *只有 SELECT / WITH / FROM 这类查询的结果可以概览：概览会把查询再运行一次。*

不写查询也能概览整张表、视图或已注册的文件：在侧栏右键它，选 **列概览**。这会把 `FROM <名称>;` 填进编辑器并运行，然后打开这个标签页。见[右键菜单](schema-and-history.md#right-click-menu)。

## 执行计划与性能分析的输出 {#explain-and-profile}

编辑器工具栏上的 [**执行计划** 和 **性能分析** 按钮](sql-editor.md#explain-and-profile)把输出显示在 **结果** 标签页里，当时开着别的标签页也会切回来。两者都会替换掉结果表格，要看回查询结果，重新运行一次即可。

**执行计划** 用等宽纯文本显示 DuckDB 的查询计划，上方一行是 `EXPLAIN · 耗时 …`。

**性能分析** 把实际执行的计划画成一棵树，每个算子一行，缩进在它所供数的算子下面：

- 一条进度条，表示该算子耗时占全部算子耗时的比例；
- 算子自身的耗时，不含子算子；
- 算子名称，如 `TABLE_SCAN`、`FILTER`、`HASH_GROUP_BY`；
- `N 行`：它产出的行数；
- `预估 N`：优化器对这一行数的估计（计划中有时才显示）。与实际相差超过 10 倍（无论偏大偏小）时高亮，鼠标移上去会提示 *预估与实际相差超过 10 倍*。估计偏差这么大，常常是连接（join）慢的原因；
- 下面几行是算子处理的内容：过滤条件、连接键、投影列。

顶部一行是 `性能分析 · 总耗时 … · 算子合计 …`：前者是实际经过的时间，后者是各算子自身耗时之和。两者不相等，因为同一流水线上的算子是交错运行的；进度条的比例以后者为准。旁边的 `最耗时：名称（耗时）` 指出自身耗时最多的算子，这个算子所在的行用红色显示。
