优化下一个和上一个元素的查询

Optimizing queries for the next and previous element

提问人:Pekka 提问时间:2/22/2010 最后编辑:GlorfindelPekka 更新时间:6/20/2019 访问量:1458

问:

我正在寻找在不运行完整查询的情况下检索记录的下一条和上一条记录的最佳方法。我有一个完全实施的解决方案,想知道是否有更好的方法可以做到这一点。

假设我们正在为一个虚构的蔬菜水果商建立一个网站。除了他的 HTML 页面之外,每周,他还想在他的网站上发布一份特别优惠列表。他希望这些选件驻留在实际的数据库表中,并且用户必须能够以三种方式对选件进行排序。

每件商品还必须有一个详情页面,其中包含有关报价的更多文本信息以及“上一个”和“下一个”按钮。“上一个”和“下一个”按钮需要指向相邻条目,具体取决于用户为列表选择的排序

alt text
(来源:pekkagaiser.com

显然,“西红柿,I 类”的“下一步”按钮在第一个示例中必须是“苹果,1 类”,第二个示例中的“梨,I 类”,第三个示例中没有。

详细信息视图中的任务是确定下一个和上一个项目,而无需每次都运行查询,列表的排序顺序是唯一可用的信息(假设我们通过 GET 参数获取它,并忽略安全隐患)。?sort=offeroftheweek_price

显然,简单地将下一个和上一个元素的 ID 作为参数传递是想到的第一个解决方案。毕竟,我们此时已经知道了 ID。但是,这不是一个选项 - 它在这个简化的示例中有效,但在我的许多现实世界用例中不起作用。

我目前在CMS中的方法是使用我称之为“排序缓存”的东西。加载列表时,我将项目位置存储在名为 的记录中。sortingcache

name (VARCHAR)             items (TEXT)

offeroftheweek_unsorted    Lettuce; Tomatoes; Apples I; Apples II; Pears
offeroftheweek_price       Tomatoes;Pears;Apples I; Apples II; Lettuce
offeroftheweek_class_asc   Apples II;Lettuce;Apples;Pears;Tomatoes

显然,该列实际上填充了数字 ID。items

在详细信息页面中,我现在访问相应的记录,提取列,分解它,搜索当前项目 ID,并返回上一个和下一个邻居。sortingcacheitems

array("current"   => "Tomatoes",
      "next"      => "Pears",
      "previous"  => null
      );

这显然很昂贵,仅适用于有限数量的记录并创建冗余数据,但让我们假设在现实世界中,创建列表的查询非常昂贵(确实如此),在每个详细信息视图中运行它是不可能的,并且需要一些缓存。

我的问题:

  • 您认为这是找出不同查询订单的相邻记录的好做法吗?

  • 您知道在性能和简单性方面有更好的做法吗?你知道是什么让它完全过时了吗?

  • 在编程理论中,这个问题有名字吗?

  • 对于这种技术,“排序缓存”这个名称是否合适且易于理解?

  • 是否有任何公认的通用模式可以解决此问题?他们叫什么?

注意:我的问题不是关于构建列表,也不是关于如何显示详细信息视图。这些只是例子。我的问题是,当无法重新查询时,确定记录邻居的基本功能,以及到达那里的最快和最便宜的方法。

如果有什么不清楚的地方,请发表评论,我会澄清的。

开始赏金 - 也许那里有更多关于这方面的信息。

php mysql 算法 数据结构 伪代码

评论

0赞 Jon Winstanley 2/22/2010
我喜欢表格格式。一定花了一段时间!(编辑!哦,这是一张图片。我被骗了!
0赞 Pekka 2/22/2010
@Jon是的,这是一个技巧:)但是 Markdown 似乎支持基本的 HTML......下次我会尝试这条路线。
0赞 Tomalak 2/23/2010
@Pekka:不过没有桌子。你必须以 ASCII 艺术的方式构建它们。
1赞 Fantius 2/7/2011
我花了一段时间才弄清楚真正的问题是什么。我认为情况是用户处于详细视图中并希望查看下一条记录,其中“下一个”取决于他之前选择的排序顺序。查询排序列表,然后查询下一条记录的详细信息是低效的。相反,您只想查询下一条记录的详细信息。
1赞 Heinrich Apfelmus 2/9/2011
我不清楚您希望如何分配资源。数据库查询是否一次只提取 5 个连续项目?数据库查询是否应该获取所有内容,但稍后对结果执行排序(这意味着服务器必须缓存结果)?这应该发生在服务器上,还是在客户端(JavaScript)上?

答:

-3赞 noonex #1

因此,您有两个任务:

  1. 构建排序的项目列表(具有不同 ORDER BY 的 SELECT)
  2. 显示有关每个项目的详细信息(从数据库中选择详细信息,并可能进行缓存)。

问题是什么?

PS:如果有序列表可能太大,您只需要实现PAGER功能即可。可能有不同的实现,例如,您可能希望将“LIMIT 5”添加到查询中并提供“显示下一个 5”按钮。按下此按钮时,会添加“WHERE price < 0.89 LIMIT 5”等条件。

评论

0赞 Pekka 2/22/2010
正如我所说,无论是列表的构建,还是细节的显示,都不是我的问题。我的问题是关于我概述的用于获取相邻记录的缓存的具体方法,以及是否有人对如何做到这一点有更好的想法。
16赞 Jessica 2/23/2010 #2

这是一个想法。当杂货店插入/更新新选件时,而不是在最终用户选择要查看的数据时,您可以将成本高昂的操作卸载到更新中。这似乎是处理排序数据的一种非动态方式,但它可能会提高速度。而且,正如我们所知,性能和其他编码因素之间总是需要权衡。

创建一个表,用于保存每个选件和每个排序选项的下一个和上一个。(或者,如果您始终有三个排序选项,则可以将其存储在 offer 表中 -- 查询速度是使数据库非规范化的一个很好的理由)

因此,您将拥有以下列:

  • 排序类型(未排序、价格、类和价格描述)
  • 产品/服务 ID
  • 上一个 ID
  • 下一个 ID

从数据库中查询产品/服务详细信息页的详细信息时,NextID 和 PrevID 将成为结果的一部分。因此,每个详情页面只需要一个查询。

每次插入、更新或删除选件时,都需要运行一个过程来验证排序类型表的完整性/准确性。

评论

0赞 Pekka 2/23/2010
这个想法非常有趣,并使这个概念可以扩展到更大的列表。这将需要额外的“清洁”工作(删除对链中已删除项目的引用等),但这可以在数据更改时处理。很好,我会考虑这个!
0赞 poisson 2/7/2011
我喜欢这个主意。听起来像是触发器/存储过程的良好候选者。
0赞 cherouvim 2/8/2011
非规范化在这里效果很好。但是,如果您需要对许多不同的项目类型进行过滤和排序,它会变得更加复杂。
0赞 Rudie 2/9/2011
这是一个很好的解决方案,但它不适用于缓存过滤器(尽管这不是问题的一部分)。“排序缓存”表结构对此更为准备。详。不过,大起来。
0赞 Rudie 2/9/2011
另外,我不会将更新后逻辑放在您的数据库中 =) 如果您使用的是模型(或等效工具),您可以通过 update() 将逻辑放在那里。
1赞 NikiC 2/8/2011 #3

我不确定我是否理解正确,所以如果没有,请告诉我;)

假设给定是排序列表的查询和该列表中的当前偏移量,即我们有一个和一个 .$query$n

最小化查询的一个非常明显的解决方案是一次获取所有数据:

list($prev, $current, $next) = DB::q($query . ' LIMIT ?i, 3', $n - 1)->fetchAll(PDO::FETCH_NUM);

该语句按当前排序顺序从数据库中获取上一个、当前和下一个元素,并将相关信息放入相应的变量中。

但是由于这个解决方案太简单了,我想我误解了一些东西。

评论

2赞 NikiC 2/8/2011
我对无缘无故和无缘无故地投反对票感到非常恼火。
2赞 cherouvim 2/8/2011 #4

我也做过这个噩梦。您当前的方法似乎是最好的解决方案,即使对于 10k 项目的列表也是如此。在 http 会话中缓存列表视图的 ID,然后使用它来显示(针对当前用户个性化)上一个/下一个。这很有效,尤其是当有太多方法可以过滤和排序初始项目列表而不仅仅是 3 个时。
此外,通过存储整个 ID 列表,您可以显示增强可用性的文本。
"you are at X out of Y"
JIRA's previous/next

顺便说一句,这也是JIRA所做的。

要直接回答您的问题,请执行以下操作:

  • 是的,这是一个很好的做法,因为当您的筛选器/排序和项目类型更加复杂时,它可以在不增加任何代码复杂性的情况下进行扩展。我正在一个具有 250k 篇文章的生产系统中使用它,这些文章具有“无限”的过滤/排序变化。将可缓存的 ID 修剪到 1000 也是一种可能性,因为用户很可能永远不会点击上一个或下一个超过 500 次(他很可能会返回并优化搜索或分页)。
  • 我不知道更好的方法。但是,如果排序受到限制,并且这是一个公共站点(没有 http 会话),那么我很可能会非规范化。
  • 不知道。
  • 是的,对缓存进行排序听起来不错。在我的项目中,我称之为“搜索结果上的上一个/下一个”或“搜索结果上的导航”。
  • 不知道。
2赞 Mark Rose 2/9/2011 #5

通常,我将索引中的数据非规范化。它们可能存储在同一行中,但我几乎总是检索我的结果 ID,然后对数据进行单独的访问。这使得缓存数据变得非常简单。在延迟低、带宽高的 PHP 中,这并不是那么重要,但是当您有一个高延迟、低带宽的应用程序时,这样的策略非常有用,例如 AJAX 网站,其中大部分网站都是用 JavaScript 呈现的。

我总是单独缓存结果列表和结果本身。如果有任何内容影响列表查询的结果,则会刷新列表结果的缓存。如果有任何影响结果本身,则会刷新这些特定结果。这允许我更新任何一个,而不必重新生成所有内容,从而实现有效的缓存。

由于我的结果列表很少更改,因此我会同时生成所有列表。这可能会使初始响应速度稍慢,但它简化了缓存刷新(所有列表都存储在单个缓存条目中)。

由于我缓存了整个列表,因此在不重新访问数据库的情况下查找相邻项是微不足道的。运气好的话,这些项目的数据也将被缓存。这在 JavaScript 中对数据进行排序时特别方便。如果我已经在客户端上缓存了副本,我可以立即调用。

要具体回答您的问题,请执行以下操作:

  • 是的,提前找出邻居,或者客户接下来可能访问的任何信息,这是一个绝妙的主意,特别是如果现在成本很低,而重新计算的成本很高。然后,它只是在额外的预计算和存储与速度之间进行权衡。
  • 在性能和简单性方面,避免将逻辑上不同的东西捆绑在一起。索引和数据是不同的,可能会在不同的时间进行更改(例如,添加新的基准面会影响索引,但不会影响现有数据),因此应单独访问。从单线程的角度来看,这可能效率略低,但每次将某些内容捆绑在一起时,都会失去缓存有效性和异时性(扩展的关键是异时性)。
  • 提前获取数据的术语是预取。预取可以在访问时或后台进行,但在实际需要预取数据之前进行。预计算也是如此。这是现在成本、存储成本和在需要时获得的成本的权衡。
  • “排序缓存”是一个恰当的名称。
  • 我不知道。

此外,当您缓存内容时,请尽可能以最通用的级别缓存它们。有些内容可能是特定于用户的(例如搜索查询的结果),而其他内容可能与用户无关,例如浏览目录。两者都可以从缓存中受益。目录查询可能很频繁,每次都节省一点,搜索查询可能很昂贵,但几次节省很多。

0赞 DeveloperChris 2/11/2011 #6

有很多方法可以做到这一点,就像给众所周知的猫剥皮一样。所以这里有几个我的。

如果你的原始查询很昂贵,你说它是,那么创建一个表,可能是一个内存表,用你昂贵且很少运行的主查询的结果填充它。

然后,可以在每个视图上查询第二个表,排序就像设置适当的排序顺序一样简单。

根据需要,使用第一个表的结果重新填充第二个表,从而使数据保持最新,但最大限度地减少使用成本高昂的查询。

或者,如果你想避免连接到数据库,那么你可以将所有数据存储在一个 php 数组中,并使用 memcached 存储它。这将非常快,并且只要您的列表不是太大,就可以节省资源。并且可以轻松分类。

直流

0赞 Jeff Ferland 2/12/2011 #7

基本假设:

  • 特价是每周一次
  • 我们可以预期网站不会经常更改......可能每天都有?
  • 我们可以使用以太 API 控制对数据库的更新,也可以通过触发器进行响应

如果网站每天都在变化,我建议所有页面都是在一夜之间静态生成的。每个排序顺序的一个查询将循环访问并生成所有相关页面。即使存在动态元素,您也可以通过包含静态页面元素来解决它们。这将提供最佳的页面服务,并且没有数据库负载。事实上,您可以生成单独的页面和包含在页面中的上一个/下一个元素。这可能更疯狂,有 200 种排序方式,但有 3 种,我是它的忠实粉丝。

?sort=price
include(/sorts/$sort/tomatoes_class_1)
/*tomatoes_class_1 is probably a numeric id; sanitize your sort key... use numerics?*/

如果由于某种原因这不可行,我会求助于记忆。Memcache因这种事情而流行(双关语!当某些内容被推送到数据库时,您可以发出触发器以使用正确的值更新缓存。如果更新的项目存在于 3 个链表中,请以相同的方式执行此操作 - 根据需要重新链接(this.next.prev = this.prev 等)。从那时起,只要您的缓存不会过度填充,您就会以主键方式从内存中提取简单值。

此方法将在 select 和 update / insert 方法上进行一些额外的编码,但它应该相当少。最后,你会抬头看.如果该密钥位于缓存中,则为金色。如果没有,请插入缓存并显示。[id of tomatoes class 1].price.next

  • 您认为这是找出不同查询订单的相邻记录的好做法吗?是的。明智的做法是对预期的即将到来的请求进行展望。
  • 您知道在性能和简单性方面有更好的做法吗?你知道是什么让它完全过时了吗?希望以上
  • 在编程理论中,这个问题有名字吗?优化?
  • 对于这种技术,“排序缓存”这个名称是否合适且易于理解?我不确定具体的合适名称。它是缓存,它是某种缓存,但我不确定告诉我你有一个“排序缓存”是否会传达即时理解。
  • 是否有任何公认的通用模式可以解决此问题?他们叫什么?缓存?

对不起,我的尾随答案有点没用,但我认为我的叙述解决方案应该非常有用。

0赞 inf3rno 2/12/2011 #8

您可以将有序列表的行号保存到视图中,并且可以在 (current_rownum-1) 和 (current_rownum+1) 行号下访问列表中的上一项和下一项。

0赞 Beffa 2/13/2011 #9

问题/数据结构被命名为双向图,或者你可以说你有几个链表。

如果您将其视为链表,则可以将每个排序和上一个/下一个键的字段添加到项目表中。但是DB Person会为此杀了你,就像GOTO一样。

如果你把它想象成一个(双向)图,你会接受杰西卡的答案。主要问题是订单更新是昂贵的操作。

 Item Next Prev
   A   B     -
   B   C     A
   C   D     B
   ...

如果您将一个项目位置更改为新订单 A、C、B、D,则必须更新 4 行。

4赞 Adukra 2/13/2011 #10

我的想法与杰西卡的想法有些相似。但是,您不是存储指向下一个和上一个排序项的链接,而是存储每种排序类型的排序顺序。若要查找上一条或下一条记录,只需获取 SortX=currentSort++ 或 SortX=currentSort 的行--.

例:

Type     Class Price Sort1  Sort2 Sort3
Lettuce  2     0.89  0      4     0
Tomatoes 1     1.50  1      0     4
Apples   1     1.10  2      2     2
Apples   2     0.95  3      3     1
Pears    1     1.25  4      1     3

此解决方案将产生非常短的查询时间,并且比 Jessica 的想法占用更少的磁盘空间。但是,我相信您已经意识到,更新一行数据的成本要高得多,因为您必须重新计算和存储所有排序顺序。但是,根据您的情况,如果数据更新很少,尤其是如果它们总是批量发生,那么此解决方案可能是最好的。

once_per_day
  add/delete/update all records
  recalculate sort orders

希望这是有用的。

评论

0赞 Adukra 2/13/2011
这种解决方案也有一些方便的副作用。1:你很容易知道你是在排序列表的头部(sortOrder=0)还是尾部(sortOrder=listLength)。2:您可以轻松地以大于 1 的增量跳转(通过使用 sortX=currentSort+5 查询行来跳转到 5 条记录)
0赞 JT703 2/14/2011
嘿!我们正在使用类似的方法来浏览我网站上的列表 - wethepixels.com。我们有很多列表需要整理,就像这样。它非常快速和高效。我强烈推荐这种方法!
0赞 Matt 2/13/2011 #11

如果我误解了,很抱歉,但我认为您希望在用户访问服务器之间保留有序列表。如果是这样,您的答案很可能在于您的缓存策略和技术,而不是数据库查询/模式优化。

我的方法是在首次检索数组后对其进行序列化(),然后将其缓存到单独的存储区域;无论是 memcached/APC/硬盘/mongoDb/ 等,并通过会话数据为每个用户单独保留其缓存位置详细信息。实际的存储后端自然取决于数组的大小,你不会详细介绍,但 memcached 可以在多个服务器和 mongo 上扩展得很好,延迟成本略高。

您也没有指出现实世界中有多少种排列;例如,您是否需要为每个用户缓存单独的列表,或者您可以全局缓存每个排序排列,然后通过PHP过滤掉不需要的内容?在您给出的示例中,我只需缓存这两种排列,并在会话数据中存储我需要 unserialize() 的两种排列中的哪一种。

当用户返回到站点时,请检查缓存数据的“生存时间”值,如果仍然有效,请重新使用它。我还有一个在 INSERT/UPDATE/DELETE 上运行的触发器,用于特别优惠,只需在单独的表中设置时间戳字段。这将立即指示缓存是否过时,并且需要重新运行查询以非常低的查询成本。仅使用触发器设置单个字段的好处在于,无需担心从该表中删除旧/冗余值。

这是否合适取决于返回的数据的大小、修改数据的频率以及服务器上可用的缓存技术。