如何在一次选择中获得当前和下一个更大的值?


18

我有一个带有列的InnoDB表'idtimes'(MySQL 5.0.22-log)

`id` int(11) NOT NULL,
`time` int(20) NOT NULL, [...]

用复合唯一键

UNIQUE KEY `id_time` (`id`,`time`)

因此每个ID可以有多个时间戳,每个时间戳可以有多个ID。

我正在尝试建立一个查询,以获取所有条目以及每个条目的下一个较大时间(如果存在),因此它应返回例如:

+-----+------------+------------+
| id  | time       | nexttime   |
+-----+------------+------------+
| 155 | 1300000000 | 1311111111 |
| 155 | 1311111111 | 1322222222 |
| 155 | 1322222222 |       NULL |
| 156 | 1312345678 | 1318765432 |
| 156 | 1318765432 |       NULL |
+-----+------------+------------+

现在,我到目前为止:

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id
    WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

但这当然会返回r.time> l.time的所有行,而不仅是第一个...

我想我需要像

SELECT outer.id, outer.time, 
    (SELECT time FROM idtimes WHERE id = outer.id AND time > outer.time 
        ORDER BY time ASC LIMIT 1)
    FROM idtimes AS outer ORDER BY outer.id ASC, outer.time ASC;

但是我不知道如何引用当前时间(我知道上面的SQL无效)。

如何使用单个查询做到这一点(并且我不希望使用不依赖于一次遍历表并记住最后一个值的@variables)?

Answers:


20

进行JOIN是您可能需要做的一件事。

SELECT l.id, l.time, r.time FROM 
    idtimes AS l LEFT JOIN idtimes AS r ON l.id = r.id

我想外部联接是有意的,并且您想获取空值。以后再说。

WHERE l.time < r.time ORDER BY l.id ASC, l.time ASC;

您只想要r。(MIN)时间比l.time高的最低行。那是您需要子查询的地方。

WHERE r.time = (SELECT MIN(time) FROM idtimes r2 where r2.id = l.id AND r2.time > l.time)

现在为空。如果“没有下一个更高的时间”,那么SELECT MIN()的计算结果将为null(或更糟),并且它本身绝不会等于任何东西,因此您的WHERE子句将永远不会得到满足,因此“最高时间”对于每个ID,永远不会出现在结果集中。

您可以通过消除JOIN并将标量子查询移到SELECT列表中来解决此问题:

SELECT id, time, 
    (SELECT MIN(time) FROM idtimes sub 
        WHERE sub.id = main.id AND sub.time > main.time) as nxttime
  FROM idtimes AS main 

4

我总是避免SELECT块中或块中使用子查询FROM,因为它会使代码“肮脏”,有时效率较低。

我认为更优雅的方法是:

1.查找时间大于时间的行

您可以使用一个具有自己的idtimesJOIN之间的表来执行此操作,将联接限制为相同的id,并且约束时间要大于当前行的时间

您应该LEFT JOIN避免使用除时间不大于当前行之一的行。

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS greater_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time

正如您提到的,问题是您有多行,其中next_time大于time

+-----+------------+--------------+
| id  | time       | greater_time |
+-----+------------+--------------+
| 155 | 1300000000 | 1311111111   |
| 155 | 1300000000 | 1322222222   |
| 155 | 1311111111 | 1322222222   |
| 155 | 1322222222 |       NULL   |
| 156 | 1312345678 | 1318765432   |
| 156 | 1318765432 |       NULL   |
+-----+------------+--------------+

2.找到行,其中greater_time不仅更高,但是:NEXT_TIME

过滤所有这些无用行的最好的办法是找出是否有时间之间的时间(大于)和greater_time(比较小),此ID

SELECT
    i1.id,
    i1.time AS time,
    i2.time AS next_time,
    i3.time AS intrudor_time
FROM
    idtimes AS i1
    LEFT JOIN idtimes AS i2 ON i1.id = i2.id AND i2.time > i1.time
    LEFT JOIN idtimes AS i3 ON i2.id = i3.id AND i3.time > i1.time AND i3.time < i2.time

行动,我们还有一个错误的next_time

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1300000000 | 1322222222   |    1311111111 |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

只需过滤此事件发生的行,在WHERE下面添加约束

WHERE
    i3.time IS NULL

Voilà,我们有我们需要的!

+-----+------------+--------------+---------------+
| id  | time       | next_time    | intrudor_time |
+-----+------------+--------------+---------------+
| 155 | 1300000000 | 1311111111   |         NULL  |
| 155 | 1311111111 | 1322222222   |         NULL  |
| 155 | 1322222222 |       NULL   |         NULL  |
| 156 | 1312345678 | 1318765432   |         NULL  |
| 156 | 1318765432 |       NULL   |         NULL  |
+-----+------------+--------------+---------------+

希望4年后您仍然需要答案!


那很聪明。我不确定这是否更容易理解。我认为,如果我们用替换is nulli和i3的联接where not exists (select 1 from itimes i3 where [same clause]),那么代码将更紧密地反映我们要表达的内容。
安德鲁·斯宾塞

兄弟,你救了我(第二天)!
雅各布

2

在介绍解决方案之前,我应该注意它并不漂亮。如果AUTO_INCREMENT表上有一些列,会容易得多(对吗?)

SELECT 
  l.id, l.time, 
  SUBSTRING_INDEX(GROUP_CONCAT(r.time ORDER BY r.time), ',', 1)
FROM 
  idtimes AS l 
  LEFT JOIN idtimes AS r ON (l.id = r.id)
WHERE 
  l.time < r.time
GROUP BY
  l.id, l.time

说明:

  • 与您的联接相同:联接两个表,正确的一个只会获得更高的时间
  • GROUP BY左表中的两列:这确保我们得到所有(id, time)组合(也称为唯一组合)。
  • 对于每一个(l.id, l.time),获得大于的第一个 。这是通过先对s via进行排序,通过对first令牌via进行切片来实现的。r.timel.timer.timeGROUP_CONCAT(r.time ORDER BY r.time)SUBSTRING_INDEX

祝您好运,如果这张桌子很大,不要指望有好表现。


2

您也可以从中获得想要的内容min()GROUP BY并且没有内部选择:

SELECT l.id, l.time, min(r.time) 
FROM idtimes l 
LEFT JOIN idtimes r on (r.id = l.id and r.time > l.time)
GROUP BY l.id, l.time;

几乎可以打赌,优化器无论如何都会将它变成与Erwin Smout的答案相同的东西,是否更清晰尚待商,,但这是为了完整性...


1
值得一提的是,SSMS&SQLServer 2016比Erwin的查询更喜欢您的查询(在24k结果集上2s运行时对比24s运行时)
Nathan Lafferty

安德鲁(Andrew)似乎打赌输了:-)
欧文·斯莫特

有趣的是,因为通常情况下,通过PK列之一联接回外部查询表的子查询与group by相同。我想知道是否还有其他数据库可以更好地对其进行优化。(我对数据库优化器BTW知之甚少;只是好奇。)
Andrew Spencer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.