优化类别过滤器

时间:2011-09-01 15:44:45

标签: sql query-optimization

This recent question让我考虑优化类别过滤器。

假设我们希望创建一个引用大量音轨的数据库,其发布日期和可从中下载音轨的世界位置列表。

我们希望优化的请求是:

  • 向我提供可从位置A下载的10条最新曲目。
  • 向我提供可从A或B位置下载的10条最新曲目。
  • 向我提供可从A和B位置下载的10条最新曲目。

如何构建该数据库?我很难想出一个简单的解决方案,不需要通过所有轨道读取至少一个位置......

2 个答案:

答案 0 :(得分:7)

要优化这些查询,您需要稍微对数据进行反规范化。

例如,您可能有一个track表,其中包含曲目的idnamerelease date,以及一个map_location_to_track表格,其中包含曲目可以从中下载。要回答“位置A的10个最近的曲目”,您需要从map_location_to_track获取位置A的所有曲目,然后将它们加入track表格,按release date排序,并选择前10名。

如果所有数据都在一个表中,则可以避免订购步骤。例如......

CREATE TABLE map_location_to_track (
  location_id   INT,
  track_id      INT,
  release_date  DATETIME,
  PRIMARY KEY (location_id, release_date, track_id)
)

SELECT * FROM map_location_to_track
WHERE location_id = A
ORDER BY release_date DESC LIMIT 10

将location_id作为主键中的第一个条目可确保WHERE子句只是索引搜索。然后没有要求重新排序数据,它已经由主键为我们订购,而是在最后选择10条记录。

您可能仍然可以加入track表来获取名称,价格等,但您现在只需要为10条记录而不是该位置的所有内容。


要解决“位置A OR B”的相同查询,根据您使用的RDBMS,有几个选项可以执行不同的操作。

第一个很简单,虽然有些RDBMS不适合IN ...

SELECT track_id, release_date FROM map_location_to_track
WHERE location_id IN (A, B)
GROUP BY track_id, release_date
ORDER BY release_date DESC LIMIT 10

下一个选项几乎完全相同,但仍有一些RDBMS不适合将OR逻辑应用于INDEXes。

SELECT track_id, release_date FROM map_location_to_track
WHERE location_id = A or location_id = B
GROUP BY track_id, release_date
ORDER BY release_date DESC LIMIT 10

在任何一种情况下,用于合理化低至10的记录列表的算法对您是隐藏的。这是一个尝试和看到的问题;索引仍然可用,这样才能实现这一目标。

另一种方法是在SQL语句中明确确定部分方法......

SELECT
  *
FROM
(
  SELECT track_id, release_date FROM map_location_to_track
  WHERE location_id = A
  ORDER BY release_date DESC LIMIT 10

  UNION

  SELECT track_id, release_date FROM map_location_to_track
  WHERE location_id = B
  ORDER BY release_date DESC LIMIT 10
)
  AS data
ORDER BY
  release_date DESC
LIMIT 10

-- NOTE: This is a UNION and not a UNION ALL
--       The same track can be available in both locations, but should only count once
--       It's in place of the GROUP BY in the previous 2 examples

仍然可以让优化器意识到这两个联合数据集是有序的,因此非常快速地创建外部订单。即使没有,订购20件物品也很快。更重要的是,这是一个固定的开销:如果你在每个位置都有十亿个曲目并不重要,我们只需合并两个10个曲目。


最难以优化的是AND条件,但即使这样,“TOP 10”约束的存在也可以帮助创造奇迹。

向基于INOR的方法添加HAVING子句可以解决这个问题,但是,再次,根据您的RDBMS,可能会运行不是最佳的。

SELECT track_id, release_date FROM map_location_to_track
WHERE location_id = A or location_id = B
GROUP BY track_id, release_date
HAVING COUNT(*) = 2
ORDER BY release_date DESC LIMIT 10


另一种方法是尝试“两种查询”方法......

SELECT
  location_a.*
FROM
(
  SELECT track_id, release_date FROM map_location_to_track
  WHERE location_id = A
)
  AS location_a
INNER JOIN  
(
  SELECT track_id, release_date FROM map_location_to_track
  WHERE location_id = B
)
  AS location_b
    ON  location_a.release_date = location_b.release_date
    AND location_a.track_id     = location_b.track_id
ORDER BY
  location_a.release_date DESC
LIMIT 10

这次我们无法将两个子查询限制为仅10条记录;据我们所知,最近的10个位置a不会出现在位置b 的所有中。然而,主键再次救了我们。这两个数据集按发布日期进行组织,RDBMS可以从每个集合的最高记录开始,然后将两者合并,直到它有10条记录,然后停止。

注意:由于release_date位于主键中,而track_id之前,确保确保在连接中使用

根据RDBMS,您甚至不需要子查询。你可以能够自行加入表而不改变RDBMS的计划......

SELECT
  location_a.*
FROM
  map_location_to_track AS location_a
INNER JOIN  
  map_location_to_track AS location_b
    ON  location_a.release_date = location_b.release_date
    AND location_a.track_id     = location_b.track_id
WHERE
      location_a.location_id = A
  AND location_b.location_id = B
ORDER BY
  location_a.release_date DESC
LIMIT 10


总而言之,三件事的结合使得效率非常高:
- 部分取消标准化数据以确保其符合我们的需求 - 知道我们只需要前10个结果
- 知道我们最多只处理2个地点


有些变体可以优化到任意数量的记录和任意数量的位置,但这些变量的性能远远低于此问题中所述的问题。

答案 1 :(得分:0)

在经典的关系模式中,您将在曲目和位置之间建立多对多的关系,以避免冗余:

CREATE TABLE tracks (
  id   INT,
  ...
  release_date  DATETIME,
  PRIMARY KEY (id)
)

CREATE TABLE locations (
  id   INT,
  ...
  PRIMARY KEY (id)
)

CREATE TABLE tracks_locations (
  location_id   INT,
  track_id      INT,
  ...
  PRIMARY KEY (location_id, track_id)
)

SELECT tracks.* FROM tracks_locations LEFT JOIN tracks ON tracks.id = tracks_locations.location_id
WHERE tracks_locations.location_id = A
ORDER BY tracks.release_date DESC LIMIT 10

您可以按位置使用表分区修改该架构。问题是它取决于实现问题或使用限制。例如,MySQL中的AFAIK你不能在分区表中有外键。要解决此问题,您还可以拥有一组表(称为“手动分区”),例如tracks_by_location_#,其中#是已知位置的ID。这些表可以存储过滤结果,并使用触发器创建/更新/删除。