2 列上的间隙和孤岛 - 如果 A 列连续且 B 列相同

Gaps and islands on 2 columns - if column A consecutive and column B identical

我有一个table如下:

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

虽然这不起作用,但发生的事情似乎是 return 名称的第一次出现是 first,最后一次出现是 lastno_records是数量上的差异,肯定是不对的

我感觉像 this question might be related,但我没能理解它,当我尝试将它调整到我的 table 时,它只是做了一个简单的等价物SELECT * 或多或少。我需要对我的查询进行哪些修改才能使其正常工作?

切记:

您的示例不是间隙和孤岛问题。如果它代表您的实际问题,您可以使用聚合:

select min(number), max(number), count(*), name
from t
group by name;

我这样说是因为没有 window 函数,间隙和孤岛更具挑战性。这就引出了一个问题,即您为什么不使用更新版本的 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 是一个 db<>fiddle.

您的查询没有太大变化。您基本上需要在子查询中 select namenumber 并按相同顺序排序。然后你可以在外部查询中按 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 子句中用 first_number 替换了 number 以避免 ONLY_FULL_GROUP_BY 模式出现问题。

一种更 stable 的生成行号的方法是在临时 table:

中使用 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() window 函数代替:

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