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

Posted

技术标签:

【中文标题】优化下一个和上一个元素的查询【英文标题】:Optimizing queries for the next and previous element 【发布时间】:2011-01-19 14:33:00 【问题描述】:

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

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

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

(来源:pekkagaiser.com)

显然,“Tomatoes, Class I”的“下一步”按钮在第一个示例中必须是“Apples, class 1”,第二个示例中必须是“Pears, class 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

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

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

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

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

我的问题:

您认为这是找出不同查询顺序的相邻记录的好习惯吗?

您知道性能和简单性方面的更好做法吗?你知道什么使这完全过时吗?

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

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

是否有任何公认的通用模式来解决此问题?它们叫什么?

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

如果有不清楚的地方,请留言,我会澄清的。

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

【问题讨论】:

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

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

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

所以你会有这些列:

排序类型(未排序、价格、类别和价格描述) 优惠 ID 上一个 ID 下一个 ID

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

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

【讨论】:

这个想法非常有趣,并且使这个概念可以扩展到更大的列表。这将需要额外的“清洁”工作(删除对链中已删除项目的引用等),但这可以在数据更改时进行处理。很好,我会考虑的! 我喜欢这个主意。听起来很适合触发器/存储过程。 非规范化在这里效果很好。但是,如果您需要对许多不同的项目类型进行过滤和排序,那么它会变得更加复杂。 这是一个很好的解决方案,但它不适用于缓存过滤器(尽管这不是问题的一部分)。 “排序缓存”表结构更适合于此。细节。尽管如此,还是大了。 另外,我不会将更新后的逻辑放在你的数据库中 =) 如果你使用的是模型(或等效的),你可以通过 update() 把逻辑放在那里。【参考方案2】:

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

希望这是有用的。

【讨论】:

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

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

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

直接回答您的问题:

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

【讨论】:

【参考方案4】:

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

我总是将结果列表和结果本身分开缓存。如果有任何事情影响列表查询的结果,则会刷新列表结果的缓存。如果有任何事情影响结果本身,则会刷新这些特定结果。这使我无需重新生成所有内容即可更新其中任何一个,从而实现有效的缓存。

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

因为我已经缓存了整个列表,所以无需重新访问数据库就可以轻松找到相邻的项目。幸运的是,这些项目的数据也将被缓存。这在 JavaScript 中对数据进行排序时特别方便。如果我已经在客户端缓存了一个副本,我可以立即采取行动。

具体回答您的问题:

是的,提前找出邻居或客户接下来可能访问的任何信息是一个绝妙的主意,尤其是在现在成本低且重新计算成本高的情况下。然后,它只是在额外的预先计算和存储与速度之间进行权衡。 就性能和简单性而言,避免将逻辑上不同的事物捆绑在一起。索引和数据不同,可能会在不同的时间发生变化(例如添加新数据会影响索引,但不会影响现有数据),因此应该单独访问。从单线程的角度来看,这可能会稍微降低效率,但每次将某些内容绑定在一起时,都会失去缓存有效性和异步性(扩展的关键是异步性)。 提前获取数据的术语是预取。预取可以在访问时或在后台发生,但在实际需要预取数据之前。预计算也是如此。这是对现在成本、存储成本和在需要时获取成本的权衡。 “排序缓存”是一个恰当的名称。 我不知道。

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

【讨论】:

【参考方案5】:

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

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

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

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

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

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

【讨论】:

我对无缘无故投反对票感到非常恼火。【参考方案6】:

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

如果您的原始查询很昂贵(您所说的确实如此),则创建另一个表,可能是一个内存表,用您的昂贵且很少运行的主查询的结果填充它。

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

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

或者,如果你想避免连接到数据库,那么你可以将所有数据存储在一个 php 数组中,并使用 memcached 存储它。这将非常快,并且如果您的列表不是太大,那么资源效率会很高。并且可以轻松排序。

直流

【讨论】:

【参考方案7】:

基本假设:

每周都有特价商品 我们可以预期该网站不会经常更改...可能每天都会发生变化? 我们可以使用以太 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 在这类事情上很受欢迎(双关语!)。当某些内容被推送到数据库时,您可以发出触发器以使用正确的值更新缓存。以与您的更新项目存在于 3 个链接列表中相同的方式执行此操作——根据需要重新链接(this.next.prev = this.prev 等)。从此,只要您的缓存没有溢出,您就会以主键方式从内存中提取简单值。

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

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

对不起,我的拖尾答案有点用处,但我认为我的叙述解决方案应该很有用。

【讨论】:

【参考方案8】:

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

【讨论】:

【参考方案9】:

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

如果您将其视为一个链表,您可以为每个排序和 prev / next 键添加字段到 items 表。但是 DB Person 会为此杀了你,就像 GOTO。

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

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

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

【讨论】:

【参考方案10】:

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

我的方法是在第一次检索到数组后对其进行序列化(),然后将其缓存到单独的存储区域;无论是 memcached/APC/hard-drive/mongoDb/ 等,并通过他们的会话数据分别为每个用户保留其缓存位置详细信息。实际的存储后端自然取决于阵列的大小,您不会对此进行详细介绍,但是 memcached 可以在多个服务器上很好地扩展,而 mongo 甚至可以进一步扩展,但延迟成本会稍高一些。

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

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

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

【讨论】:

【参考方案11】:

所以你有两个任务:

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

有什么问题?

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

【讨论】:

正如我所说,列表的构建和细节的显示都不是我的问题。我的问题是关于我为获取相邻记录而概述的缓存的具体方式,以及是否有人对如何做到这一点有更好的想法。

以上是关于优化下一个和上一个元素的查询的主要内容,如果未能解决你的问题,请参考以下文章

在 MySQL 中优化 ORDER BY LIMIT 查询

Joomla中的mysql orderby中的下一个和上一个SQL语句

laravel 仅在分页中显示下一个和上一个链接

如何在Java中单击下一个和上一个按钮时显示下一个和上一个记录?

编码下一个和上一个按钮

下一个和上一个 jQuery 幻灯片