提高 self-JOIN SQL Query 性能
Posted
技术标签:
【中文标题】提高 self-JOIN SQL Query 性能【英文标题】:Improve self-JOIN SQL Query performance 【发布时间】:2017-03-06 11:52:32 【问题描述】:我尝试使用 MariaDB 10.1.18 (Linux Debian Jessie) 提高 SQL 查询的性能。
服务器有大量 RAM (192GB) 和 SSD 磁盘。
真实表有数亿行,但我可以在数据子集和简化布局上重现我的性能问题。
这是(简化的)表定义:
CREATE TABLE `data` (
`uri` varchar(255) NOT NULL,
`category` tinyint(4) NOT NULL,
`value` varchar(255) NOT NULL,
PRIMARY KEY (`uri`,`category`),
KEY `cvu` (`category`,`value`,`uri`),
KEY `cu` (`category`,`uri`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
为了重现我的内容的实际分布,我插入了大约 200,000 行这样的行(bash 脚本):
#!/bin/bash
for i in `seq 1 100000`;
do
mysql mydb -e "INSERT INTO data (uri, category, value) VALUES ('uri$i', 1, 'foo');"
done
for i in `seq 99981 200000`;
do
mysql mydb -e "INSERT INTO data (uri, category, value) VALUES ('uri$i', 2, '$(($i % 5))');"
done
所以,我们插入:
类别 1 中的 100'000 行以静态字符串 ("foo") 作为值 类别 2 中的 100'000 行,数值为 1 到 5 之间的数字 20 行在每个数据集之间有一个共同的“uri”(类别 1 / 2)我总是在查询之前运行分析表。
这是我运行的查询的解释输出:
MariaDB [mydb]> EXPLAIN EXTENDED
-> SELECT d2.uri, d2.value
-> FROM data as d1
-> INNER JOIN data as d2 ON d1.uri = d2.uri AND d2.category = 2
-> WHERE d1.category = 1 and d1.value = 'foo';
+------+-------------+-------+--------+----------------+---------+---------+-------------------+-------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+------+-------------+-------+--------+----------------+---------+---------+-------------------+-------+----------+-------------+
| 1 | SIMPLE | d1 | ref | PRIMARY,cvu,cu | cu | 1 | const | 92964 | 100.00 | Using where |
| 1 | SIMPLE | d2 | eq_ref | PRIMARY,cvu,cu | PRIMARY | 768 | mydb.d1.uri,const | 1 | 100.00 | |
+------+-------------+-------+--------+----------------+---------+---------+-------------------+-------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)
MariaDB [mydb]> SHOW WARNINGS;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note | 1003 | select `mydb`.`d2`.`uri` AS `uri`,`mydb`.`d2`.`value` AS `value` from `mydb`.`data` `d1` join `mydb`.`data` `d2` where ((`mydb`.`d1`.`category` = 1) and (`mydb`.`d2`.`uri` = `mydb`.`d1`.`uri`) and (`mydb`.`d2`.`category` = 2) and (`mydb`.`d1`.`value` = 'foo')) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
MariaDB [mydb]> SELECT d2.uri, d2.value FROM data as d1 INNER JOIN data as d2 ON d1.uri = d2.uri AND d2.category = 2 WHERE d1.category = 1 and d1.value = 'foo';
+-----------+-------+
| uri | value |
+-----------+-------+
| uri100000 | 0 |
| uri99981 | 1 |
| uri99982 | 2 |
| uri99983 | 3 |
| uri99984 | 4 |
| uri99985 | 0 |
| uri99986 | 1 |
| uri99987 | 2 |
| uri99988 | 3 |
| uri99989 | 4 |
| uri99990 | 0 |
| uri99991 | 1 |
| uri99992 | 2 |
| uri99993 | 3 |
| uri99994 | 4 |
| uri99995 | 0 |
| uri99996 | 1 |
| uri99997 | 2 |
| uri99998 | 3 |
| uri99999 | 4 |
+-----------+-------+
20 rows in set (0.35 sec)
此查询在 ~350 毫秒内返回 20 行。
对我来说似乎很慢。
有没有办法提高此类查询的性能?有什么建议吗?
【问题讨论】:
一般经验法则:只要在“决策上下文”(where
、join
、order by
等)中使用字段,您就可以在其上放置索引。
查询返回多少个结果?
看起来你的索引已经覆盖了;也许(如果category = 2
是data
的一个小得多的子集)你可能在第二次引用data
作为子查询时会有更好的运气。否则,我的主要建议是重组您的数据,以便您不使用 varchar 或任何字符串类型作为主键(或连接条件)...尤其是不作为 PK 的第一个元素。
如果您使用 EXPLAIN EXTENDED,您还应该通过 SHOW WARNINGS 提供扩展信息。
请提供EXPLAIN FORMAT=JSON SELECT ...;
。
【参考方案1】:
你能试试下面的查询吗?
SELECT dd.uri, max(case when dd.category=2 then dd.value end) v2
FROM data as dd
GROUP by 1
having max(case when dd.category=1 then dd.value end)='foo' and v2 is not null;
目前我无法重复您的测试,但我希望只需扫描一次表就可以补偿聚合函数的使用。
已编辑
创建了一个测试环境并测试了一些假设。 截至今天,最佳性能(100 万行)是:
1 - 在 uri 列上添加索引
2 - 使用以下查询
select d2.uri, d2.value
FROM data as d2
where exists (select 1
from data d1
where d1.uri = d2.uri
AND d1.category = 1
and d1.value='foo')
and d2.category=2
and d2.uri in (select uri from data group by 1 having count(*) > 1);
具有讽刺意味的是,在第一个提案中,我试图最小化对 table 的访问,而现在我提出了三个访问。
编辑:30/10
好的,所以我做了一些其他的实验,我想总结一下结果。 首先,我想扩展一下 Aruna 的答案: 我在 OP 问题中发现有趣的是,它是数据库优化中经典“经验法则”的一个例外:如果所需结果的数量与所涉及的表的维度相比非常小,则应该可以使用正确的索引具有非常好的性能。
为什么我们不能简单地添加一个“魔术索引”来拥有我们的 20 行?因为我们没有任何明确的“攻击向量”。我的意思是,没有明确的选择标准可以应用于记录以显着减少目标行数。
想一想:值必须是“foo”这一事实只是从等式中删除了 50% 的表格。此外,该类别根本没有选择性:唯一感兴趣的是,对于 20 个 uri,它们同时出现在类别 1 和 2 的记录中。
但问题在于:条件涉及比较两行,不幸的是,据我所知,索引(甚至基于 Oracle 函数的索引)无法减少依赖于多行信息的条件。
结论可能是:如果您需要这种查询,您应该修改您的数据模型。例如,如果您的类别数量有限且数量较少(假设为三个=,则您的表可能会写为:
uri、value_category1、value_category2、value_category3
查询将是:
选择 uri,value_category2 其中 value_category1='foo' 且 value_category2 不为空;
顺便说一句,让我们回到最初的问题。 我创建了一个更高效的测试数据生成器 (http://pastebin.com/DP8Uaj2t)。
我用过这张桌子:
use mydb;
DROP TABLE IF EXISTS data2;
CREATE TABLE data2
(
uri varchar(255) NOT NULL,
category tinyint(4) NOT NULL,
value varchar(255) NOT NULL,
PRIMARY KEY (uri,category),
KEY cvu (category,value,uri),
KEY ucv (uri,category,value),
KEY u (uri),
KEY cu (category,uri)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
结果是:
+--------------------------+----------+----------+----------+
| query_descr | num_rows | num | num_test |
+--------------------------+----------+----------+----------+
| exists_plus_perimeter | 10000 | 0.0000 | 5 |
| exists_plus_perimeter | 50000 | 0.0000 | 5 |
| exists_plus_perimeter | 100000 | 0.0000 | 5 |
| exists_plus_perimeter | 500000 | 2.0000 | 5 |
| exists_plus_perimeter | 1000000 | 4.8000 | 5 |
| exists_plus_perimeter | 5000000 | 26.7500 | 8 |
| max_based | 10000 | 0.0000 | 5 |
| max_based | 50000 | 0.0000 | 5 |
| max_based | 100000 | 0.0000 | 5 |
| max_based | 500000 | 3.2000 | 5 |
| max_based | 1000000 | 7.0000 | 5 |
| max_based | 5000000 | 49.5000 | 8 |
| max_based_with_ucv | 10000 | 0.0000 | 5 |
| max_based_with_ucv | 50000 | 0.0000 | 5 |
| max_based_with_ucv | 100000 | 0.0000 | 5 |
| max_based_with_ucv | 500000 | 2.6000 | 5 |
| max_based_with_ucv | 1000000 | 7.0000 | 5 |
| max_based_with_ucv | 5000000 | 36.3750 | 8 |
| standard_join | 10000 | 0.0000 | 5 |
| standard_join | 50000 | 0.4000 | 5 |
| standard_join | 100000 | 2.4000 | 5 |
| standard_join | 500000 | 13.4000 | 5 |
| standard_join | 1000000 | 33.2000 | 5 |
| standard_join | 5000000 | 205.2500 | 8 |
| standard_join_plus_perim | 5000000 | 155.0000 | 2 |
+--------------------------+----------+----------+----------+
使用的查询是: - query_max_based_with_ucv.sql - query_exists_plus_perimeter.sql - query_max_based.sql - query_max_based_with_ucv.sql - query_standard_join_plus_perim.sql query_standard_join.sql
最好的查询仍然是我在第一次环境创建后放置的“query_exists_plus_perimeter”。
【讨论】:
有趣。它在我的示例数据集上运行得更快(~160 毫秒)。但在真实数据集(350'000'000 行)上,这是最糟糕的...... 确实很有趣。。我得找时间自己创建一个测试环境来做进一步的分析。 只是想知道.. 也许 ucv(url、category、value)索引可以改善这种查询行为。我会试着找出答案。【参考方案2】:这主要是由于分析的行数。即使您的表索引了主要决策条件“WHERE d1.category = 1 and d1.value = 'foo'”,也会过滤大量行
+------+-------------+-------+-.....-+-------+----------+-------------+
| id | select_type | table | | rows | filtered | Extra |
+------+-------------+-------+-.....-+-------+----------+-------------+
| 1 | SIMPLE | d1 | ..... | 92964 | 100.00 | Using where |
每个匹配的行都必须再次读取类别 2 的表。由于它正在读取主键,因此可以直接获取匹配的行。
在您的原始表格上检查类别和价值组合的基数。如果它更倾向于唯一,您可以在 (category, value) 上添加一个索引,这应该会提高性能。如果它与给出的示例相同,您可能不会获得任何性能改进。
【讨论】:
以上是关于提高 self-JOIN SQL Query 性能的主要内容,如果未能解决你的问题,请参考以下文章