【问题标题】:Cursor Pagination (prev / next) with null values具有空值的光标分页(上一个/下一个)
【发布时间】:2021-10-28 11:20:52
【问题描述】:

我有一个用 MySQL(8.0 版)实现的游标分页,只要不涉及 null 值,它就可以正常工作。

这是我的示例数据(id 是随机 UUID,date 是日期,time 是时间):

id | date       | time
--------------------------
68 | 2017-10-28 | 22:00:00
d3 | 2017-11-03 | null
dd | 2017-11-03 | 21:45:00
62 | 2017-11-04 | 14:00:00
a1 | 2017-11-04 | 19:40:00

我使用的cursor 总是包含所有三列。

我使用这个查询来获得下一个结果(在cursor之后):

SELECT * FROM table
WHERE (date > cursor.date)
    OR (date = cursor.date AND time > cursor.time)
    OR (date = cursor.date AND time = cursor.time AND id > cursor.id)
ORDER BY date ASC, time ASC, id ASC

这个查询prev结果(在cursor之前):

SELECT * FROM table
WHERE (date < cursor.date)
    OR (date = cursor.date AND time < cursor.time)
    OR (date = cursor.date AND time = cursor.time AND id < cursor.id)
ORDER BY date DESC, time DESC, id DESC

当使用带有cursor [id = dd, date = 2017-11-03, time = 21:45:00]prev 查询时,它不会返回带有id = d3 的行,因为timenull,而这不会被@987654335 选中@。

虽然我尝试使用time &lt; cursor.time OR time IS NULL 而不是time &lt; cursor.time 来包含具有null 值的行。这似乎解决了这个特殊问题,但随后又产生了一个新问题:当使用带有cursor [id = d3, date = 2017-11-03, time = null]prev 查询时,因为现在结果包含所提供游标的行。

我希望有一个简单的解决方案。网络上似乎没有处理光标分页中null 值的示例或教程。

注意:对于解决方案,null 是否在 non-null 值之前或之后排序并不重要,只要它是一致的。 (MySQL的默认排序是null &lt; non-null

【问题讨论】:

  • 请注明您使用的 MySQL 版本。
  • @KaziMohammadAliNur 我正在运行 MySQL 8.0.26
  • 感谢@Benjamin M 如此快速的回复。我试图用 cte 和 row_number() 找到解决方案。
  • 如果我的评论很幼稚,我很抱歉,但是在您的查询中不能使用time &lt; cursor.time OR time IS NULL OR cursor.time IS NULL 吗?我对游标不太满意,但我不知道为什么它不起作用。
  • @Christophe 无需抱歉。我们都在这里学习新事物。你的想法行不通:正如我在问题中所说,我尝试使用已经选择了太多行的time &lt; cursor.time OR time IS NULL。当您现在添加另一个 OR 条件时,您不会缩小结果范围,而是可能选择更多行。

标签: mysql sql pagination


【解决方案1】:

向表中添加另一列。将其设为DATETIME。不为NULL时将datetime合并到其中;将date 与NULL 时的某个特定时间结合起来。然后你的光标有两列可以使用,并且没有空值。

如果您有相当新的 MySQL 版本,则可以使用“生成的存储”列,从而避免任何代码更改。

并且一定要有INDEX(datetime, id)

【讨论】:

  • "generated virtual" 会导致与COALESCE 解决方案相同的问题,因为值是在读取时动态计算的。我猜你的意思是“生成的存储的”,它计算并存储INSERTUPDATE 上的值。此外,添加生成的存储列将触发表重建。仍在寻找仅查询的解决方案。但到目前为止,这是我的首选解决方案。
【解决方案2】:

我不会涉及使用光标进行分页的话题。还有其他选择,比如limit/offset

但我对您的查询的建议是使用coalesce(),为比较分配一个假时间。 MySQL 使这有点简单,因为它支持超过 24 小时的 time 值。对于date/time 组合,这些值不是有效值。

所以:

SELECT *
FROM table
WHERE (date > cursor.date) OR
      (date = cursor.date AND COALESCE(time, '24:00:00') > COALESCE(cursor.time, '24:00:00')) OR
      (date = cursor.date AND COALESCE(time, '24:00:00') = COALESCE(cursor.time, '24:00:00') AND id > cursor.id)
ORDER BY date ASC, time ASC, id ASC

更简洁的WHERE 子句是:

WHERE (date, COALESCE(time, '24:00:00'), id) > (cursor.date, COALESCE(cursor.time, '24:00:00'), cursor.id)

【讨论】:

  • COALESCE 看起来不错并且可以工作,但遗憾的是它违背了光标分页的目的——即(除其他外)通过最小化结果集来加速非常大的OFFSET 的分页使用带有索引值的 WHERE 子句。 COALESCE 不允许使用索引,因为这些值是动态生成的。尽管如此,这对于使用小表格进行光标分页可能是一个有用的答案。
  • @BenjaminM。 . .我可能会建议使用'24:00:00'(或其他值)更新NULL 次以加快此类查询。
  • 这意味着要重构大量代码。作为最后的手段,我会记住这一点
【解决方案3】:

如果您使用的是 MySQL 8.0,那么您可以考虑使用 row_number() 窗口函数为每一行创建一个唯一的顺序 id (rn)。然后只需将rn 传递给当前行即可获取前面的行。

架构和插入语句:

 create table cursortable( id varchar(10), date date, time time);

 insert into cursortable values('68' , '2017-10-28' , '22:00:00');
 insert into cursortable values('d3' , '2017-11-03' ,  null);
 insert into cursortable values('dd' , '2017-11-03' , '21:45:00');
 insert into cursortable values('62' , '2017-11-04' , '14:00:00');
 insert into cursortable values('a1' , '2017-11-04' , '19:40:00');

第一次查询结果:

 select *,row_number()over(order by date,time,id)rn from cursortable

输出:

id date time rn
68 2017-10-28 22:00:00 1
d3 2017-11-03 null 2
dd 2017-11-03 21:45:00 3
62 2017-11-04 14:00:00 4
a1 2017-11-04 19:40:00 5

查询以获取 cursor [id = dd, date = 2017-11-03, time = 21:45:00, rn=3] 的前几行,仅包含 cursor [rn=3]

 with cte as
 (
   select *,row_number()over(order by date,time,id)rn from cursortable
 )
 select * from cte where rn<3

输出:

id date time rn
68 2017-10-28 22:00:00 1
d3 2017-11-03 null 2

db小提琴here

如果您不想在代码中引入计算列,请尝试以下解决方案,考虑所有三列cursor [id = dd, date = 2017-11-03, time = 21:45:00]

查询:

with cte as
 (
   select *,row_number()over(order by date,time,id)rn from cursortable
 )
 ,cte2 as
 (
   select * from cte where id='dd' and date=  '2017-11-03' and time= '21:45:00'
 )
 select cte.id,cte.date,cte.time from cte inner join cte2 on cte.rn<cte2.rn

输出:

id date time
68 2017-10-28 22:00:00
d3 2017-11-03 null

db小提琴here

您的代码如下所示:

with cte as
 (
   select *,row_number()over(order by date,time,id)rn from cursortable
 )
 ,cte2 as
 (
   select * from cte where id=cursor.id and date=  cursor.date and time= cursor.time
 )
 select cte.id,cte.date,cte.time from cte inner join cte2 on cte.rn<cte2.rn

【讨论】:

  • 感谢您的回答!一些我从未听说过的新关键字和功能。我看看我能不能弄明白。至少还有两个额外的问题需要解决:1. 当使用游标分页时,您希望使用 db 索引。例如,如果您使用LIMIT 10 OFFSET 10000 进行经典分页,它将创建一个包含 10010 行的结果集,然后它会丢弃前 10000 行。这是很多不必要的工作和内存使用。通过游标分页,您可以使用复杂的 WHERE 子句(与 db 索引结合)来创建不包含这 10000 行的结果集。
  • 2. 使用光标分页,您的光标不必是现有行。你可以说“给我[id = 00, time = 00:00:00, data: 1970-01-01]之后的前10行”。表是否实际包含具有这些值的行并不重要。使用此功能,您可以在多个分页查询之间插入和删除行,而不必担心返回数据中的重复行。
  • @BenjaminM 很高兴知道您得到了答案。如果你试过我的,请告诉我。只是想知道。
猜你喜欢
  • 2021-05-20
  • 1970-01-01
  • 2018-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多