# 示例：通过 HTTPS 查询 350 万条出租车行程

> 用真实数据走一遍 DuckLocal：通过 HTTPS 直接查询纽约 350 万条出租车行程的公开 Parquet 文件，按小时、时段和支付方式画图，再做成一个仪表盘。

Source: https://ducklocal.app/docs/zh/example-nyc-taxi

**[English](../example-nyc-taxi.md)**

纽约市将黄色出租车的每趟行程数据公开为 Parquet 文件。以 2025 年 1 月的数据为例，文件大小为 59 MB，包含 3,475,226 行。DuckLocal 支持就地查询，无需预先下载、搭建集群或导入数据库。本示例带你从探索原始数据开始逐步编写查询，最终生成三张图表和一个仪表盘。

阅读前建议先完成 [10 分钟上手教程](tutorial.md)；至少需要了解使用 **⌘↵** 在编辑器中执行 SQL（Windows 和 Linux 下为 Ctrl+Enter），以及结果面板中包含 **图表** 标签页。

文件地址：

```text
https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet
```

::: tip 网络
第一次查询 `https://` 地址时，DuckDB 要下载 `httpfs` 扩展，所以需要联网
一次。之后每条查询只读取用得到的那部分文件：Parquet 按列分开存放，DuckDB
按字节范围请求，不会把整个文件拉下来。
:::

## 1. 先看一眼

在查询标签页里粘贴下面的 SQL，按 **⌘↵**：

```sql
SELECT *
FROM 'https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet'
LIMIT 100;
```

查询返回 100 行、20 列，包含上下车时间、距离、车费、小费、上下车区域以及支付方式。若不想横向滚动查看，可在查询语句前加上 `DESCRIBE`，直接查看所有列名及其数据类型：

```sql
DESCRIBE SELECT *
FROM 'https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet';
```

下面会用到的列：

| 列 | 类型 | 含义 |
| --- | --- | --- |
| `tpep_pickup_datetime` | TIMESTAMP | 开始计价的时间 |
| `fare_amount` | DOUBLE | 计价器车费，单位美元 |
| `tip_amount` | DOUBLE | 小费，单位美元（只含刷卡小费，见第 4 步） |
| `payment_type` | BIGINT | `1` 信用卡，`2` 现金，`3` 免费，`4` 争议 |

## 2. 一个月，按小时看

1 月里每个小时有多少趟行程？

```sql
SELECT date_trunc('hour', tpep_pickup_datetime) AS hour,
       count(*) AS trips
FROM 'https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet'
WHERE tpep_pickup_datetime >= '2025-01-01'
  AND tpep_pickup_datetime <  '2025-02-01'
GROUP BY 1
ORDER BY 1;
```

这条查询把 350 万行聚合成 744 行（这个月每小时一行），只要两秒多。
切到 **图表**：第一列是时间戳，所以 DuckLocal 画出一张随时间变化的面积图。
每天是一个峰，峰的高低随星期起伏；全月最忙的一个小时是 1 月 16 日（周四）
18 点，13,288 趟。

有两个细节让这张图不失真：

- **`WHERE` 条件。** 文件里有 22 趟行程不是在 1 月开始的：21 趟在跨年夜，
  1 趟在 2 月 1 日零点 44 秒。不加过滤，图的两端会多出四个零星的小时。
- **`ORDER BY 1`。** 图表按查询返回的行顺序绘制。`GROUP BY` 不保证顺序，
  不排序的话曲线会来回乱跳。

744 个点没有超过图表 1,500 个点的上限，所以每个小时都原样画出，不会分桶
取平均。图表标题里写明了画了多少个点，可以看一眼。

## 3. 一天的形状

把整个月折叠成一天：按一天中的钟点统计行程，从 00:00 到 23:00。

```sql
SELECT format('{:02d}:00', hour(tpep_pickup_datetime)) AS pickup_hour,
       count(*) AS trips
FROM 'https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet'
GROUP BY 1
ORDER BY 1;
```

**图表** 这次画出 24 根柱子。城市在凌晨 4 点最安静（全月 20,033 趟），
傍晚 6 点最忙（267,951 趟），后者是前者的十三倍多。

::: warning 为什么钟点要做成文本
标签特意用 `format('{:02d}:00', …)` 生成。结果图表画的是**第一个数值列**，
直接写 `hour(…)` 得到的却是数字。这样钟点本身就成了柱子的高度，看到的是一道
从 0 到 23 的整齐阶梯，而不是行程数。第一列是文本时只当标签，不会当数值。
补零（写成 `04:00` 而不是 `4:00`），`ORDER BY` 按文本排序时顺序才对。
:::

## 4. 谁给小费？

按支付方式看小费占车费的比例：

```sql
SELECT CASE payment_type
         WHEN 1 THEN 'Credit card' WHEN 2 THEN 'Cash'
         WHEN 3 THEN 'No charge'   WHEN 4 THEN 'Dispute'
         ELSE 'Other'
       END AS payment,
       round(100 * avg(tip_amount / fare_amount), 1) AS tip_pct,
       count(*) AS trips
FROM 'https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet'
WHERE fare_amount > 0
GROUP BY 1
ORDER BY trips DESC;
```

| payment | tip_pct | trips |
| --- | --- | --- |
| Credit card | 26.8 | 2444292 |
| Other | 2.7 | 455288 |
| Cash | 0.0 | 375850 |
| Dispute | 0.0 | 38787 |
| No charge | 0.1 | 15493 |

刷卡的乘客给车费的 27% 左右作小费；付现金的乘客，看上去一分不给。这是数据
的问题，不是乘客的问题：计价器只记录刷卡支付的小费。现金小费直接到了司机手里，
根本进不了这个文件。弄清一列数据从哪里来，和写对 SQL 一样重要。

`WHERE fare_amount > 0` 排除退款和零车费，避免除以零或符号反转。图表画的是
`tip_pct`，因为它是第一个数值列；想看行程量，就把 `trips` 放到前面。

## 5. 做成仪表盘

值得反复看的三条查询，适合放进一个[仪表盘](dashboards.md)。下载
[nyc-taxi.dash](https://ducklocal.app/docs/samples/nyc-taxi.dash)：里面有
按小时的趋势、按钟点的柱状图、小费图，以及一张列出背后数字的表。把它拖到
DuckLocal 窗口上，就会打开一个标签页，运行所有查询并画出所有图。

SQL 读的是绝对的 `https://` 地址，所以这个文件不管怎么打开、在哪里打开都能用。
分享之前，还可以用 `ducklocal check` 把每条查询跑一遍，确认没问题：

```bash
ducklocal check nyc-taxi.dash                         # 只检查语法和引用
ducklocal check nyc-taxi.dash --database any.duckdb   # 同时运行每条查询
```

## 6. 在终端里问同样的问题

不打开窗口，用 CLI 也能得到答案；AI agent 就是这样提问的：

```bash
ducklocal query --format md --sql "
  SELECT format('{:02d}:00', hour(tpep_pickup_datetime)) AS pickup_hour,
         count(*) AS trips
  FROM 'https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2025-01.parquet'
  GROUP BY 1 ORDER BY trips DESC LIMIT 3"
```

## 更进一步

- **离线使用，或者更快。** 每条查询都要重新通过网络读取所需的列。想留一份
  副本，就把第 1 步的 `SELECT *` 去掉 `LIMIT` 运行，然后点 **导出 Parquet…**，
  再把导出的本地文件拖进来，就得到一个可以按名字查询的视图。
- **更多月份。** 文件名遵循同一个规律：`yellow_tripdata_YYYY-MM.parquet`。
  DuckDB 可以把一组文件当成一张表来读：
  `FROM read_parquet(['https://…/yellow_tripdata_2025-01.parquet', 'https://…/yellow_tripdata_2025-02.parquet'])`。
  通配符（`*`）在普通 HTTPS 上不可用，因为 Web 服务器无法列出目录；在
  [S3](s3.md) 上可以。
- **区域。** `PULocationID` 和 `DOLocationID` 是出租车区域编号；纽约市出租车
  与豪华轿车委员会（TLC）另外发布了编号与区域名称的对照表。

数据由[TLC](https://www.nyc.gov/site/tlc/about/tlc-trip-record-data.page)
发布，那里也有每一列的说明文档。
