【问题标题】:A SQL query searching for rows that satisfy Column1 <= X <= Column2 is very slow搜索满足 Column1 <= X <= Column2 的行的 SQL 查询非常慢
【发布时间】:2018-05-27 16:02:01
【问题描述】:

我正在使用 MySQL 数据库,并且有下表:

CREATE TABLE SomeTable (
  PrimaryKeyCol BIGINT(20) NOT NULL,
  A BIGINT(20) NOT NULL,
  FirstX INT(11) NOT NULL,
  LastX INT(11) NOT NULL,
  P INT(11) NOT NULL,
  Y INT(11) NOT NULL,
  Z INT(11) NOT NULL,
  B BIGINT(20) DEFAULT NULL,
  PRIMARY KEY (PrimaryKeyCol),
  UNIQUE KEY FirstLastXPriority_Index (FirstX,LastX,P)
) ENGINE=InnoDB;

该表包含 430 万行,并且一旦初始化就永远不会更改。

此表的重要列是FirstXLastXYZP

如您所见,我在行 FirstXLastXP 上有一个唯一索引。

FirstXLastX 列定义整数范围。

我需要在这个表上运行的查询为给定的 X 获取 FirstX

例如,如果表格包含行(我只包括相关的列):

FirstX LastX P Y Z
100000 500000 1 111 222
150000 220000 2 333 444
180000 190000 3 555 666
550000 660000 4 777 888
700000 900000 5 999 111
750000 850000 6 222 333

例如,我需要包含值185000 的行,应该返回第一行3

我尝试的查询,应该是使用索引,是:

SELECT P, Y, Z FROM SomeTable WHERE FirstX <= ? AND LastX >= ? LIMIT 10;

即使没有 LIMIT,对于任何给定的 X,此查询也应该返回少量记录(少于 50)。

这个查询是由一个 Java 应用程序针对 X 的 120000 值执行的。令我惊讶的是,它花费了 10 小时 (!),每个查询的平均时间为 0.3 秒

这是不可接受的,甚至几乎不能接受。它应该更快。

我检查了一个耗时 0.563 秒 的查询,以确保索引正在被使用。我尝试的查询(与上面的查询相同,使用特定整数值而不是 ?)返回 2 行

我使用EXPLAIN 来了解发生了什么:

id               1
select_type      SIMPLE
table            SomeTable 
type             range
possible_keys    FirstLastXPriority_Index
key              FirstLastXPriority_Index 
key_len          4
ref              NULL
rows             2104820
Extra            Using index condition

如您所见,执行涉及2104820行(接近表的50%的行),即使只有2行满足条件,所以检查了一半的索引以仅返回2行.

查询或索引有问题吗?您能否提出改进查询或索引的建议?

编辑:

一些答案​​建议我为多个 X 值分批运行查询。我不能这样做,因为我实时运行此查询,因为输入到达我的应用程序。每次输入 X 到达时,我都必须执行 X 的查询并对查询的输出进行一些处理。

【问题讨论】:

  • 如果二叉树上的某些点确实被条目堵塞,您可能会在这里遇到非常慢的查询。我知道边界测试在某些类型的数据上的扩展性非常差,这是 3D 类型应用程序中的一个长期存在的问题,例如碰撞检测,因此您可能需要一种比这里的简单索引方法更好的索引方法。
  • @tadman 感谢您的评论。那可能是哪种更好的索引方法?
  • innodb 缓冲池的值是多少?如果服务器专用于仅在 MySQL 上运行,SELECT @@innodb_buffer_pool_size 的总 RAM 应该会减少 75 - 80%
  • FirstX 与 P 的组合是唯一的吗? LastX 呢?
  • @Eran,是的,我明白了,但我想知道是否可以定义一对唯一索引,分别省略 FirstX 和 LastX。显然这取决于真实数据。

标签: mysql sql performance


【解决方案1】:

我找到了一个依赖表中数据属性的解决方案。我宁愿有一个不依赖于当前数据的更通用的解决方案,但目前这是我拥有的最好的解决方案。

原始查询的问题:

SELECT P, Y, Z FROM SomeTable WHERE FirstX <= ? AND LastX >= ? LIMIT 10;

当大部分行满足第一个条件FirstX &lt;= ? 时,执行可能需要扫描FirstX,LastX,P 索引中的大部分条目。

我为减少执行时间所做的是观察LastX-FirstX 相对较小。

我运行了查询:

SELECT MAX(LastX-FirstX) FROM SomeTable;

得到4200000

这意味着FirstX &gt;= LastX – 4200000 用于表中的所有行。

所以为了满足LastX &gt;= ?,我们还必须满足FirstX &gt;= ? – 4200000

所以我们可以在查询中添加一个条件如下:

SELECT P, Y, Z FROM SomeTable WHERE FirstX <= ? AND FirstX >= ? - 4200000 AND LastX >= ? LIMIT 10;

在我在问题中测试的示例中,处理的索引条目数从2104820减少到18,运行时间从0.563秒减少到0.0003秒

我使用与X 相同的120000 值测试了新查询。输出与旧查询相同。时间从10多小时缩短到5.5分钟,比快了100多倍

【讨论】:

  • 对一组不同的值进行类似改进??
  • 不,和我的不一样。表中值的范围是多少? 4200000 会保持不变,还是会随着新数据的插入而改变?
  • 您可以定义一个索引虚拟列,如RangeX INT AS (LastX - FirstX)。然后你可以用(SELECT MAX(RangeX) FROM SomeTable)替换硬编码值4200000
  • @RickJames 好吧,表格保持不变,直到我们将它的新版本导入数据库(我们的应用程序从不更新表格),所以我可以将4200000 常量设置为配置参数。
  • @PaulSpiegel 如果我们的应用程序更新了表格,那将会很有用。既然不是,那么将该常量定义为配置参数就没有问题。
【解决方案2】:

WHERE col1 &lt; ... AND ... &lt; col2 几乎不可能优化。

任何有用的查询都将涉及 col1 或 col2 上的“范围”。不能在单个INDEX 中使用两个范围(在两个不同的列上)。

因此,您尝试的任何索引都有检查大量表的风险: INDEX(col1, ...) 将从起点扫描到 col1 命中 ... 的位置。 col2 也是如此,扫描到最后。

更麻烦的是,范围是重叠的。所以,你不能拉一个快的并添加ORDER BY ... LIMIT 1 来快速停止。如果你说LIMIT 10,但只有9个,它不会停止,直到表格的开始/结束。

您可以做的一件简单的事情(但它不会加快速度)是交换PRIMARY KEYUNIQUE。这可能会有所帮助,因为 InnoDB 将 PK 与数据“聚集”在一起。

如果范围没有重叠,我会指给你http://mysql.rjweb.org/doc.php/ipranges

那么,有什么办法呢??范围有多“均匀”和“小”?如果它们相当“好”,那么以下将需要一些代码,但应该快得多。 (在您的示例中,100000 500000 非常丑陋,稍后您将看到。)

将存储桶定义为例如 floor(number/100)。然后建立一个关联桶和范围的表。样品:

FirstX  LastX  Bucket
123411  123488  1234
222222  222444  2222
222222  222444  2223
222222  222444  2224
222411  222477  2224

注意一些范围是如何“属于”多个存储桶的。

然后,首先在查询中的存储桶上进行搜索,然后在详细信息上进行搜索。寻找 X=222433 会找到两行 bucket=2224,然后确定两者都可以。但是对于X=222466,两行有bucket,但是只有一个匹配firstXlastX。

WHERE bucket = FLOOR(X/100)
  AND firstX <= X
  AND X <= lastX

INDEX(bucket, firstX)

但是...对于100000 500000,将有 4001 行,因为这个范围在很多“桶”中。

B 计划(应对广泛的范围)

将范围分为宽范围和窄范围。通过简单的表扫描做大范围,通过我的桶方法做窄范围。 UNION ALL结果一起。希望“宽”表比“窄”表小很多。

【讨论】:

  • 好主意,但我的表中 FirstX 和 LastX 之间的最大差异是 4200000,因此单个范围需要很多行。
  • 好的,我添加了一个 kludge 来处理较大的差异。
【解决方案3】:

您需要在 LastX 上添加另一个索引。

唯一索引 FirstLastXPriority_Index (FirstX,LastX,P) 表示这些值的串联,因此与 'AND LastX >= ?' 将无用WHERE 子句的一部分。

【讨论】:

  • 我添加了第二个索引,现在它只使用第二个索引(在 LastX 列上)。不应该两者都用吗?至于运行时间,它比原来的更快(对于我刚刚测试的单个查询),但仍然很慢 - 0.2 秒。
  • MySQL 基本上从不同时使用两个索引。而且我认为它甚至不会在这里考虑。
【解决方案4】:

似乎使查询快速的唯一方法是减少获取和比较字段的数量。这是想法。

我们可以声明一个新的索引字段(例如 UNSIGNED BIGINT)并使用其中一个字段的偏移量将 FistX 和 LastX 这两个值存储在其中。

例如:

FirstX     LastX      CombinedX
100000     500000     100000500000
150000     220000     150000220000
180000     190000     180000190000
550000     660000     550000660000   
70000      90000      070000090000 
75         85         000075000085

另一种方法是将字段声明为DECIMAL 并将 FirstX + LastX / MAX(LastX) 存储在其中。 稍后查找满足将值与单个字段 CombinedX 进行比较的条件的值。

附加

然后您可以获取仅检查一个字段的行: 通过类似 where param1=160000

SELECT * FROM new_table 
WHERE
(CombinedX <= 160000*1000000) AND
(CombinedX % 1000000 >= 160000);

这里我假设对于所有 FistX

【讨论】:

  • 使用您建议的 CombinedX 列或 DECIMAL 列的查询看起来如何?
  • @Eran 我附加了答案。
  • 我的原始查询中只有一个参数。我需要找到满足 FirstX = x 的行,所以我不确定您的示例中的两个参数 (param1=100000 and param2=120000) 指的是什么。
  • 这没有任何意义。真的 - 一点也不。
  • 一个有趣的想法,但我不确定索引对于CombinedX % 1000000 &gt;= 160000 的性能有多好,因为这相当于(CombinedX &gt;= 160000 AND CombinedX &lt; 1000000) OR (CombinedX &gt;= 1160000 AND CombinedX &lt; 2000000) OR ...,它搜索1000000 个不同的范围。
【解决方案5】:

Eran,我相信您自己找到的解决方案在最低成本方面最好的。在优化过程中考虑数据库中数据的分布属性是正常的。此外,在大型系统中,如果不考虑数据的性质,通常不可能达到令人满意的性能。

但是,这种解决方案也有缺点。并且每次数据更改都需要更改配置参数是最少的。更重要的可能是以下。假设有一天,表格中出现了一个非常大的范围。例如,让它的长度覆盖所有可能值的一半。我不知道你的数据的性质,所以我不能肯定知道这样的范围是否会出现,所以这只是一个假设。从观点到结果,都还可以。这只是意味着现在大约每第二个查询将返回一条记录。但是即使只有一个这样的间隔也会完全破坏您的优化,因为条件FirstX &lt;=? AND FirstX&gt; =? - [MAX (LastX-FirstX)] 将不再有效地切断足够的记录。

因此,如果您无法确定是否会出现太长的范围,我建议您保持相同的想法,但从另一面考虑。 我建议,在将新数据加载到表中时,将所有长范围分成较小的长度,长度不超过某个值。你写了The important columns of this table are FirstX, LastX, Y, Z and P。所以你可以一次选择某个数字N,每次加载数据到表中,如果找到LastX-FirstX > N的范围,用几行替换:

FirstX; FirstX + N
FirstX + N; FirstX + 2N
...
FirstX + kN; LastX

并且对于每一行,保持Y、Z和P的值相同。

对于以这种方式准备的数据,您的查询将始终相同:

SELECT P, Y, Z FROM SomeTable WHERE FirstX <=? AND FirstX> =? - N AND LastX> =?

并且总是同样有效。

现在,如何为 N 选择最佳值?我会用不同的值做一些实验,看看什么会更好。并且最优值可能小于当前区间的最大长度 4200000。起初它可能会让人感到惊讶,因为 N 的减少肯定会伴随着表格的增长,因此它可以变得比 430 万大得多。但实际上,当您的查询使用索引足够好时,巨大的表大小不是问题。在这种情况下,随着 N 的减少,索引的使用效率会越来越高。

【讨论】:

  • 这是个好主意,如果我们得到一个带有太大MAX (LastX-FirstX) 的表,我肯定会考虑以这种方式拆分表的行。目前,我对当前解决方案的性能感到满意,因此我认为不需要为当前数据拆分行。 +1
【解决方案6】:

在这种情况下,索引对您没有帮助,除了 X 的所有可能值中的一小部分。

比如说:

  • FirstX 包含从 1 到 1000 均匀分布的值
  • LastX 包含从 1 到 1042 均匀分布的值

你有以下索引:

  1. FirstX, LastX, &lt;covering columns&gt;
  2. LastX, FirstX, &lt;covering columns&gt;

现在:

  • 如果 X 为 50,则子句 FirstX &lt;= 50 匹配大约 5% 的行,而 LastX &gt;= 50 匹配大约 95% 的行。 MySQL 将使用第一个索引。

  • 如果 X 为 990,则子句 FirstX &lt;= 990 匹配大约 99% 的行,而 LastX &gt;= 990 匹配大约 5% 的行。 MySQL 将使用第二个索引。

  • 这两者之间的任何 X 都会导致 MySQL 不使用任何一个索引(我不知道确切的阈值,但 5% 在我的测试中有效)。即使 MySQL 使用了索引,也有太多的匹配项,并且索引很可能被用于覆盖而不是搜索。

您的解决方案是最好的。您正在做的是定义“范围”搜索的上限和下限:

WHERE FirstX <= 500      -- 500 is the middle (worst case) value
AND   FirstX >= 500 - 42 -- range matches approximately 4.3% rows
AND   ...

理论上,即使您在 FirstX 中搜索中间值,这也应该有效。话虽如此,你很幸运获得了 4200000 的价值;可能是因为第一个和最后一个之间的最大差异是较小的百分比。


如果有帮助,您可以在加载数据后执行以下操作:

ALTER TABLE testdata ADD COLUMN delta INT NOT NULL;
UPDATE testdata SET delta = LastX - FirstX;
ALTER TABLE testdata ADD INDEX delta (delta);

这使得选择MAX(LastX - FirstX) 更加容易。


我测试了可以在这种情况下使用的 MySQL SPATIAL INDEXES。不幸的是,我发现空间索引速度较慢并且有很多限制。

【讨论】:

    【解决方案7】:

    编辑:想法 #2

    您可以控制 Java 应用程序吗?因为,老实说,0.3 秒的索引扫描还不错。您的问题是您试图获取一个查询,运行 120,000 次,以获得合理的结束时间。

    如果您确实可以控制 Java 应用程序,您可以让它一次性提交 所有 X 值 - 并且让 SQL 不必进行索引扫描12 万次。或者你甚至可以只在 Java 端编写逻辑,因为它相对容易优化。

    原创:

    您是否尝试过创建多列索引?

    拥有多个索引的问题在于,每个索引只会将其缩小到大约 50% 的记录 - 然后它必须将大约 200 万行索引 A 与大约 200 万行索引 B 进行匹配。

    相反,如果您在同一个索引中获取两列,SQL 引擎可以先执行 Seek 操作以获取记录的开头,然后执行单个索引扫描以获取所需的记录列表。没有一个索引与另一个匹配。

    不过,我建议不要将此作为聚集索引。原因是什么?您不会期待很多结果,因此将索引扫描的结果与表进行匹配不会很耗时。相反,您希望索引尽可能小,以便索引扫描尽可能快地进行。聚集索引表 - 因此聚集索引将具有与表本身相同的扫描速度。同样,您可能不希望索引中出现除 FirstX 和 LastX 之外的任何其他字段 - 使索引尽可能小,以便扫描顺利进行。

    最后,就像您现在所做的那样,您需要提示引擎,因为您不希望从搜索中返回大量数据 - 您要确保它使用该紧凑索引它的扫描(而不是说,“嗯,我最好只进行全表扫描。)

    【讨论】:

    • 除非我不明白您所说的“多列索引”是什么意思,否则我的索引已经有多个列 - UNIQUE KEY FirstLastXPriority_Index (FirstX,LastX,P)。我应该如何改变它?
    • 啊-错过了。在这种情况下,您可以尝试从索引中删除 P。请记住,无论您如何构建查询,都必须进行部分索引扫描,因此您希望索引尽可能小。删除 P 会将索引缩小 33%,将扫描时间减少 33%。如果您只是让它从主表中查找几条记录,那么在索引中没有 P 的情况下它可能会更快。无论如何,再看一遍这个问题,我想我可能会从另一个角度回答另一个答案。
    【解决方案8】:

    一种方法可能是按不同的范围对表进行分区,然后只查询适合某个范围的内容,从而使其需要检查的数量要小得多。这可能不起作用,因为 java 可能更慢。但它可能会减轻数据库的压力。 可能还有一种方法可以不多次查询数据库并拥有更具包容性的 SQL(您可能能够发送值列表并让 sql 将其发送到不同的表)。

    【讨论】:

      【解决方案9】:

      假设您将执行时间缩短到 0.1 秒。结果 3 小时 20 分钟可以接受吗?

      一个简单的事实是,对同一个查询的数千次调用效率非常低。除了数据库必须承受的问题之外,还需要考虑网络流量、磁盘寻道时间和各种处理开销。

      假设您的表中还没有 x 的 120,000 个值,这就是我要开始的地方。我会一次将它们分批插入到一个表中,大约 500 个:

      insert into xvalues (x)
      select 14 union all
      select 18 union all
      select 42 /* and so on */
      

      然后,将您的查询更改为加入xvalues

      我认为仅优化就能让您的运行时间缩短到几分钟或几秒,而不是几小时(基于我多年来所做的许多此类优化)。

      它还为进一步优化打开了大门。如果 x 值可能至少有一些重复项(例如,至少 20% 的值多次出现),那么可能值得研究一种解决方案,您只运行唯一值的查询并插入到 @ 987654325@ 对应每个具有匹配值的x

      通常:您可以批量执行的任何操作都可能会以指数方式胜过您逐行执行的任何操作。

      PS:

      您提到了查询,但存储过程也可以与输入表一起使用。在某些 RDBMS 中,您可以将表作为参数传递。我认为这在 MySQL 中不起作用,但您可以创建一个临时表,调用代码填写并存储过程加入。或以相同方式使用的永久表。不使用临时表的主要缺点是您可能需要关注会话管理或丢弃过时的数据。只有您自己知道这是否适用于您的情况。

      【讨论】:

      • x 的 120000 个值只是我测试查询的输入样本。 x 的实际可能唯一值可能远大于 120000(限制为 2^32)。当 x 的输入到达我的应用程序时,我运行查询,因此我无法对查询进行分组,并且我事先不知道 x 的所有可能值。我可以缓存我在表中获得的值(在这种情况下,我不需要原始查询 - 我将简单地为 x 的每个值存储 y 和 z 的相应值),但我仍然必须支持 new x 的值。
      • 至于Suppose you got the execution time down to 0.1 seconds. Would the resulting 3 hours, twenty minutes be acceptable? - 不,它不会,因为正如您在我发布的答案中看到的那样,我已经有一个解决方法可以在 5.5 分钟内完成,所以任何更好的解决方案都必须有类似的性能。
      • 如果您在值到达时运行查询(我一定在您的问题中错过了这一点),那么乍一看,为每个值单独运行查询是可以的。但如果 10 小时实际上是总运行时间的较小部分。如果值很快到达,您仍然可以从批量处理中受益。然后,您不会为 120K 记录运行一次,而是每分钟运行一次,或者任何合适的时间间隔。关键是,作为一般规则,基于集合的处理速度会呈指数级增长,而不仅仅是基于您当前的数据集。
      【解决方案10】:

      所以,我没有足够的数据来确定运行时间。这仅在 P 列是唯一的情况下才有效?为了让两个索引正常工作,我创建了两个索引和以下查询...

      Index A - FirstX, P, Y, Z
      Index B - P, LastX
      

      这是查询

      select A.P, A.Y, A.Z 
      from 
          (select P, Y, Z from asdf A where A.firstx <= 185000 ) A
          join 
          (select P from asdf A where A.LastX >= 185000 ) B
          ON A.P = B.P
      

      由于某种原因,这似乎比

      更快
      select A.P, A.Y, A.Z 
      from asdf A join asdf B on A.P = B.P
      where A.firstx <= 185000 and B.LastX >= 185000
      

      【讨论】:

      【解决方案11】:

      要优化此查询:

      从 SomeTable WHERE FirstX = ?限制 10;

      您可以使用以下 2 种资源:

      • 降序索引
      • 空间索引

      降序索引:

      一种选择是使用在 FirstX 上下降并在 LastX 上上升的索引。

      https://dev.mysql.com/doc/refman/8.0/en/descending-indexes.html

      类似:

      在 SomeTable 上创建索引 SomeIndex (FirstX DESC, LastX);

      相反,您可以创建索引 (LastX, FirstX DESC)。

      空间索引:

      另一种选择是将空间索引与 (FirstX, LastX) 一起使用。如果您将 FirstX 和 LastX 视为 2D 空间坐标,那么您的搜索就是选择由线 FirstX=0, LastX>=X 分隔的连续地理区域中的点。

      这是一个关于空间索引的链接(不是特定于 MySQL,但有图纸):

      https://docs.microsoft.com/en-us/sql/relational-databases/spatial/spatial-indexes-overview

      【讨论】:

      • 理论上,降序索引无济于事,仅当您按该列 desc 排序时才有用。空间索引是一个很好的建议,但仅适用于 MySQL MyISAM 表。
      【解决方案12】:

      如果这个数字不太大,另一种方法是预先计算解决方案。

      CREATE TABLE SomeTableLookUp (
          X INT NOT NULL
          PrimaryKeyCol BIGINT NOT NULL,
          PRIMARY KEY(X, PrimaryKeyCol)
      );
      

      现在您只需预先填充常量表。

      INSERT INTO SomeTableLookUp
      SELECT X, PrimaryKeyCol
      FROM SomeTable
      JOIN (
         SELECT DISTINCT X FROM SomeTable 
      ) XS
      WHERE XS.X BETWEEN StartX AND EndX 
      

      现在您可以直接选择您的答案。

      SELECT SomeTable.*
      FROM SomeTableLookup
      JOIN SomeTable
      ON SomeTableLookup.PrimaryKeyCol = SomeTable.PrimaryKeyCol
      WHERE SomeTableLookup = ?
      LIMIT 10
      

      【讨论】:

        猜你喜欢
        • 2019-12-09
        • 1970-01-01
        • 1970-01-01
        • 2016-01-13
        • 2015-07-31
        • 1970-01-01
        • 2011-01-29
        • 2023-03-27
        • 1970-01-01
        相关资源
        最近更新 更多