提高 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 行。

对我来说似乎很慢。

有没有办法提高此类查询的性能?有什么建议吗?

【问题讨论】:

一般经验法则:只要在“决策上下文”(wherejoinorder by 等)中使用字段,您就可以在其上放置索引。 查询返回多少个结果? 看起来你的索引已经覆盖了;也许(如果category = 2data 的一个小得多的子集)你可能在第二次引用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 性能的主要内容,如果未能解决你的问题,请参考以下文章

在提高 SQL Query 的查询性能方面需要帮助

如何提高 SQL Server 查询的性能 [关闭]

需要帮助提高 SQL DELETE 性能

[翻译]通过使用正确的search arguments来提高SQL Server数据库的性能

php 提高WP_Query的性能

mysql有哪些饮鸩止渴提高性能的方法