2 列上的间隙和孤岛 - 如果 A 列连续且 B 列相同
Posted
技术标签:
【中文标题】2 列上的间隙和孤岛 - 如果 A 列连续且 B 列相同【英文标题】:Gaps and islands on 2 columns - if column A consecutive and column B identical 【发布时间】:2020-06-09 14:28:19 【问题描述】:我有一张如下表:
CREATE TABLE `table` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`cc` int(3) unsigned NOT NULL,
`number` int(10) NOT NULL,
`name` varchar(64) NOT NULL,
`datetime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DBMS 是 Debian 9.1 上的 MariaDB 10.1.26。我一直试图让它列出连续数字的范围。通过以下查询,我能够做到这一点:
SELECT min(number) first_number, max(number) last_number, count(*) AS no_records FROM (
SELECT c.*, @rn := @rn + 1 rn
from (SELECT number FROM table WHERE cc = 1 GROUP BY number ORDER BY number) AS c
CROSS JOIN (SELECT @rn := 0) r
) c
GROUP BY number - rn ORDER BY number ASC
但是,如果我希望根据附加列中的值将项目组合在一起,则此方法行不通。假设我希望仅当 name
的值都相同时才对项目进行分组。说这是我的数据:
INSERT INTO `table` (`id`, `cc`, `number`, `name`) VALUES
(1, 1, 12, 'Hello'),
(2, 1, 2, 'Apple'),
(3, 1, 3, 'Bean'),
(4, 1, 10, 'Hello'),
(5, 1, 11, 'Hello'),
(6, 1, 1, 'Apple'),
(7, 1, 14, 'Deer'),
(8, 1, 14, 'Door'),
(9, 1, 15, 'Hello'),
(10, 1, 17, 'Hello'),
我想得到这样的报告:
first last count name
1 2 2 Apple
3 3 1 Bean
10 12 3 Hello
14 14 1 Deer
14 14 1 Door
15 15 1 Hello
17 17 1 Hello
换句话说,除了对连续的项目进行分组之外,当它们的name
值不同时,这些组还会被分成不同的组。 (换句话说,如果项目都是连续的并且具有完全相同的name
,则项目仅在一个岛中。)我来的最近(而且不是很近)是这样做的:
SELECT min(number) first_number, max(number) last_number, count(*) AS no_records FROM (
SELECT c.*, @rn := @rn + 1 rn
from (SELECT number FROM table WHERE cc = 1 GROUP BY number, name ORDER BY number) AS c
CROSS JOIN (SELECT @rn := 0) r
) c
GROUP BY number - rn, name ORDER BY number ASC
不过,这不起作用,发生的情况是它似乎返回名称的第一次出现为first
,最后一次出现为last
,no_records
是它们之间的数量差异,这当然是不对的。
我感觉像this question might be related,但我无法理解它,当我尝试将它调整到我的桌子上时,它或多或少地相当于一个简单的SELECT *
。我需要对我的查询进行哪些修改才能使其正常工作?
记住:
项目可以按任何顺序插入 数字可以重复 名称可以重复,不一定连续【问题讨论】:
@GordonLinoff 我可能打错了一些东西,但 Apple 适用于 1 和 2,Hello 适用于 10 到 12。它们是如何“组合”的?请注意,它基于number
,而不是id
。我故意混淆了一些 INSERT 的顺序。
。 .我误解了数据。这相当令人困惑,因为原始数据的排序方式与结果集不同。我想我现在明白了。
@GordonLinoff 是的,我是故意这样做的,因为我不知道某些查询功能(如行变量)是否按id
ASC 顺序通过,所以我这样做只是为了说明它应该无论如何工作。
【参考方案1】:
您的查询没有太大变化。您基本上需要在子查询中选择name
和number
,并按相同的顺序进行排序。然后您可以在外部查询中按name, number - rn
分组。
SELECT
min(number) first_number,
max(number) last_number,
count(*) AS no_records,
name
FROM (
SELECT c.*, @rn := @rn + 1 rn
from (
SELECT name, number
FROM `table`
WHERE cc = 1
ORDER BY name, number
LIMIT 99999999999999999
) AS c
CROSS JOIN (SELECT @rn := 0) r
) c
GROUP BY name, number - rn
ORDER BY first_number ASC, name ASC;
结果:
first_number last_number no_records name
1 2 2 Apple
3 3 1 Bean
10 12 3 Hello
14 14 1 Deer
14 14 1 Door
15 15 1 Hello
17 17 1 Hello
db<>fiddle
我通常反对以这种方式使用会话变量。原因是此类解决方案依赖于内部实现,并且可能被版本更新或设置更改破坏。例如:一旦 MariaDB 决定在没有 LIMIT 的情况下忽略子查询中的 ORDER BY 子句。这就是为什么我包含了一个巨大的 LIMIT。
我还在外部 ORDER BY 子句中将 number
替换为 first_number
以避免 ONLY_FULL_GROUP_BY 模式出现问题。
一种更稳定的生成行号的方法是在临时表中使用 AOTO_INCREMENT 列:
drop temporary table if exists tmp_tbl;
create temporary table tmp_tbl (
rn int unsigned auto_increment primary key,
name varchar(64) not null,
number int not null
);
insert into tmp_tbl (name, number)
select name, number
from `table`
order by name, number;
最终的 SELECT 查询与上面的外部查询相同:
SELECT
min(number) first_number,
max(number) last_number,
count(*) AS no_records,
name
FROM tmp_tbl
GROUP BY name, number - rn
ORDER BY first_number ASC, name ASC;
db<>fiddle
在更新的版本中(从 MariaDB 10.2 开始),您可以改用 ROW_NUMBER()
窗口函数:
SELECT
min(number) first_number,
max(number) last_number,
count(*) AS no_records,
name
FROM (
SELECT
name,
number,
row_number() OVER (ORDER BY name, number) as rn
FROM `table`
WHERE cc = 1
) c
GROUP BY name, number - rn
ORDER BY first_number ASC, name ASC;
db<>fiddle
【讨论】:
【参考方案2】:您的示例不是孤岛问题。如果它代表您的实际问题,您可以使用聚合:
select min(number), max(number), count(*), name
from t
group by name;
我这样说是因为如果没有窗口函数,gaps-and-islands 会更具挑战性。这就引出了一个问题,即为什么您不使用更新版本的 MariaDB。无论如何,10.1 的生命周期结束时间是今年 10 月。
编辑:
作为一个间隙和岛屿,这有点棘手,因为每个名称都必须单独处理。诀窍是使用row_number()
进行分区:
select name, min(number), max(number), count(*)
from (select t.*,
row_number() over (partition by name order by number) as seqnum
from t
) t
group by name, (number - seqnum);
如果名称有相邻的数字并减去一个连续的值,则结果是恒定的。例如:
Name Number Seq Diff
Hello 10 1 9
Hello 11 2 9
Hello 12 3 9
Hello 15 4 11
diff
标识要聚合的组。
糟糕,我忘了这是针对即将过时的 MariaDB 版本:
select name, min(number), max(number), count(*)
from (select t.*,
(select count(*)
from `table` t2
where t2.name = t.name and t2.number <= t.number
) as seqnum
from `table` t
) t
group by name, (number - seqnum);
为了提高性能,您需要在(name, number)
上建立索引。性能应该是合理的,除非名称的行数超过几百行。
Here 是一个 dbfiddle。
【讨论】:
我刚刚运行了这个查询。就像我尝试过的其他事情一样,它失败了。为什么你说这不是缝隙和孤岛问题?返回的每个“记录”都应该是number
s 的范围,它们是连续的并且具有相同的名称。如果你去掉name
部分,这是一个 1 列的间隙和孤岛问题,不是吗?
@InterLinked 。 . .在您的示例数据中,name
s 仅出现在具有相邻值的相邻行上。
公平点 - 已更新问题以包含该问题。名字可以出现在任何地方。在此视图中,我不希望名称聚集在一起,除非它们再次位于连续范围内。我有一个不同的观点,它比你提出的要简单得多,如果我不关心数字,那么我只需按名称分组,然后按 COUNT(*)。但在这种情况下,我想要数字,所以它有点复杂
这似乎可行,但我仍在努力获得它。无论我尝试什么,查询都不会执行,只是永远说“正在加载”。我正在添加一个数字索引,但它也表示“正在加载”,直到它超时。不建议在基于字符串的列上添加索引吗? (例如 VARCHAR)?现在,该表只有 400,000 条记录,我想知道当它有 8000 万条记录时这将如何工作......
好的,我基本上为除日期时间之外的所有列添加了索引。尽管如此,查询还没有完成执行。我仍在尝试将其缩小到数据的一小部分以上是关于2 列上的间隙和孤岛 - 如果 A 列连续且 B 列相同的主要内容,如果未能解决你的问题,请参考以下文章
使用间隙和孤岛查找连续的时间/日期 - SQL/BigQuery