在 SQL 中处理日期
使用 SQL 按时间段对结果进行分组,比较周与周的总数,并查找两个日期之间的持续时间。
我们将介绍在 SQL 中处理日期时的三种常见场景。我们将使用 Metabase 附带的示例数据库,以便您可以跟随操作,并坚持一些在许多数据库中通用的常用 SQL 函数和技术。我们假设这不是您的第一个 SQL 查询,并且您希望升级。但即使您刚开始,您也应该能够掌握一些技巧。
场景 | 示例 |
---|---|
按时间段对结果进行分组 | 每周有多少人创建了帐户? |
比较周与周的总数 | 本周的订单数量与上周相比如何? |
查找两个日期之间的持续时间 | 客户创建帐户和下第一个订单之间有多少天? |
按时间段对结果进行分组
我们经常想问这样的问题:每个月有多少客户注册?或者每周下了多少订单?在这里,我们将浏览结果表,计算行数,并按时间段对这些计数进行分组。
示例:每周有多少人创建了帐户?
在这里,我们希望返回两列
| WEEK | ACCOUNTS CREATED |
|------|------------------|
| ... | ... |
让我们看一下我们的 People
表。我们可以 SELECT * FROM people LIMIT 1
查看字段列表,或者只需单击书本图标即可查看有关我们正在使用的数据库中表的元数据。
由于我们对客户何时注册帐户感兴趣,因此我们将需要 created_at
字段,根据我们的数据参考,该字段是“用户记录创建的日期。也称为用户的‘加入日期’”。
我们需要对这些帐户创建进行分组,但不是按日期分组,而是需要按周分组。要查看每个 created_at
日期属于哪一周,我们将使用 DATE_TRUNC
函数。
DATE_TRUNC
允许您将时间戳舍入(“截断”)为您关心的粒度:周、月等等。DATE_TRUNC
接受两个参数:文本和时间戳,并返回时间戳。第一个文本参数是时间段,在本例中为“周”,但我们可以指定不同的粒度,如月、季度或年(查看数据库关于 DATE_TRUNC
的文档以查看选项)。对于我们此处的目的,我们将编写 DATE_TRUNC('week', created_at)
,这将返回每周星期一的日期。顺便说一句,SQL 不区分大小写,因此您可以随意区分代码的大小写(date_trunc
也有效,或者 DaTe_TrUnc
如果您是讽刺地查询)。
我们还将为结果使用别名,以便为列提供更具体的名称。例如,使用 AS
关键字,我们将 Count(*)
更改为显示为 accounts_created
。
SELECT
DATE_TRUNC('week', created_at) AS week,
COUNT(*) AS accounts_created
FROM
people
GROUP BY
week
ORDER BY
week
返回结果
| WEEK | ACCOUNTS_CREATED |
|---------|------------------|
| 4/18/16 | 13 |
| 4/25/16 | 17 |
| 5/2/16 | 17 |
| ... | ... |
我们可以将此结果可视化为折线图
这看起来与我们从随机数据集中预期的几乎一样。
比较周与周的总数
您通常希望查看计数从一周到下一周的变化,这可以通过将表自身连接起来,并将每周与其前一周进行比较来计算。
示例:订单与上周相比如何?
我们在这里寻找的是周、该周的订单计数以及周与周的变化(订单是上升、下降还是保持不变?)
| WEEK | COUNT_OF_ORDERS | WOW_CHANGE |
|---------|-----------------|------------|
| ... | ... | ... |
要获取此数据,我们首先需要获取一个列出每周订单计数的表。我们将执行与我们刚刚为 People
表所做的基本相同的事情,但这次是为 Orders
表:我们将使用 DATE_TRUNC
按周对订单计数进行分组。
SELECT
DATE_TRUNC('week', orders.created_at) AS week,
COUNT(*) AS order_count
FROM
orders
GROUP BY
week
这给了我们
| WEEK | ORDER_COUNT |
|----------|-------------|
| 7/1/2019 | 115 |
| 7/2/2018 | 119 |
| 7/3/2017 | 78 |
| ... | ... |
我们将使用这些结果来构建查询的其余部分。我们现在需要做的是从每周的订单计数(我们将其称为 w1
)中减去前一周的计数(我们将其称为 w2
)。这里的挑战是,为了执行减法,我们需要以某种方式将每周的计数与前一周的计数放在同一行中。
以下是我们如何做到这一点
- 将我们的结果包装在公用表表达式 (CTE) 中。
- 通过将连接偏移 1 周,将该 CTE 连接到自身
- 从每周的总数中减去前一周的订单计数总数,以获得周与周的变化
我们将使用 WITH
关键字将上面的查询设为公用表表达式 (CTE)。本质上,CTE 是一种为临时结果分配变量的方法,然后我们可以将这些结果视为数据库中的实际表(如 Orders
或 Table
)。我们将结果表称为 order_count_by_week
。然后我们将使用此表并将其连接到自身,但带有偏移量:其行偏移一周。
以下是带有偏移连接的查询
WITH order_count_by_week AS (
SELECT
DATE_TRUNC('week', orders.created_at) AS week,
COUNT(*) AS order_count
FROM
orders
GROUP BY
week
)
SELECT
*
FROM
order_count_by_week w1
LEFT JOIN order_count_by_week w2 ON w1.week = DATEADD(WEEK, 1, w2.week)
ORDER BY
w1.week
此查询产生
| WEEK | ORDER_COUNT | WEEK | ORDER_COUNT |
|-----------|-------------|-----------|-------------|
| 4/25/2016 | 1 | | |
| 5/2/2016 | 3 | 4/25/2016 | 1 |
| 5/9/2016 | 3 | 5/2/2016 | 3 |
| ... | ... | ... | ... |
让我们解开这里发生的事情。我们将 order_count_by_week
CTE 别名为 w1
,然后再次别名为 w2
。接下来,我们左连接了这两个 CTE。这里的关键是 DATEADD
函数,我们使用该函数向每个 w2.week
值添加一周以偏移连接的列
LEFT JOIN order_count_by_week w2 ON w1.week = DATEADD(WEEK, 1, w2.week)
DATEADD
函数采用一段时间 (WEEK)、要应用的周数(在本例中为 1,因为我们想知道与一周前的差异)以及要应用加法的日期列 (w2.week
)。(请注意,某些数据库使用 INTERVAL
而不是 DATEADD
,例如 w2.week + INTERVAL '1 week'
)。这“对齐”了行,但偏移了一周(请注意,上面第一行中第二组周/订单计数中缺少值)。
我们现在有一个表,其中包含计算每行周与周变化所需的一切。现在我们所要做的就是修改我们的 select 语句以返回我们正在查找的列
- 下订单的周
- 该周的订单计数
- 周与周的变化(即,本周计数与前一周计数之间的差异)。
这是完整的查询
WITH order_count_by_week AS (
SELECT
DATE_TRUNC('week', orders.created_at) AS week,
COUNT(*) AS order_count
FROM
orders
GROUP BY
week
)
SELECT
w1.week,
w1.order_count AS count_of_orders,
w1.order_count - w2.order_count AS wow_change
FROM
order_count_by_week w1
LEFT JOIN order_count_by_week w2 ON w1.week = DATEADD(WEEK, 1, w2.week)
ORDER BY
w1.week
返回结果
| WEEK | COUNT_OF_ORDERS | WOW_CHANGE |
|---------|-----------------|------------|
| 4/25/16 | 1 | |
| 5/2/16 | 3 | 2 |
| 5/9/16 | 3 | 0 |
| ... | ... | ... |
查找两个日期之间的持续时间
您通常希望找到两个事件之间的时间量:注册和结帐之间的时间(以秒为单位),或结帐和交付之间的时间(以天为单位)。
示例:客户创建帐户和下第一个订单之间有多少天?
为了回答这个问题,让我们返回四列
- 客户的 ID
- 客户创建帐户的日期
- 客户下第一个订单的日期
- 这两个日期之间的差异
现在,要获取此信息,我们需要从 People
和 Orders
表中获取数据。但我们不想连接这两个表,因为我们只需要每个客户下的第一个订单。
让我们首先找出每个客户何时下了他们的第一个订单。
SELECT
user_id,
MIN(created_at) as first_order_date
FROM
orders
GROUP BY
user_id
在这里,我们按客户对订单进行分组(GROUP BY user_id
),并使用 MIN
函数查找最早的订单日期。我们将这些结果存储为 first_orders
,然后继续我们的查询。
WITH first_orders AS (
SELECT
user_id,
MIN(created_at) as first_order_date
FROM
orders
GROUP BY
user_id
)
SELECT
people.id,
people.created_at AS account_creation,
first_orders.first_order_date,
DATEDIFF(
'day', people.created_at, first_orders.first_order_date
) AS days_before_first_order
FROM
PEOPLE
JOIN first_orders ON first_orders.user_id = people.id
ORDER BY
account_creation
这给了我们
| ID | ACCOUNT_CREATION | FIRST_ORDER_DATE | DAYS_BEFORE_FIRST_ORDER |
|------|------------------|------------------|-------------------------|
| 915 | 4/19/16 21:35 | 10/9/16 8:42 | 173 |
| 1712 | 4/21/16 23:46 | 8/15/16 4:01 | 116 |
| 2379 | 4/22/16 4:07 | 5/22/16 3:56 | 30 |
| ... | ... | ... | ... |
总结一下:我们抓取了客户的 created_at
日期,并将查询连接到我们的 CTE。我们使用 DATEDIFF
函数查找帐户创建与其第一个订单之间的天数,然后将结果存储为 days_before_first_order
。DATEDIFF
采用时间段(如“天”、“周”、“月”),并返回两个时间戳之间的时间段数。
(鉴于示例数据库是随机的,我们的响应与现实不太匹配——人们在帐户设置和购买之间等待 173 天的情况有多常见?)
进一步阅读
我们希望这些查询演练为您自己的问题提供了一些想法,但请记住,不同的数据库支持不同的 SQL 函数,因此在处理查询时,请养成查阅数据库文档的习惯。您还可以查看编写 SQL 查询的最佳实践。如果您对连接的工作原理有点模糊,请查看Metabase 中的连接。
下一步:使用公用表表达式 (CTE) 简化复杂查询
CTE 是命名的结果集,有助于保持代码的组织性。它们允许您在同一查询中重用结果,并执行多级聚合。