通过选择可能为空的顶行进行 SQL 分组

Posted

技术标签:

【中文标题】通过选择可能为空的顶行进行 SQL 分组【英文标题】:SQL group by selecting top rows with possible nulls 【发布时间】:2022-01-01 14:48:30 【问题描述】:

示例表:

id name create_time group_id
1 a 2022-01-01 12:00:00 group1
2 b 2022-01-01 13:00:00 group1
3 c 2022-01-01 12:00:00 NULL
4 d 2022-01-01 13:00:00 NULL
5 e NULL group2

我需要在以下条件下按group_id 分组的前 1 行(最小的create_time):

    create_time 可以为 null - 它应该被视为最小值 group_id 可以为 null - 应返回所有可以为 null 的行 group_id 应该可以对查询应用分页(因此连接可能是个问题) 查询应尽可能通用(因此没有特定于供应商的内容)。同样,如果不可能,它应该可以在 mysql 5&8、PostgreSQL 9+ 和 H2 中运行

示例的预期输出:

id name create_time group_id
1 a 2022-01-01 12:00:00 group1
3 c 2022-01-01 12:00:00 NULL
4 d 2022-01-01 13:00:00 NULL
5 e NULL group2

我已经阅读过关于 SO 的类似问题,但 90% 的答案是使用特定关键字(PARTITION BY 的许多答案,如 https://***.com/a/6841644/5572007),其他人不尊重组条件列中的空值,并且可能分页(比如https://***.com/a/14346780/5572007)。

【问题讨论】:

窗口函数是标准sql。 您的问题是minimal reproducible example,应该包含足够的数据和预期的输出。不是每个人都是以英语为母语的人,并且可以理解您所说的意思:“1 rows with lesser create_time”。这就是为什么您应该指定所需的输出,以及您自己尝试编写此查询的原因是什么?您在哪里遇到问题? 问题已更新 前 1 行是什么意思?只有一排?您的示例输出有 4 行。 您说,如果 create_time 为 null,则应将其视为最小值。在您的示例输出中,它被视为最大值。 【参考方案1】:

您可以将两个查询与UNION ALL 结合起来。例如:

select id, name, create_time, group_id
from mytable
where group_id is not null
and not exists
(
  select null
  from mytable older
  where older.group_id = mytable.group_id
  and older.create_time < mytable.create_time  
)
union all
select id, name, create_time, group_id
from mytable
where group_id is null
order by id;

这是标准的 SQL,而且非常基础。它应该适用于几乎所有 RDBMS。

至于分页:这通常代价高昂,因为您一次又一次地运行相同的查询,以便始终选择结果的“下一个”部分,而不是只运行一次查询。最好的方法通常是使用主键进入下一部分,这样就可以使用键上的索引。在上面的查询中,我们最好将where id &gt; :last_biggest_id 添加到查询中并限制结果,在标准SQL 中为fetch next &lt;n&gt; rows only。每次运行查询时,我们都会使用上次读取的 ID 作为:last_biggest_id,因此我们从那里继续读取。

然而,变量在各种 DBMS 中的处理方式不同;最常见的是,它们前面有一个冒号、一个美元符号或一个 at 符号。标准的 fetch 子句也只有部分 DBMS 支持,而其他的则有 LIMITTOP 子句。

如果这些小差异导致无法应用它们,那么您必须找到解决方法。对于变量,这可以是一个包含最后读取的最大 ID 的单行表。对于 fetch 子句,这可能意味着您只需获取所需数量的行并停在那里。当然这并不理想,因为那时 DBMS 并不知道您只需要接下来的 n 行并且无法相应地优化执行计划。

然后可以选择不在 DBMS 中进行分页,而是将完整的结果读入您的应用程序并在那里处理分页(然后变成单纯的显示内容,当然会分配大量内存)。

【讨论】:

如果我只使用limit(不使用offset)运行此查询一次,处理它,然后再次运行以获得全新的结果,是否可以? 如前所述:这个想法是您知道您已经阅读了哪个 ID,因此使用 where id &gt; :last_biggest_id 您可以获得所有未读数据,而使用 LIMIT n 您只能获得接下来的 n 行。查询保持不变,只是 ID 绑定变量的值发生了变化。【参考方案2】:
select * from T t1
where coalesce(create_time, 0) = (
    select min(coalesce(create_time, 0)) from T t2
    where coalesce(t2.group_id, t2.id) = coalesce(t1.group_id, t1.id)
)

不确定您认为“分页”应该如何工作。这是一种方法:

and (
    select count(distinct coalesce(t2.group_id, t2.id)) from T t2
    where coalesce(t2.group_id, t2.id) <= coalesce(t1.group_id, t1.id)
) between 2 and 5 /* for example */
order by coalesce(t1.group_id, t1.id)

我假设有一个从 0 到日期值的隐式转换,其结果值低于数据库中的所有值。不确定这是否可靠。 (改用'19000101'?)否则其余的应该是通用的。您可能还可以使用与页面范围相同的方式对其进行参数化。

group_idid 空格之间的潜在冲突也可能会导致并发症。尽管混合数据类型会产生自己的问题,但您的似乎没有这个问题。

当您想按name 等其他列排序时,这一切都变得更加困难:

select * from T t1
where coalesce(create_time, 0) = (
    select min(coalesce(create_time, 0)) from T t2
    where coalesce(t2.group_id, t2.id) = coalesce(t1.group_id, t1.id)
) and (
    select count(*) from (
        select * from T t1
        where coalesce(create_time, 0) = (
            select min(coalesce(create_time, 0)) from T t2
            where coalesce(t2.group_id, t2.id) = coalesce(t1.group_id, t1.id)
        )
    ) t3
    where t3.name < t1.name or t3.name = t1.name
        and coalesce(t3.group_id, t3.id) <= coalesce(t1.group_id, t1.id)
) between 2 and 5
order by t1.name;

这确实处理了关系,但也做出了name 不能为空的简化假设,这将增加另一个小转折。至少您可以看到没有 CTE 和窗口函数是可能的,但预计它们的运行效率也会低很多。

https://dbfiddle.uk/?rdbms=mysql_5.5&fiddle=9697fd274e73f4fa7c1a3a48d2c78691

【讨论】:

【参考方案3】:

我猜

SELECT id, name, MAX(create_time), group_id
FROM tb GROUP BY group_id 
UNION ALL
SELECT id, name, create_time, group_id
FROM tb WHERE group_id IS NULL
ORDER BY name

我应该指出'name'是一个保留字。

【讨论】:

以上是关于通过选择可能为空的顶行进行 SQL 分组的主要内容,如果未能解决你的问题,请参考以下文章

通过选择不为空的特定值在 pyspark 中创建一个新列

oracle 我有1000条查询语句,通过执行每一条sql语句,返回所有的查询结果为空的记录,能实现吗?

mysql 如何按月分组查询出当前年度每个月的短信数量(数据库中这个月要是为空的话就用0条怎么显示出来)

MyBatis的动态sql语句

sql server 使用外连接结果转换成表格式

Oracle/SQL - 在另一个表中查找或为空或可能不存在或为空的记录