ClickHouseInc

ClickHouse DateTime:高效处理时间数据的秘密武器

图片

本文字数:768;估计阅读时间:19 分钟

作者:Mark Needham

Image

Meetup活动

ClickHouse 上海第3届 Meetup 火热报名中,详见文末海报!

图片

在本文中,我们将探讨 ClickHouse 中一些最有用的日期和日期时间查询与过滤函数,包括按小时或15分钟窗口进行时间取整、按一天中的特定时间过滤,以及计算两个时间戳之间的时间差。

如果你正在处理需要首先转换的原始日期字符串或时间戳,请查阅我撰写的另一篇关于在 ClickHouse 中解析日期和日期时间的文章(https://clickhouse.com/blog/parsing-dates-datetimes)。本文将在此基础上,重点介绍当数据已存储在 DateTime 列中时如何进行操作。

Image
导入纽约市出租车数据集

我们选用的数据集是纽约市出租车数据集,接下来我们将在 ClickHouse 中进行设置。首先,我们创建一个数据库:

CREATE DATABASE nyc_taxi;

然后创建一个表:

CREATE TABLE nyc_taxi.trips_small (

    trip_id             UInt32,

    pickup_datetime     DateTime,

    dropoff_datetime    DateTime,

    pickup_longitude    Nullable(Float64),

    pickup_latitude     Nullable(Float64),

    dropoff_longitude   Nullable(Float64),

    dropoff_latitude    Nullable(Float64),

    passenger_count     UInt8,

    trip_distance       Float32,

    fare_amount         Float32,

    extra               Float32,

    tip_amount          Float32,

    tolls_amount        Float32,

    total_amount        Float32,

    payment_type        Enum('CSH' = 1, 'CRE' = 2, 'NOC' = 3, 'DIS' = 4, 'UNK' = 5),

    pickup_ntaname      LowCardinality(String),

    dropoff_ntaname     LowCardinality(String)

)

ENGINE = MergeTree

PRIMARY KEY (pickup_datetime, dropoff_datetime);

接着,我们可以运行以下命令导入数据:

INSERT INTO nyc_taxi.trips_small

SELECT

    trip_id,

    pickup_datetime,

    dropoff_datetime,

    pickup_longitude,

    pickup_latitude,

    dropoff_longitude,

    dropoff_latitude,

    passenger_count,

    trip_distance,

    fare_amount,

    extra,

    tip_amount,

    tolls_amount,

    total_amount,

    payment_type,

    pickup_ntaname,

    dropoff_ntaname

FROM s3(

    'https://datasets-documentation.s3.eu-west-3.amazonaws.com/nyc-taxi/trips_{0..2}.gz',

    'TabSeparatedWithNames'

);

此查询导入了超过300万条记录,从下面的输出中可以看出:

3000317 rows in set. Elapsed: 32.077 sec. Processed 3.00 million rows, 256.38 MB (93.53 thousand rows/s., 7.99 MB/s.)

Peak memory usage: 536.51 MiB.

如果需要导入更多数据,我们可以调整 URI 中的 {0..2} 以包含更多文件。

数据加载完成后,现在可以开始编写查询了。我们将主要处理 pickup_datetime 和 dropoff_datetime 这两列。

Image
按小时统计的行程

让我们首先探索2015年7月1日的出租车行程数据。我们将使用 toStartOfHour 将日期时间向下取整到最近的小时,使用 toDate 按指定日期过滤,并使用 toHour 将时间限定在上午时段:

SELECT

  toStartOfHour(pickup_datetime) as hour,

  count() as trips,

  round(avg(passenger_count), 1) as avg_passengers

FROM nyc_taxi.trips_small

WHERE toDate(pickup_datetime) = '2015-07-01'

AND toHour(pickup_datetime) < 13

GROUP BY hour

ORDER BY hour;

┌────────────────hour─┬─trips─┬─avg_passengers─┐

│ 2015-07-01 00:00:00 │   663 │            1.7 │

│ 2015-07-01 01:00:00 │   381 │            1.6 │

│ 2015-07-01 02:00:00 │   249 │            1.8 │

│ 2015-07-01 03:00:00 │   155 │            1.6 │

│ 2015-07-01 04:00:00 │   159 │            1.5 │

│ 2015-07-01 05:00:00 │   197 │            1.5 │

│ 2015-07-01 06:00:00 │   530 │            1.6 │

│ 2015-07-01 07:00:00 │   849 │            1.6 │

│ 2015-07-01 08:00:00 │  1034 │            1.6 │

│ 2015-07-01 09:00:00 │  1033 │            1.7 │

│ 2015-07-01 10:00:00 │   898 │            1.7 │

│ 2015-07-01 11:00:00 │   900 │            1.6 │

│ 2015-07-01 12:00:00 │   961 │            1.7 │

└─────────────────────┴───────┴────────────────┘

夜间较为平静,行程量从早上6点左右开始上升,并在上午8-9点达到峰值。但7月1日的数据是否具有代表性呢?让我们移除日期过滤条件,观察所有日期的数据。为此,我们利用 ::Time 将 toStartOfHour 强制转换为 Time 类型,这会去除日期信息,仅保留时间,从而将所有日期的数据按时间进行聚合:

SELECT

  toStartOfHour(pickup_datetime)::Time as hour,

  count() as trips,

  round(avg(passenger_count), 1) as avg_passengers

FROM nyc_taxi.trips_small

WHERE toHour(pickup_datetime) < 13

GROUP BY hour

ORDER BY hour;

┌─────hour─┬──trips─┬─avg_passengers─┐

│ 00:00:00 │ 118268 │            1.7 │

│ 01:00:00 │  86495 │            1.7 │

│ 02:00:00 │  65246 │            1.7 │

│ 03:00:00 │  47377 │            1.7 │

│ 04:00:00 │  34840 │            1.7 │

│ 05:00:00 │  32328 │            1.6 │

│ 06:00:00 │  68644 │            1.6 │

│ 07:00:00 │ 107494 │            1.6 │

│ 08:00:00 │ 132596 │            1.6 │

│ 09:00:00 │ 136228 │            1.6 │

│ 10:00:00 │ 134286 │            1.7 │

│ 11:00:00 │ 137561 │            1.7 │

│ 12:00:00 │ 145282 │            1.7 │

└──────────┴────────┴────────────────┘

观察所有日期的数据,我们依然发现夜间行程量呈下降趋势,但行程量的增长提前了几个小时,从早上6点到7点。此后四小时内,行程数量保持相对稳定。

Image
15分钟时间窗口的高峰时段

早上高峰期究竟从何时开始?让我们使用 toStartOfFifteenMinutes 函数将数据按 15 分钟间隔进行分桶,并使用 formatDateTime 函数使输出结果更具可读性。在 WHERE 子句中,我们将 pickup_datetime 转换为 Time 类型,以便仅按一天中的时间进行过滤,无需考虑日期:

SELECT

  formatDateTime(toStartOfFifteenMinutes(pickup_datetime), '%r') AS timeWindow,

  count() as trips,

  round(avg(trip_distance), 2) as avgDistance

FROM nyc_taxi.trips_small

WHERE pickup_datetime::Time BETWEEN '06:00:00'::Time AND '09:59:59'::Time

AND trip_distance > 0

GROUP BY timeWindow

ORDER BY timeWindow;

┌─timeWindow─┬─trips─┬─avgDistance─┐

│ 06:00 AM   │ 11601 │        4.47 │

│ 06:15 AM   │ 14645 │        3.97 │

│ 06:30 AM   │ 19033 │        3.67 │

│ 06:45 AM   │ 22795 │         3.2 │

│ 07:00 AM   │ 23179 │        3.27 │

│ 07:15 AM   │ 25465 │        3.12 │

│ 07:30 AM   │ 28350 │        3.04 │

│ 07:45 AM   │ 29914 │        2.89 │

│ 08:00 AM   │ 30444 │           3 │

│ 08:15 AM   │ 32063 │        2.91 │

│ 08:30 AM   │ 34293 │         2.8 │

│ 08:45 AM   │ 35116 │        2.63 │

│ 09:00 AM   │ 33776 │        2.74 │

│ 09:15 AM   │ 33800 │        2.72 │

│ 09:30 AM   │ 33694 │        2.73 │

│ 09:45 AM   │ 34235 │        2.63 │

└────────────┴───────┴─────────────┘

高峰从 6:30 开始出现,到 7:30 达到加速阶段,并在 8:45 达到峰值。

Image
高峰期出行体验如何?

我们已经知道了高峰期何时开始,但如果你乘坐出租车,高峰期会是怎样的体验呢?我们可以使用 dateDiff 函数计算行程持续时间(分钟),这使我们能够计算平均速度。我们还在 WHERE 子句中使用 dateDiff 来过滤掉持续时间为零的行程(错误数据):

WITH buckets AS (

  SELECT

    formatDateTime(toStartOfFifteenMinutes(pickup_datetime), '%r') AS timeWindow,

    count() as trips,

    round(avg(trip_distance), 2) as avgDist,

    round(avg(dateDiff('minute', pickup_datetime, dropoff_datetime)), 1) AS avgDuration,

    round(avg(

      trip_distance /

      (dateDiff('minute', pickup_datetime, dropoff_datetime) / 60)

    ), 1) AS avgSpeed

  FROM nyc_taxi.trips_small

  WHERE pickup_datetime::Time BETWEEN '06:00:00'::Time AND '09:59:59'::Time

  AND trip_distance > 0

  AND dateDiff('minute', pickup_datetime, dropoff_datetime) > 0

  GROUP BY timeWindow

  ORDER BY timeWindow

)

SELECT timeWindow, trips, avgDuration, avgDist, avgSpeed,

       bar(avgSpeed, 0, (SELECT max(avgSpeed) FROM buckets), 20) AS speedBar

FROM buckets

ORDER BY timeWindow ASC;

┌─timeWindow─┬─trips─┬─avgDuration─┬─avgDist─┬─avgSpeed─┬─speedBar─────────────┐

│ 06:00 AM   │ 11562 │        13.2 │    4.48 │     19.3 │ ████████████████████ │

│ 06:15 AM   │ 14609 │        13.1 │    3.98 │     18.2 │ ██████████████████▊  │

│ 06:30 AM   │ 18993 │        12.5 │    3.67 │     17.3 │ █████████████████▉   │

│ 06:45 AM   │ 22754 │        11.5 │    3.21 │     16.1 │ ████████████████▋    │

│ 07:00 AM   │ 23139 │        12.3 │    3.27 │     15.4 │ ███████████████▉     │

│ 07:15 AM   │ 25428 │        12.7 │    3.12 │     14.3 │ ██████████████▊      │

│ 07:30 AM   │ 28313 │        13.9 │    3.04 │     13.5 │ █████████████▉       │

│ 07:45 AM   │ 29873 │        13.8 │    2.89 │     12.7 │ █████████████▏       │

│ 08:00 AM   │ 30411 │        14.2 │       3 │       12 │ ████████████▍        │

│ 08:15 AM   │ 32017 │        15.2 │    2.91 │     11.5 │ ███████████▉         │

│ 08:30 AM   │ 34258 │        15.4 │     2.8 │       11 │ ███████████▍         │

│ 08:45 AM   │ 35071 │        14.9 │    2.64 │     10.8 │ ███████████▏         │

│ 09:00 AM   │ 33718 │        15.3 │    2.74 │     10.9 │ ███████████▎         │

│ 09:15 AM   │ 33754 │        15.3 │    2.72 │     10.8 │ ███████████▏         │

│ 09:30 AM   │ 33657 │        15.4 │    2.73 │     10.8 │ ███████████▏         │

│ 09:45 AM   │ 34188 │        14.8 │    2.63 │     10.9 │ ███████████▎         │

└────────────┴───────┴─────────────┴─────────┴──────────┴──────────────────────┘

bar 函数用于绘制根据最大值进行缩放的 ASCII 柱状图,这是一种方便地以内联方式可视化相对值的方法。

早上 6 点,出租车行驶速度略高于 19 英里/小时。到早上 8 点,速度降至约 12 英里/小时——虽然很慢,但对于大城市高峰期来说相当普遍。随着上午时间的推移,速度会持续下降。

Image
工作日与周末

我们已经证实了高峰期的存在,但周末也会出现高峰期吗?我们可以使用 countIf 结合 toDayOfWeek 函数来区分工作日和周末的行程:其中,1-5 代表工作日,6-7 代表周末。接着,我们使用 lag 窗口函数计算每 15 分钟窗口之间行程数量的百分比变化:

WITH trips AS (

  SELECT

    formatDateTime(toStartOfFifteenMinutes(pickup_datetime), '%r') AS timeWindow,

    countIf(toDayOfWeek(pickup_datetime) <= 5) as wdTrips,

    countIf(toDayOfWeek(pickup_datetime) > 5) as weTrips

  FROM nyc_taxi.trips_small

  WHERE trip_distance > 0

  AND pickup_datetime::Time BETWEEN '06:00:00'::Time AND '09:59:59'::Time

  GROUP BY timeWindow

  ORDER BY timeWindow

)

SELECT timeWindow, wdTrips,

  round((

    (wdTrips - lag(wdTrips) OVER (ORDER BY timeWindow)) /

    lag(wdTrips) OVER (ORDER BY timeWindow)) * 100,

  1) as wdPctChange,

  weTrips,

  round(

    ((weTrips - lag(weTrips) OVER (ORDER BY timeWindow)) /

    lag(weTrips) OVER (ORDER BY timeWindow)) * 100,

  1) as wePctChange

FROM trips

ORDER BY timeWindow;

┌─timeWindow─┬─wdTrips─┬─wdPctChange─┬─weTrips─┬─wePctChange─┐

│ 06:00 AM   │    9398 │         inf │    2203 │         inf │

│ 06:15 AM   │   12254 │        30.4 │    2391 │         8.5 │

│ 06:30 AM   │   16106 │        31.4 │    2927 │        22.4 │

│ 06:45 AM   │   19727 │        22.5 │    3068 │         4.8 │

│ 07:00 AM   │   20285 │         2.8 │    2894 │        -5.7 │

│ 07:15 AM   │   22129 │         9.1 │    3336 │        15.3 │

│ 07:30 AM   │   24494 │        10.7 │    3856 │        15.6 │

│ 07:45 AM   │   25681 │         4.8 │    4233 │         9.8 │

│ 08:00 AM   │   26259 │         2.3 │    4185 │        -1.1 │

│ 08:15 AM   │   27509 │         4.8 │    4554 │         8.8 │

│ 08:30 AM   │   28891 │           5 │    5402 │        18.6 │

│ 08:45 AM   │   29154 │         0.9 │    5962 │        10.4 │

│ 09:00 AM   │   27872 │        -4.4 │    5904 │          -1 │

│ 09:15 AM   │   27268 │        -2.2 │    6532 │        10.6 │

│ 09:30 AM   │   26426 │        -3.1 │    7268 │        11.3 │

│ 09:45 AM   │   26070 │        -1.3 │    8165 │        12.3 │

└────────────┴─────────┴─────────────┴─────────┴─────────────┘

工作日显示在 6:15–6:30 有一个急剧增长,在 7:15–7:30 左右有第二次(较小)增长,并在 8:45 达到峰值后趋于平稳。周末则有所不同:6:30 的增长更为平缓,且行程数量在上午持续稳步增长。

图片
Meetup 活动报名通知

好消息:ClickHouse Shanghai User Group第 3 届 Meetup 火热报名中,将于2026年5月16日在上海市浦东新区世纪大道1568号中建大厦33层 Optiver上海举行,扫码免费报名图片图片

Image

/END/

试用阿里云 ClickHouse企业版

轻松节省30%云资源成本?阿里云数据库ClickHouse 云原生架构全新升级,首次购买ClickHouse企业版计算和存储资源组合,首月消费不超过99.58元(包含最大16CCU+450G OSS用量)了解详情:https://t.aliyun.com/Kz5Z0q9G

图片
图片

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:[email protected]

图片图片