我尝试使用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,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
所以,我们插入:
我总是在查询之前运行一个ANALYZE TABLE。
以下是我运行的查询的解释输出:
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)
此查询在~350ms内返回20行。
对我来说似乎很慢。
有没有办法提高此类查询的效果?有什么建议吗?
答案 0 :(得分: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);
具有讽刺意味的是,在第一个提案中,我试图尽量减少对表格的访问,现在我提议进行三次访问。
编辑:30/10
好的,所以我做了一些其他的实验,我想总结一下结果。 首先,我想扩展一下Aruna的答案: 我在OP问题中发现的有趣之处在于它是经典"经验法则的例外。在数据库优化中:如果所需结果的#与所涉及的表的维度相比非常小,则应该可以使用正确的索引来获得非常好的性能。
为什么我们不能简单地添加一个&#34;魔法索引&#34;拥有我们的20行?因为我们没有任何明确的攻击向量&#34; ...我的意思是,我们没有明确的选择标准可以应用于记录,以显着减少目标行的数量。< / p>
想一想:价值必须是&#34; foo&#34;只是从表中删除表的50%。此类别根本没有选择性:唯一感兴趣的是,对于20 uri,它们同时出现在类别1和2的记录中。
但问题在于:问题涉及比较两行,不幸的是,据我所知,索引(甚至不是基于Oracle函数的索引)也无法减少依赖于信息的条件多行。
结论可能是:如果您需要这些查询,则应修改数据模型。例如,如果你有一个有限的和少量的类别(让我们说三个=,你的表可能写成:
uri,value_category1,value_category2,value_category3
查询将是:
选择uri,value_category2 其中value_category1 =&#39; foo&#39;和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
最好的查询仍然是我在第一次环境创建后提出的&#34; query_exists_plus_perimeter&#34;
答案 1 :(得分:0)
主要是由于分析的行数。即使您有表格索引主要决策条件&#34; WHERE d1.category = 1和d1.value =&#39; foo&#39;&#34;过滤大量行
root
它必须再次读取表中的每个匹配行,这对于类别2.由于它在主键上读取,它可以直接获得匹配的行。
在原始表格中,检查类别和值组合的基数。如果它更加独特,您可以在(类别,值)上添加索引,这应该可以提高性能。如果它与给出的示例相同,则可能无法获得任何性能提升。