高效查找最近 k 天内未更新的记录

Posted

技术标签:

【中文标题】高效查找最近 k 天内未更新的记录【英文标题】:Finding records not updated in last k days efficiently 【发布时间】:2014-11-20 15:15:49 【问题描述】:

我有一张表,其中包含过去 n 天的记录。该表中的记录约为 1 亿条。我需要找到最后k个没有更新的记录

我对这个问题的解决方案是

在 k1 上对表进行分区。时间戳列的索引。现在不要更新时间戳(以便不重建索引),而是执行删除 + 插入。通过这样做,我认为查找过去 k 天内未更新的记录的查询会很快。

还有其他更好的方法来优化这些操作吗?

例如,

假设我们有很多用户,每个用户可以使用不同的产品。用户也可以随时开始使用(成为所有者)新产品。如果用户在 n 天内未使用产品,他的所有权将到期。现在我们需要找到一个用户在过去 k 天内没有使用过的所有产品。用户数量为10000个订单,他可以选择的产品数量为100,000个订单。

我使用具有 (user_id, product_id, last_used) 架构的表对这个问题进行了建模。 product_id 是用户正在使用的产品的 ID。每当用户使用产品 last_used 时都会更新。此外,如果用户在 n 天内未使用产品,用户对产品的所有权也会过期。我在 user_id 上的表上分区并索引 last_used(timestamp)。我也没有更新,而是执行了删除+创建。我进行了分区和索引以优化查询以获取用户在过去 k 天内未更新的记录

有没有更好的方法来解决这个问题?

【问题讨论】:

您已经很好地描述了设置,但请进一步描述问题。您正在删除超过 x 天的记录并且它会影响读取访问权限?您一次要删除多少条记录?典型的删除查询是什么样的?创新数据库?删除记录时不会对索引进行任何“重建”,只是对其进行一些更新。 请举例说明您希望从查询中得到的结果。 什么是主键?你想做什么? @MarcusAdams 假设我们有一个用户表(user_id、product_id、last_used)。 product_id 是用户正在使用的产品的 ID。每当用户使用产品 last_used 时都会更新。用户也可以随时开始使用新产品。现在我需要为一个用户找到他在过去 k 天内没有使用过的所有产品。此外,如果用户在 n 天内未使用产品,用户对产品的所有权也会过期。如果索引在 last_used 并且 last_used 被修改,则索引将被“重新构建”。 这张表还有什么?还是只有这三列? 【参考方案1】:

你说过你需要“找到”,我认为在一定天数后“过期”属于特定用户的记录。

看,即使在具有良好索引的大表中也可以做到这一点,而不会带来太多麻烦。我向你保证,对表进行分区将是一件麻烦。您已经断言,由于更新,在您的应用程序中为您的last_used 列添加索引太昂贵了。但是,考虑到维护分区表的初始和持续费用,我强烈建议您首先证明该断言。您可能对维护索引的成本有误。

(用索引的列更新一行不会重建索引,它会修改它。我向你保证,mysql 存储引擎开发人员已经优化了这个用例。)

我相信您知道,此查询将检索特定用户的旧记录。

SELECT product_id 
   FROM tbl
  WHERE user_id = <<<chosen user>>>
    AND last_used <= CURRENT_DATE() - <<<k>>> DAY

将生成您的产品列表。如果您在(user_id, last_used, product_id) 上有一个复合覆盖索引,这确实会非常有效。如果您不知道复合覆盖索引是什么,您真的应该使用您最喜欢的搜索引擎来查找。这将随机访问特定用户,然后在 last_used 日期进行范围扫描。然后它将从索引中返回产品 ID。

如果你想摆脱所有旧记录,我建议你编写一个主机程序,在循环中重复这个查询,直到你发现它已经处理了零行。在您的应用程序的非高峰时间运行它。 LIMIT 子句将防止每个单独的查询花费太长时间并干扰表的其他用途。为了加快此查询的速度,您需要在last_used 上建立一个索引。

DELETE FROM tbl
 WHERE last_used <= CURRENT_DATE() - <<<k>>> DAY
 LIMIT 500

我希望这会有所帮助。它来自某人犯了一个代价高昂的错误,试图对不需要分区的东西进行分区。

【讨论】:

感谢您的回复。我将阅读有关复合覆盖指数的信息。但是如果 last_used 列的更新非常频繁,是不是每次更新操作都会修改索引,导致更新成本高昂? 缓存删除(快速),然后在服务器不忙时实际更新索引,但您仍然希望避免长时间运行的事务,因此一次删除有限数量不是如果花费的时间太长,这是一个坏主意。【参考方案2】:

当您修改索引值时,MySQL 不会“重建”索引(不完全)。事实上,它甚至不会对记录进行重新排序。它只是将记录移动到正确的 16KB 页面。

在一个页面中,记录按添加顺序排列。如果您按顺序插入,则它们是按顺序插入的,否则它们不是。

所以,当他们说 MySQL 的聚集索引是按物理顺序排列时,它只在页面级别是正确的,而不是在页面内。

聚集索引仍然具有页面数据与索引在同一页面上的优势,因此如果行数据足够小以适合页面,则无需进一步查找。读取速度更快,但重组速度较慢,因为您必须使用索引移动数据。二级索引的更新速度要快得多,但要实际检索数据(覆盖索引除外),必须通过二级索引产生的主键进行进一步查找以检索实际数据。

示例

第 1 页可能保存姓氏从 A 到 B 开头的人的用户记录。第 2 页可能保存姓名 C 到 D 等。如果 Bob 将自己重命名为 Chuck,他的记录只会从第 1 页复制到第 2 页。他的记录将始终放在第 2 页的末尾。键保持排序,但它们指向的数据不排序。

如果页面已满,MySQL 将拆分页面。在这种情况下,假设 C 和 D 之间均匀分布,则第 1 页为 A 到 B,第 2 页为 C,第 3 页为 D。

当一条记录被删除时,空间被压缩,如果记录不到半满,MySQL将合并相邻的页面,并可能释放中间的页面。

所有这些更改都被缓冲,MySQL 会在它不忙时进行实际的写入。

该示例对聚集(主)索引和二级索引的工作方式相同,但请记住,对于聚集索引,键指向实际表数据,而对于二级索引,键指向的值等于主键。

总结

一段时间后,随机插入导致的页面拆分将导致页面在磁盘上变得不连续。表格将变得“碎片化”。优化表(重建表/索引)解决了这个问题。

删除然后重新插入记录没有任何好处。事实上,您只会增加事务开销。让 MySQL 为您处理更新索引。

现在您对索引有了更多了解,也许您可​​以更好地决定如何优化您的数据库。

【讨论】:

以上是关于高效查找最近 k 天内未更新的记录的主要内容,如果未能解决你的问题,请参考以下文章

SQL 中高效的“查找最近的数字或日期”,其中日期/数字列被索引覆盖

排行榜的高效数据结构,即记录列表(名称、点数) - 高效搜索(名称)、搜索(排名)和更新(点数)

高效更新大量核心数据记录

排序算法-Sort,更高效率的插入排序—希尔排序

需要一个高效的内存缓存,每秒可以处理 4k 到 7k 的查找或写入

在 PostgreSql 中批量更新或删除哪个更高效?