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

时间:2010-02-22 11:06:39

标签: php mysql algorithm data-structures pseudocode

我正在寻找在不运行完整查询的情况下检索记录的下一个和上一个记录的最佳方法。我有一个完全实现的解决方案,并想知道是否有更好的方法来实现这一目标。

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

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

alt text http://www.pekkagaiser.com/stuff/Sort.gif?

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

详细视图中的任务是来确定下一个和上一个项目,而不是每次都运行查询,列表的排序顺序是唯一可用的信息(假设我们通过一个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

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

在详细信息页面中,我现在访问相应的sortingcache记录,获取items列,将其展开,搜索当前项ID,然后返回上一个和下一个邻居。

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

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

我的问题:

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

  • 您是否了解性能和简单性方面的更好做法?你知道一些让它完全过时的东西吗?

  • 在编程理论中,这个问题是否有名称?

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

  • 是否有任何公认的常见模式可以解决这个问题?他们叫什么?

  

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

如果不清楚,请发表评论,我会澄清。

  

开始赏金 - 也许有更多关于此的信息。

11 个答案:

答案 0 :(得分:16)

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

为每个商品和每个分页选项创建一个表,以保存下一个和上一个。 (或者,如果您总是有三个排序选项,则可以将其存储在商品表中 - 查询速度是对数据库进行非规范化的一个很好的理由)

所以你会有这些专栏:

  • 排序类型(未分类,价格,类别及价格说明)
  • 优惠ID
  • 上一个ID
  • 下一个ID

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

每次插入,更新或删除商品时,您都需要运行一个验证sorttype表的完整性/准确性的过程。

答案 1 :(得分:4)

我有一个类似于杰西卡的想法。但是,不存储指向下一个和上一个排序项的链接,而是存储每个排序类型的排序顺序。要查找上一条或下一条记录,只需使用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

希望这很有用。

答案 2 :(得分:2)

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

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

直接回答你的问题:

  • 是的,这是一种很好的做法,因为当您的过滤器/排序和项目类型变得更加复杂时,它会在没有任何额外代码复杂性的情我在一个生产系统中使用它,有250k文章,带有“无限”过滤/排序变化。将可缓存的ID修剪为1000也是可能的,因为用户很可能永远不会点击上一次或下一次超过500次(他很可能会返回并优化搜索或分页)。
  • 我不知道更好的方法。但是如果排序有限并且这是一个公共站点(没有http会话)那么我很可能会反规范化。
  • 说不上。
  • 是的,排序缓存听起来不错。在我的项目中,我将其称为“搜索结果中的上一个/下一个”或“搜索结果上的导航”。
  • 说不上。

答案 3 :(得分:2)

通常,我会对索引中的数据进行反规范化。它们可能存储在相同的行中,但我几乎总是检索我的结果ID,然后为数据单独行。这使得缓存数据变得非常简单。在PHP中,延迟较低且带宽较高,这一点并不那么重要,但是当您拥有高延迟,低带宽应用程序(如AJAX网站,其中大部分网站都是使用JavaScript呈现)时,这种策略非常有用。 / p>

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

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

因为我已经缓存了整个列表,所以在不重新访问数据库的情况下查找相邻项目是微不足道的。幸运的是,这些项目的数据也将被缓存。在使用JavaScript排序数据时,这尤其方便。如果我已经在客户端上缓存了一个副本,我可以立即求助。

具体回答您的问题:

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

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

答案 4 :(得分:1)

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

让我们说,givens是对排序列表的查询以及该列表中的当前偏移量,即我们有$query$n

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

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

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

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

答案 5 :(得分:0)

有很多方法可以做到这一点,以便为众所周知的猫皮肤做准备。所以这里有几个。

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

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

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

或者,如果您想避免连接到数据库,那么您可以将所有数据存储在php数组中并使用memcached存储它。这将是非常快的,如果您的列表不是太大将资源有效。并且可以很容易地分类。

DC

答案 6 :(得分:0)

基本假设:

  • 特价是每周
  • 我们可以预期该网站不经常更改......可能每天都会更改?
  • 我们可以使用ether API控制数据库更新或通过触发器响应

如果网站每天都在变化,我建议所有网页都是一夜之间静态生成的。每个排序顺序的一个查询迭代并生成所有相关页面。即使存在动态元素,也可以通过包含静态页面元素来解决它们。这将提供最佳页面服务并且无需数据库负载。实际上,您可能会生成单独的页面和包含在页面中的prev / next元素。这可能更疯狂,有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很受欢迎(pun!)。将某些内容推送到数据库时,您可以发出触发器以使用正确的值更新缓存。以与您在3个链接列表中存在更新项目相同的方式执行此操作 - 根据需要重新链接(this.next.prev = this.prev等)。从那时起,只要您的缓存没有溢出,您就会以主键方式从内存中提取简单值。

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

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

抱歉,我的拖尾答案有点无用,但我认为我的叙述解决方案应该非常有用。

答案 7 :(得分:0)

您可以将有序列表的row numbers保存到views,然后您可以在(current_rownum-1)和(current_rownum + 1)行号下找到列表中的上一个和下一个项目。

答案 8 :(得分:0)

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

如果您将其视为链接列表,则只需在每个排序和prev / next键的items表中添加字段即可。但是DB Person会因此而杀了你,就像GOTO。

如果您将其视为(双向)方向图,您可以使用Jessica的答案。主要问题是订单更新是昂贵的操作。

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

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

答案 9 :(得分:0)

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

我的方法是在数组首次检索时序列化(),然后将其缓存到一个单独的存储区域;是否是memcached / APC / hard-drive / mongoDb /等,并通过其会话数据单独保留每个用户的缓存位置详细信息。实际的存储后端自然会取决于阵列的大小,你不会详细讨论,但是memcached在多个服务器上的扩展性很强,甚至可以稍微延长成本。

您也没有说明现实世界中有多少种排列;例如你需要为每个用户缓存单独的列表,还是可以全局缓存每个排序,然后通过PHP过滤掉你不需要的东西?在你给出的例子中,我只是缓存两个排列并存储我需要在会话数据中反序列化()中的哪一个。

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

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

答案 10 :(得分:-3)

所以你有两个任务:

  1. 构建排序的项目列表(具有不同ORDER BY的SELECT)
  2. 显示有关每个项目的详细信息(具有可能的缓存的数据库中的SELECT详细信息)。
  3. 有什么问题?

    PS:如果有序列表可能太大,您只需要实现PAGER功能。可以有不同的实现,例如您可能希望在查询中添加“LIMIT 5”并提供“Show next 5”按钮。按下该按钮时,添加“WHERE price< 0.89 LIMIT 5”之类的条件。