我正在使用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万行,初始化后永远不会更改。
该表的重要栏目是FirstX
,LastX
,Y
,Z
和P
。
如你所见,我在行FirstX
,LastX
和P
上有一个独特的索引。
列FirstX
和LastX
定义了一系列整数。
我需要在此表上运行的查询获取给定X所有具有FirstX <= X <= LastX的行(即,其范围包含输入数X的所有行)。
例如,如果表包含行(我只包含相关列):
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的查询并对查询的输出执行一些处理。
我找到了一个依赖于表中数据属性的解决方案。我宁愿有一个更通用的解决方案,不依赖于当前数据,但暂时是我所拥有的最好的解决方案。
原始查询的问题:
SELECT P, Y, Z FROM SomeTable WHERE FirstX <= ? AND LastX >= ? LIMIT 10;
当第一个条件FirstX
被大部分行满足时,执行可能需要扫描LastX
,P
,FirstX <= ?
索引中的大部分条目。
我做了什么来减少执行时间是观察到LastX-FirstX
相对较小。
我运行了查询:
SELECT MAX(LastX-FirstX) FROM SomeTable;
并得到4200000
。
这意味着FirstX >= LastX – 4200000
表中的所有行。
因此,为了满足LastX >= ?
,我们还必须满足FirstX >= ? – 4200000
。
所以我们可以为查询添加一个条件,如下所示:
SELECT P, Y, Z FROM SomeTable WHERE FirstX <= ? AND FirstX >= ? - 4200000 AND LastX >= ? LIMIT 10;
在我在问题中测试的示例中,处理的索引条目的数量从2104820
减少到18
,并且运行时间从0.563秒减少到0.0003秒。
我用120000
的X
值测试了新查询。输出与旧查询相同。时间从10多个小时下降到5.5分钟,快了100多倍。
所以,我没有足够的数据来确定运行时间。这只有在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
要优化此查询:
SELECT P,Y,Z FROM SomeTable WHERE FirstX <=? AND LastX> =?限制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)的SPATIAL INDEX。如果您将FirstX和LastX视为2D空间坐标,则您的搜索功能是选择由FirstX <= LastX,FirstX> = 0,LastX> = X行分隔的连续地理区域中的点。
这是空间索引的链接(不是特定于MySQL,但有图纸):
https://docs.microsoft.com/en-us/sql/relational-databases/spatial/spatial-indexes-overview
WHERE col1 < ... AND ... < col2
几乎不可能优化。
任何有用的查询都将涉及col1或col2上的“范围”。两个范围(在两个不同的列上)不能用于单个INDEX
。
因此,您尝试的任何索引都有可能会检查很多表:INDEX(col1, ...)
将从开始扫描col1
命中...
。同样对于col2
和扫描直到结束。
为了增加你的困境,范围是重叠的。所以,你不能拉一个快速的,并添加ORDER BY ... LIMIT 1
快速停止。如果你说LIMIT 10
,但只有9,它将不会停止,直到表的开始/结束。
你可以做的一件简单的事情(但它不会加快速度)是交换PRIMARY KEY
和UNIQUE
。这可能会有所帮助,因为InnoDB会将PK与数据“聚集”在一起。
如果范围没有重叠,我会指向http://mysql.rjweb.org/doc.php/ipranges。
那么,可以做些什么?范围“均匀”和“小”是多少?如果它们合理地“好”,那么下面将采用一些代码,但应该快得多。 (在你的例子中,100000 500000
非常难看,你会在一分钟内看到。)
将桶定义为地板(数量/ 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,两行有桶,但只有一行与firstX和lastX匹配。
WHERE bucket = FLOOR(X/100)
AND firstX <= X
AND X <= lastX
同
INDEX(bucket, firstX)
但是......使用100000 500000
,会有4001行,因为这个范围在很多“桶”中。
B计划(解决范围广泛)
将范围分为宽和窄。通过简单的表扫描进行宽范围,通过我的桶方法进行窄范围。 UNION ALL
将结果汇总在一起。希望“宽”表比“窄”表小得多。
您需要在LastX上添加另一个索引。
唯一索引FirstLastXPriority_Index(FirstX,LastX,P)表示这些值的串联,因此它与'AND LastX> =?'无关。 WHERE子句的一部分。
似乎快速进行查询的唯一方法是减少获取和比较字段的数量。这是个主意。
我们可以声明一个新的索引字段(例如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进行比较。
附加
然后你可以获取只检查一个字段的行:通过类似param1 = 160000的地方
SELECT * FROM new_table
WHERE
(CombinedX <= 160000*1000000) AND
(CombinedX % 1000000 >= 160000);
在这里,我假设所有FistX <LastX。当然,您可以提前计算param1 *偏移量并将其存储在一个变量中,以便进行进一步的比较。当然,您可以考虑不是十进制偏移而是按位移位。选择十进制偏移,因为它们更容易被人阅读以在样本中显示。
伊兰,我相信你发现自己的解决方案在最低成本方面是最好的。在优化过程中考虑数据库中数据的分布属性是正常的。此外,在大型系统中,如果不考虑数据的性质,通常不可能获得令人满意的性能。
但是,这种解决方案也有缺点。每次数据更改时更改配置参数的需求最少。更重要的可能是以下内容。我们假设有一天表中出现了很大的范围。例如,让它的长度覆盖所有可能值的一半。我不知道你的数据的性质,所以我不能确定是否可以出现这样的范围,所以这只是一个假设。从结果来看,没关系。它只是意味着大约每一秒查询现在将返回一个记录。但即使只有一个这样的间隔也会完全扼杀你的优化,因为条件FirstX <=? AND FirstX> =? - [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的情况下,索引将被越来越有效地使用。
在这种情况下,索引不会对您有所帮助,除了X的所有可能值的一小部分。
让我们举例说:
你有以下索引:
FirstX, LastX, <covering columns>
LastX, FirstX, <covering columns>
现在:
FirstX <= 50
子句匹配大约5%的行,而LastX >= 50
匹配大约95%的行。 MySQL将使用第一个索引。FirstX <= 990
子句匹配大约99%的行,而LastX >= 990
匹配大约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。不幸的是,我发现空间索引较慢并且有许多约束。
编辑:想法#2
您是否可以控制Java应用程序?因为,老实说,索引扫描的0.3秒也不错。您的问题是您正在尝试获取查询,运行120,000次,以获得合理的结束时间。
如果您确实可以控制Java应用程序,则可以让它一次提交所有X值 - 并让SQL不必进行120k次索引扫描。或者您甚至可以在Java端编写逻辑,因为它可以相对容易地进行优化。
创见:
您是否尝试过创建多列索引?
拥有多个索引的问题在于,每个索引只会将其缩小到记录的约50% - 然后必须将那些约200万行索引A与约200万行索引B进行匹配。
相反,如果您在同一索引中获得两个列,则SQL引擎可以先执行Seek操作以获取记录的开头,然后执行单个索引扫描以获取所需的记录列表。没有匹配一个索引与另一个。
不过,我建议不要将它作为聚集索引。原因是什么?您并不期望获得很多结果,因此将Index Scan的结果与表格相匹配并不会非常耗时。相反,您希望使索引尽可能小,以便索引扫描尽可能快。聚簇索引是表 - 因此聚簇索引将具有与表本身相同的扫描速度。同样,您可能不希望索引中除FirstX和LastX之外的任何其他字段 - 使索引尽可能小,以便扫描飞行。
最后,就像你现在正在做的那样,你将需要提醒引擎,因为你不期望从搜索中返回大量数据 - 你想确保它使用那个紧凑的索引进行扫描(而不是说,“呃,我只是做一个全表扫描会更好。”
一种方法可能是将表分区不同的范围,然后只查询适合范围的东西,从而使得需要检查的数量要小得多。这可能不起作用,因为java可能会更慢。但它可能会减轻数据库的压力。可能还有一种方法是不要多次查询数据库并拥有更具包容性的SQL(您可能能够发送值列表并让sql将其发送到不同的表)。
假设您的执行时间缩短到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%的值出现多次),那么可能值得调查一个解决方案,在该解决方案中,您只运行查询的唯一值,并为每个值插入SomeTable
x
具有匹配值。
作为一项规则:您可以批量执行的任何操作都可能以指数方式超越您逐行执行的任何操作。
PS:
您引用了一个查询,但存储过程也可以使用输入表。在某些RDBMS中,您可以将表作为参数传递。我认为这不适用于MySQL,但您可以创建一个临时表,调用代码填写并存储过程连接到。或以相同方式使用的永久表。不使用临时表的主要缺点是您可能需要关注会话管理或丢弃陈旧数据。只有您知道这是否适用于您的情况。