通过选择可能为空的顶行进行 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 > :last_biggest_id
添加到查询中并限制结果,在标准SQL 中为fetch next <n> rows only
。每次运行查询时,我们都会使用上次读取的 ID 作为:last_biggest_id
,因此我们从那里继续读取。
然而,变量在各种 DBMS 中的处理方式不同;最常见的是,它们前面有一个冒号、一个美元符号或一个 at 符号。标准的 fetch 子句也只有部分 DBMS 支持,而其他的则有 LIMIT
或 TOP
子句。
如果这些小差异导致无法应用它们,那么您必须找到解决方法。对于变量,这可以是一个包含最后读取的最大 ID 的单行表。对于 fetch 子句,这可能意味着您只需获取所需数量的行并停在那里。当然这并不理想,因为那时 DBMS 并不知道您只需要接下来的 n 行并且无法相应地优化执行计划。
然后可以选择不在 DBMS 中进行分页,而是将完整的结果读入您的应用程序并在那里处理分页(然后变成单纯的显示内容,当然会分配大量内存)。
【讨论】:
如果我只使用limit
(不使用offset
)运行此查询一次,处理它,然后再次运行以获得全新的结果,是否可以?
如前所述:这个想法是您知道您已经阅读了哪个 ID,因此使用 where id > :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_id
和 id
空格之间的潜在冲突也可能会导致并发症。尽管混合数据类型会产生自己的问题,但您的似乎没有这个问题。
当您想按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 分组的主要内容,如果未能解决你的问题,请参考以下文章
oracle 我有1000条查询语句,通过执行每一条sql语句,返回所有的查询结果为空的记录,能实现吗?