为每个用户的一天确定第一个 activity

Determining first activity for the day per user

我有一个 table "UserData",其中包含以下信息:

User    Date    DateTime        Input
1   8/4/2019    8/4/2019 0:55   Request
1   8/4/2019    8/4/2019 0:56   Ticket
1   8/4/2019    8/4/2019 2:08   Submit
1   8/4/2019    8/4/2019 2:21   Submit
2   8/4/2019    8/4/2019 13:10  Submit
2   8/20/2019   8/20/2019 2:10  Ticket
2   8/20/2019   8/20/2019 2:12  Ticket
2   8/20/2019   8/20/2019 2:13  Request
3   8/20/2019   8/20/2019 2:15  Request
3   8/19/2019   8/19/2019 2:16  Ticket
3   6/12/2020   6/12/2020 2:22  Submit
3   6/12/2020   6/12/2020 2:26  Submit
3   6/12/2020   6/12/2020 3:26  Ticket

我正在寻找 return 来自每个用户的第一个输入(基于时间),但仅在最近的一天。所以我的输出会是这样的:

User    DateTime    Input
1   8/4/2019 0:55   Request
2   8/20/2019 2:10  Ticket
3   6/12/2020 2:22  Submit

我想我需要为每个日期分配一个等级,但不确定从哪里开始。

具有row_number()window功能:

select t."User", t."Date", t."DateTime", t."Input"
from (
  select *, row_number() over (partition by "User" order by "Date" desc, "DateTime") rn
  from UserData
) t
where t.rn = 1

对于每个用户,该行按 "Date" 降序 排序以查找最新日期,然后按 "DateTime" 升序 获取当天的第一个输入。
demo.
结果:

| User | Date       | DateTime         | Input   |
| ---- | ---------- | ---------------- | ------- |
| 1    | 2019-08-04 | 2019-08-04 00:55 | Request |
| 2    | 2019-08-20 | 2019-08-20 02:10 | Ticket  |
| 3    | 2020-06-12 | 2020-06-12 02:22 | Submit  |

我了解您希望每个用户和每天的最早记录。在 Postgres 中,您可以简单地使用 distinct on 来解决这个 top-1-per-group 问题:

select distinct on (u.user, u.date) u.*
from userData u
order by u.user, u.date, u.datetime

如果您想要每个用户的最早记录,而不考虑日期,那么只需:

select distinct on (u.user) u.*
from userData u
order by u.user, u.datetime

编辑:如果你想在最近一天记录最早,那么:

select distinct on (u.user) u.*
from userData u
order by u.user, u.date desc, u.datetime
SELECT * 
FROM ( User,
       Date,
       Datetime, 
       Input,
       ROW_NUMBER() OVER (
           PARTITION BY User 
           ORDER BY Datetime DESC) dataOrder
       FROM UserData) z
WHERE z.dataOrder = 1

我明白你想要:

每个用户最近一天时间最早的行

Table设计

对于初学者:删除 Date 列。冗余存储会增加比其价值更多的成本和复杂性:

CREATE TABLE userdata (
  user_id  int
, datetime timestamp
, input    text
);

input 确实应该是一些廉价的枚举实现(enum,FK,...)。
timestamptz 可能是 datetime 的合适类型。要看。参见:

  • Ignoring time zones altogether in Rails and PostgreSQL

索引

无论如何,为了让您的操作更快,这是完美的索引:

CREATE INDEX userdata_special_idx ON userdata
(user_id, (datetime::date) DESC NULLS LAST, datetime);

datetime::date 是一个 非常 廉价演员表,取代了您多余的日期列。我仍然将日期添加到 multicolumn expression index 以提高性能。 (日期 取决于使用 timestamptz 时的时区。如果您处理多个时区,则需要做更多。)

请注意添加的 NULLS LAST:由于您的问题中没有任何内容表明时间戳是 NOT NULL,因此您在查询中需要它以防止无意义的结果 - 索引必须匹配以获得最佳结果.参见:

  • PostgreSQL sort by datetime asc, null first?

查询

每个用户只有几行DISTINCT ON应该是最好的选择(就像 GMB 已经建议的那样)- 简单快速:

SELECT DISTINCT ON (user_id)
       user_id, datetime, input 
FROM   userdata
ORDER  BY user_id, datetime::date DESC NULLS LAST, datetime;

参见:

  • Select first row in each GROUP BY group?

对于 每个用户的许多行,此替代查询应该(显着)更快:

SELECT u.user_id, d.*
FROM   users u
LEFT   JOIN LATERAL (
   SELECT d.datetime, d.input 
   FROM   userdata d
   WHERE  d.user_id = u.user_id         -- lateral reference
   ORDER  BY d.datetime::date DESC NULLS LAST, d.datetime
   LIMIT  1
   ) d ON true;

通常,这是适合您的场景的方法

注意 LEFT JOIN:它 returns 每个用户一行,即使 userdata 中没有条目。如果这不是您想要的,请改用 CROSS JOIN。相关:

这假设存在 users table,通常存在。如果没有,我建议您添加它(出于多种原因)。如果那不是一个选项,仍然有快速的解决方法。参见:

  • Optimize GROUP BY query to retrieve latest row per user

db<>fiddle here

旁白:我强烈建议始终使用 ISO 日期格式 (as does the manual)。区域格式取决于当前会话的设置,可能会以丑陋的方式失败。