如何在大表上使用LEFT JOIN优化非常慢的SELECT


15

我正在谷歌搜索,自我教育和寻找解决方案数小时,但没有运气。我在这里找到了一些类似的问题,但不是这种情况。

我的桌子:

  • 人(约1000万行)
  • 属性(位置,年龄,...)
  • 人与属性之间的链接(M:M)(约4000万行)

完整转储〜280MB

情况: 我尝试person_id从某些位置(location.attribute_value BETWEEN 3000 AND 7000),性别(gender.attribute_value = 1),出生年份(bornyear.attribute_value BETWEEN 1980 AND 2000)和眼睛颜色(eyecolor.attribute_value IN (2,3))选择所有人的身份()。

这是我的询问女巫用了3〜4 分钟。我想优化:

SELECT person_id
FROM person
    LEFT JOIN attribute location ON location.attribute_type_id = 1 AND location.person_id = person.person_id
    LEFT JOIN attribute gender ON gender.attribute_type_id = 2 AND gender.person_id = person.person_id
    LEFT JOIN attribute bornyear ON bornyear.attribute_type_id = 3 AND bornyear.person_id = person.person_id
    LEFT JOIN attribute eyecolor ON eyecolor.attribute_type_id = 4 AND eyecolor.person_id = person.person_id
WHERE 1
    AND location.attribute_value BETWEEN 3000 AND 7000
    AND gender.attribute_value = 1
    AND bornyear.attribute_value BETWEEN 1980 AND 2000
    AND eyecolor.attribute_value IN (2,3)
LIMIT 100000;

结果:

+-----------+
| person_id |
+-----------+
|       233 |
|       605 |
|       ... |
|   8702599 |
|   8703617 |
+-----------+
100000 rows in set (3 min 42.77 sec)

扩展说明:

+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
| id | select_type | table    | type   | possible_keys                               | key             | key_len | ref                      | rows    | filtered | Extra                    |
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
|  1 | SIMPLE      | bornyear | range  | attribute_type_id,attribute_value,person_id | attribute_value | 5       | NULL                     | 1265229 |   100.00 | Using where              |
|  1 | SIMPLE      | location | ref    | attribute_type_id,attribute_value,person_id | person_id       | 5       | test1.bornyear.person_id |       4 |   100.00 | Using where              |
|  1 | SIMPLE      | eyecolor | ref    | attribute_type_id,attribute_value,person_id | person_id       | 5       | test1.bornyear.person_id |       4 |   100.00 | Using where              |
|  1 | SIMPLE      | gender   | ref    | attribute_type_id,attribute_value,person_id | person_id       | 5       | test1.eyecolor.person_id |       4 |   100.00 | Using where              |
|  1 | SIMPLE      | person   | eq_ref | PRIMARY                                     | PRIMARY         | 4       | test1.location.person_id |       1 |   100.00 | Using where; Using index |
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
5 rows in set, 1 warning (0.02 sec)

分析:

+------------------------------+-----------+
| Status                       | Duration  |
+------------------------------+-----------+
| Sending data                 |  3.069452 |
| Waiting for query cache lock |  0.000017 |
| Sending data                 |  2.968915 |
| Waiting for query cache lock |  0.000019 |
| Sending data                 |  3.042468 |
| Waiting for query cache lock |  0.000043 |
| Sending data                 |  3.264984 |
| Waiting for query cache lock |  0.000017 |
| Sending data                 |  2.823919 |
| Waiting for query cache lock |  0.000038 |
| Sending data                 |  2.863903 |
| Waiting for query cache lock |  0.000014 |
| Sending data                 |  2.971079 |
| Waiting for query cache lock |  0.000020 |
| Sending data                 |  3.053197 |
| Waiting for query cache lock |  0.000087 |
| Sending data                 |  3.099053 |
| Waiting for query cache lock |  0.000035 |
| Sending data                 |  3.064186 |
| Waiting for query cache lock |  0.000017 |
| Sending data                 |  2.939404 |
| Waiting for query cache lock |  0.000018 |
| Sending data                 |  3.440288 |
| Waiting for query cache lock |  0.000086 |
| Sending data                 |  3.115798 |
| Waiting for query cache lock |  0.000068 |
| Sending data                 |  3.075427 |
| Waiting for query cache lock |  0.000072 |
| Sending data                 |  3.658319 |
| Waiting for query cache lock |  0.000061 |
| Sending data                 |  3.335427 |
| Waiting for query cache lock |  0.000049 |
| Sending data                 |  3.319430 |
| Waiting for query cache lock |  0.000061 |
| Sending data                 |  3.496563 |
| Waiting for query cache lock |  0.000029 |
| Sending data                 |  3.017041 |
| Waiting for query cache lock |  0.000032 |
| Sending data                 |  3.132841 |
| Waiting for query cache lock |  0.000050 |
| Sending data                 |  2.901310 |
| Waiting for query cache lock |  0.000016 |
| Sending data                 |  3.107269 |
| Waiting for query cache lock |  0.000062 |
| Sending data                 |  2.937373 |
| Waiting for query cache lock |  0.000016 |
| Sending data                 |  3.097082 |
| Waiting for query cache lock |  0.000261 |
| Sending data                 |  3.026108 |
| Waiting for query cache lock |  0.000026 |
| Sending data                 |  3.089760 |
| Waiting for query cache lock |  0.000041 |
| Sending data                 |  3.012763 |
| Waiting for query cache lock |  0.000021 |
| Sending data                 |  3.069694 |
| Waiting for query cache lock |  0.000046 |
| Sending data                 |  3.591908 |
| Waiting for query cache lock |  0.000060 |
| Sending data                 |  3.526693 |
| Waiting for query cache lock |  0.000076 |
| Sending data                 |  3.772659 |
| Waiting for query cache lock |  0.000069 |
| Sending data                 |  3.346089 |
| Waiting for query cache lock |  0.000245 |
| Sending data                 |  3.300460 |
| Waiting for query cache lock |  0.000019 |
| Sending data                 |  3.135361 |
| Waiting for query cache lock |  0.000021 |
| Sending data                 |  2.909447 |
| Waiting for query cache lock |  0.000039 |
| Sending data                 |  3.337561 |
| Waiting for query cache lock |  0.000140 |
| Sending data                 |  3.138180 |
| Waiting for query cache lock |  0.000090 |
| Sending data                 |  3.060687 |
| Waiting for query cache lock |  0.000085 |
| Sending data                 |  2.938677 |
| Waiting for query cache lock |  0.000041 |
| Sending data                 |  2.977974 |
| Waiting for query cache lock |  0.000872 |
| Sending data                 |  2.918640 |
| Waiting for query cache lock |  0.000036 |
| Sending data                 |  2.975842 |
| Waiting for query cache lock |  0.000051 |
| Sending data                 |  2.918988 |
| Waiting for query cache lock |  0.000021 |
| Sending data                 |  2.943810 |
| Waiting for query cache lock |  0.000061 |
| Sending data                 |  3.330211 |
| Waiting for query cache lock |  0.000025 |
| Sending data                 |  3.411236 |
| Waiting for query cache lock |  0.000023 |
| Sending data                 | 23.339035 |
| end                          |  0.000807 |
| query end                    |  0.000023 |
| closing tables               |  0.000325 |
| freeing items                |  0.001217 |
| logging slow query           |  0.000007 |
| logging slow query           |  0.000011 |
| cleaning up                  |  0.000104 |
+------------------------------+-----------+
100 rows in set (0.00 sec)

表结构:

CREATE TABLE `attribute` (
  `attribute_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `attribute_type_id` int(11) unsigned DEFAULT NULL,
  `attribute_value` int(6) DEFAULT NULL,
  `person_id` int(11) unsigned DEFAULT NULL,
  PRIMARY KEY (`attribute_id`),
  KEY `attribute_type_id` (`attribute_type_id`),
  KEY `attribute_value` (`attribute_value`),
  KEY `person_id` (`person_id`)
) ENGINE=MyISAM AUTO_INCREMENT=40000001 DEFAULT CHARSET=utf8;

CREATE TABLE `person` (
  `person_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `person_name` text CHARACTER SET latin1,
  PRIMARY KEY (`person_id`)
) ENGINE=MyISAM AUTO_INCREMENT=20000001 DEFAULT CHARSET=utf8;

已在具有SSD和1GB RAM的DigitalOcean虚拟服务器上执行查询。

我认为数据库设计可能存在问题。您对改善这种情况有什么建议吗?或者只是调整上面的选择?


4
这就是您为EAV设计支付的价格。您可能要尝试在attribute (person_id, attribute_type_id, attribute_value)
mustaccio 2015年

1
我会尝试将这些指标:(attribute_type_id, attribute_value, person_id)(attribute_type_id, person_id, attribute_value)
ypercubeᵀᴹ

5
并使用InnoDB,丢弃MyISAM。这是2015年,MyiSAM已死。
ypercubeᵀᴹ

2
第一件事-摆脱L​​EFT联接,当您在WHERE条件下使用所有表时,它没有任何作用,有效地将所有联接变为INNER联接(优化器应该能够理解和优化该联接,但最好不要使其变得更难)。第二件事-禁用查询缓存,除非您有充分的理由使用它(=您对其进行了测试并确定对您有帮助)
jkavalik 2015年

2
OT:您将LIMIT与ORDER BY一起使用是否很奇怪?这将返回一些随机的100000行吗?
ibre5041 2015年

Answers:


8

选择一些要包含在中的属性person。以几种组合对它们进行索引-使用复合索引,而不是单列索引。

从本质上讲,这是摆脱EAV性能困扰的唯一方法,而这正是您所处的位置。

这里是更多讨论:http : //mysql.rjweb.org/doc.php/eav, 其中包括使用JSON而不是键值表的建议。


3

为以下项添加索引attribute

  • (person_id, attribute_type_id, attribute_value)
  • (attribute_type_id, attribute_value, person_id)

说明

根据您当前的设计,EXPLAIN您的查询希望检查中的1,265,229 * 4 * 4 * 4 = 80,974,656attribute。您可以通过添加降低这个数字综合指数attribute(person_id, attribute_type_id)。使用该索引的查询只会检查1,而不是4行的每一个locationeyecolorgender

您可以将该索引扩展为包括attribute_type_value(person_id, attribute_type_id, attribute_value)。这会将这个索引变成该查询的覆盖索引,这也将提高性能。

此外,在上添加一个索引(attribute_type_id, attribute_value, person_id)(再次通过包含来覆盖索引person_id),应该比仅在attribute_value需要检查更多行的位置上使用索引提高性能。在这种情况下,它将加快您解释的第一步:从中选择一个范围bornyear

使用这两个索引将查询在我的系统上的执行时间从〜2.0 s减少到〜0.2 s,并且说明输出如下所示:

+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
| id | select_type | table    | type   | possible_keys                       | key               | key_len | ref                            |    rows | filtered | Extra                    |
+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
|  1 | SIMPLE      | bornyear | range  | person_type_value,type_value_person | type_value_person |       9 |                                | 1861881 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | location | ref    | person_type_value,type_value_person | person_type_value |       8 | bornyear.person_id,const       |       1 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | eyecolor | ref    | person_type_value,type_value_person | person_type_value |       8 | bornyear.person_id,const       |       1 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | gender   | ref    | person_type_value,type_value_person | person_type_value |      13 | bornyear.person_id,const,const |       1 |   100.00 | Using index              |
|  1 | SIMPLE      | person   | eq_ref | PRIMARY                             | PRIMARY           |       4 | bornyear.person_id             |       1 |   100.00 | Using index              |
+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+

1
感谢您的广泛答复和解释。我做了您提到的所有内容,但查询仍然需要2分钟左右的时间。请使用哪种表类型(innodb,myisam)以及执行了什么精确查询?
马丁

1
除了添加索引外,我还使用了与您完全相同的数据和定义,因此我确实使用了MyISAM。我将查询的第一行更改为,SELECT person.person_id因为否则显然无法运行。ANALYZE TABLE attribute添加索引后,您做了吗?您可能还想在问题中添加新的EXPLAIN输出(在添加indeces之后)。
wolfgangwalther

3

我认为数据库设计可能存在问题。

您正在使用所谓的Entity-Attribute-Value设计,该设计通常会因设计而表现不佳。

您对改善这种情况有什么建议吗?

设计此关系的经典关系方法是为每个属性创建一个单独的表。在一般情况下,你可以拥有这些独立的表:locationgenderbornyeareyecolor

以下内容取决于是否始终为一个人定义某些属性。并且,一个人是否只能具有一个属性值。例如,通常该人只有一种性别。在您当前的设计中,没有什么可以阻止您为同一个人添加三行的,而其中的性别值不同。您还可以将性别值设置为1或2,而不是一些没有意义的数字,例如987,并且数据库中没有任何约束可以阻止它。但是,这是通过EAV设计维护数据完整性的另一个独立问题。

如果你总是知道该人的性别,那么这是毫无意义的把它放在一个单独的表,它是更好的方式有一个非空列GenderIDperson表中,这将是一个外键查找表的列表所有可能的性别及其名字。如果您大多数时候(但并非总是)知道某人的性别,则可以将此列设置为空并将其设置NULL为无信息时。如果大多数情况下都不知道该人的性别,那么最好有一个单独的表gender,该表链接到person1:1,并且仅对那些具有已知性别的人具有行。

类似的考虑适用于eyecolorbornyear-这个人不太可能为eyecoloror 设置两个值bornyear

如果一个人可能有多个属性值,那么您一定要将其放在单独的表中。例如,一个人拥有多个地址(住所,工作地点,邮政地点,假期等)并不罕见,因此您可以在表格中列出所有地址location。表personlocation将链接为1:M。


或者只是调整上面的选择?

如果使用EAV设计,则至少要执行以下操作。

  • 列集attribute_type_idattribute_valueperson_idNOT NULL
  • 设置attribute.person_id与链接的外键person.person_id
  • 在三列上创建一个索引(attribute_type_id, attribute_value, person_id)。列的顺序在这里很重要。
  • 据我所知,MyISAM不接受外键,所以不要使用它,而应使用InnoDB。

我会这样写查询。使用INNER而不是LEFT联接,并为每个属性显式编写子查询,以使优化程序有使用索引的所有机会。

SELECT person.person_id
FROM
    person
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 1
            AND location.attribute_value BETWEEN 3000 AND 7000
    ) AS location ON location.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 2
            AND location.attribute_value = 1
    ) AS gender ON gender.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 3
            AND location.attribute_value BETWEEN 1980 AND 2000
    ) AS bornyear ON bornyear.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 4
            AND location.attribute_value IN (2, 3)
    ) AS eyecolor ON eyecolor.person_id = person.person_id
LIMIT 100000;

此外,它可能是值得划分attribute由表attribute_type_id


性能警告: JOIN ( SELECT ... )优化效果不佳。 JOINing直接到表的效果更好(但仍然有问题)。
里克·詹姆斯

3

我希望我找到了足够的解决方案。它受本文启发。

简短答案:

  1. 我创建了具有所有属性的1个表。一栏对应一个属性。加上主键列。
  2. 属性值以类似CSV的格式存储在文本单元中(用于全文搜索)。
  3. 创建全文索引。在此之前,重要的是ft_min_word_len=1[mysqld]部分中设置(对于MyISAM)和文件中的innodb_ft_min_token_size=1(对于InnoDb)my.cnf,然后重新启动mysql服务。
  4. 搜索示例:SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000其中123456a 789是人员应在中关联的ID attribute_1。此查询用时不到1秒。

详细答案:

步骤1. 使用全文索引创建表。InnoDb支持MySQL 5.7中的全文索引,因此,如果使用5.5或5.6,则应使用MyISAM。FT搜索有时比InnoDb更快。

CREATE TABLE `person_attribute_ft` (
  `person_id` int(11) NOT NULL,
  `attr_1` text,
  `attr_2` text,
  `attr_3` text,
  `attr_4` text,
  PRIMARY KEY (`person_id`),
  FULLTEXT KEY `attr_1` (`attr_1`),
  FULLTEXT KEY `attr_2` (`attr_2`),
  FULLTEXT KEY `attr_3` (`attr_3`),
  FULLTEXT KEY `attr_4` (`attr_4`),
  FULLTEXT KEY `attr_12` (`attr_1`,`attr_2`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8

步骤2,从EAV(实体属性值)表中插入数据。例如,有问题的示例可以使用1个简单的SQL来完成:

INSERT IGNORE INTO `person_attribute_ft`
SELECT
    p.person_id,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 1 AND a.person_id = p.person_id LIMIT 10) attr_1,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 2 AND a.person_id = p.person_id LIMIT 10) attr_2,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 3 AND a.person_id = p.person_id LIMIT 10) attr_3,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 4 AND a.person_id = p.person_id LIMIT 10) attr_4
FROM person p

结果应该是这样的:

mysql> select * from person_attribute_ft limit 10;
+-----------+--------+--------+--------+--------+
| person_id | attr_1 | attr_2 | attr_3 | attr_4 |
+-----------+--------+--------+--------+--------+
|         1 | 541    | 2      | 1927   | 3      |
|         2 | 2862   | 2      | 1939   | 4      |
|         3 | 6573   | 2      | 1904   | 2      |
|         4 | 2432   | 1      | 2005   | 2      |
|         5 | 2208   | 1      | 1995   | 4      |
|         6 | 8388   | 2      | 1973   | 1      |
|         7 | 107    | 2      | 1909   | 4      |
|         8 | 5161   | 1      | 2005   | 1      |
|         9 | 8022   | 2      | 1953   | 4      |
|        10 | 4801   | 2      | 1900   | 3      |
+-----------+--------+--------+--------+--------+
10 rows in set (0.00 sec)

步骤3.从带有查询的表中进行选择,如下所示:

mysql> SELECT SQL_NO_CACHE *
    -> FROM `person_attribute_ft`
    -> WHERE 1 AND MATCH(attr_1) AGAINST ("3000 3001 3002 3003 3004 3005 3006 3007" IN BOOLEAN MODE)
    -> AND MATCH(attr_2) AGAINST ("1" IN BOOLEAN MODE)
    -> AND MATCH(attr_3) AGAINST ("1980 1981 1982 1983 1984" IN BOOLEAN MODE)
    -> AND MATCH(attr_4) AGAINST ("2,3" IN BOOLEAN MODE)
    -> LIMIT 10000;
+-----------+--------+--------+--------+--------+
| person_id | attr_1 | attr_2 | attr_3 | attr_4 |
+-----------+--------+--------+--------+--------+
|     12131 | 3002   | 1      | 1982   | 2      |
|     51315 | 3007   | 1      | 1984   | 2      |
|    147283 | 3001   | 1      | 1984   | 2      |
|    350086 | 3005   | 1      | 1982   | 3      |
|    423907 | 3004   | 1      | 1982   | 3      |
... many rows ...
|   9423907 | 3004   | 1      | 1982   | 3      |
|   9461892 | 3007   | 1      | 1982   | 2      |
|   9516361 | 3006   | 1      | 1980   | 2      |
|   9813933 | 3005   | 1      | 1982   | 2      |
|   9986892 | 3003   | 1      | 1981   | 2      |
+-----------+--------+--------+--------+--------+
90 rows in set (0.17 sec)

查询选择所有行:

  • 至少匹配以下ID中的一个attr_13000, 3001, 3002, 3003, 3004, 3005, 3006 or 3007
  • 并在同一时间匹配1attr_2(此列表示性别,所以如果在该溶液中定制的,它应该是smallint(1)简单的指数,等...)
  • 并且同时匹配1980, 1981, 1982, 1983 or 1984in 中的至少一个attr_3
  • 并在同一时间匹配23attr_4

结论:

我知道该解决方案在许多情况下都不是完美的理想选择,但可以用作EAV表设计的良好替代方案。

希望对您有所帮助。


1
我发现这种设计的性能要比您的原始设计(带有复合索引)的性能要好得多。您做了什么测试来比较它们?
ypercubeᵀᴹ

0

尝试使用查询索引提示多数民众赞成在看起来合适

Mysql索引提示


1
提示可能会帮助一个版本的查询,但会损害另一个版本。请注意,优化器选择了bornyear作为最佳的第一张表,这可能是因为如果将最不希望的行过滤掉了。
里克·詹姆斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.