【问题标题】:MySQL,composite index for large table queryMySQL,用于大表查询的复合索引
【发布时间】:2011-12-12 02:28:24
【问题描述】:

以下查询在 user_chars(大约 20 毫米记录)和 user_data(大约 10 毫米记录)上运行。查询运行太慢,我想知道更好的复合索引是否可以改善这种情况。

你知道什么是最好的复合索引吗?

SELECT username, title, status  
FROM (  
    SELECT username, title, status  
    FROM user_chars w, user_data r  
    WHERE w.user_id = r.user_id  
    AND (status < '300' OR is_admin = '1')    
    AND (  
        (rating_id = 'rating1' AND rating BETWEEN 55 AND 65)  
        OR (rating_id = 'rating2' AND rating BETWEEN 50 AND 60)  
        OR (rating_id = 'rating3' AND rating BETWEEN 30 AND 40)  
        OR (rating_id = 'rating4' AND rating BETWEEN 90 AND 100)  
        ...  
    )  
    GROUP BY w.user_id  
    HAVING COUNT(*) >= 3  
) data  
WHERE username != '0'  
AND title != '0'

以下是表格:

CREATE TABLE user_data (
  user_id int(10) unsigned NOT NULL AUTO_INCREMENT,
  username decimal(17,14) DEFAULT NULL,
  title decimal(17,14) DEFAULT NULL,
  status smallint(6) unsigned NOT NULL,
  is_admin tinyint(1) NOT NULL DEFAULT '0',
      PRIMARY KEY (user_id),
  KEY username (username),
  KEY title (title),
  KEY status (status),
  KEY is_admin (is_admin),
  KEY chars_avg_index (user_id,username,title,status),
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;


CREATE TABLE user_chars (
  user_id int(10) unsigned NOT NULL,
  rating_id char(32) DEFAULT NULL,
  rating tinyint(3) unsigned NOT NULL,
  PRIMARY KEY (user_id),
  KEY rating_id (rating_id),
  KEY rating (rating),
  KEY chars_index (user_id,rating_id,rating)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;

编辑:添加解释

+----+-------------+------------+--------+-------- ------------------------------------+------------- ----+---------+------------+-------+--------------- --------------------------------------------+ |编号 |选择类型 |表|类型 |可能的键 |关键 | key_len |参考 |行 |额外 | +----+-------------+------------+--------+-------- ------------------------------------+------------- ----+---------+------------+-------+--------------- --------------------------------------------+ | 1 |初级 | |全部 |空 |空 |空 |空 | 3668 |使用位置 | | 2 |派生 | w |范围 | user_id,rating_id,rating,chars_index |字符索引 | 98 |空 | 13215 |使用哪里;使用索引;使用临时的;使用文件排序 | | 2 |派生 | r | eq_ref | PRIMARY,status,is_admin,chars_avg_index |初级 | 4 | w.user_id | 1 |使用位置 | +----+-------------+------------+--------+-------- ------------------------------------+------------- ----+---------+------------+-------+--------------- --------------------------------------------+

【问题讨论】:

  • 我知道您的问题是关于索引而不是查询本身,但是这里使用子查询而不是单个查询的原因是什么?
  • 没有真正的原因。您将如何重写查询?
  • 我用重写的查询编辑了我的答案:]

标签: mysql query-optimization


【解决方案1】:

不幸的是,user_data 表的正确结构阻碍了任何索引的有效使用。

基本上,从user_data 获取的数据的总体条件如下:

WHERE username != '0' AND title != '0' AND (status < '300' OR is_admin = '1')

条件应该在聚合之前应用,否则聚合会处理多余的数据。

当您搜索任何等于其他内容并且条件与 AND 连接时,索引可以发挥最大作用,您的情况正好相反。 因此,为了优化查询,您可以引入一些非规范化列,它可以以某种方式存储 (username != '0' AND title != '0' AND (status

您将结果与user_chars 连接起来,它再次包含多个 OR,但它们都对 rating_id 和 rating 进行操作。由于评级列更具选择性(具有更多不同的值),因此最好将左侧列放在复合索引中(评级,评级_id)。拥有您不再需要的索引 (rating) 和 (rating_id, rating) 上的索引,只需删除它们即可。

现在,我不确定MySQL是否可以自己做优化,所以你需要比较以下查询的执行情况:

SELECT user_id
FROM user_data JOIN user_chars USING (user_id)
WHERE username != '0' AND title != '0' AND (status < '300' OR is_admin = '1')
AND (  
    (rating_id = 'rating1' AND rating BETWEEN 55 AND 65)  
    OR (rating_id = 'rating2' AND rating BETWEEN 50 AND 60)  
    OR (rating_id = 'rating3' AND rating BETWEEN 30 AND 40)  
    OR (rating_id = 'rating4' AND rating BETWEEN 90 AND 100)
)
GROUP BY user_id
HAVING COUNT(*) > 3

第二个:

SELECT user_id
FROM user_data JOIN user_chars USING (user_id)
WHERE username != '0' AND title != '0' AND (status < '300' OR is_admin = '1')
AND rating_id in ('rating1', 'rating2', 'rating3', 'rating4')
AND rating BETWEEN 55 AND 100 -- adjust the lines according to ... in your query
AND (  
    (rating_id = 'rating1' AND rating BETWEEN 55 AND 65)  
    OR (rating_id = 'rating2' AND rating BETWEEN 50 AND 60)  
    OR (rating_id = 'rating3' AND rating BETWEEN 30 AND 40)  
    OR (rating_id = 'rating4' AND rating BETWEEN 90 AND 100)
)
GROUP BY user_id
HAVING COUNT(*) > 3

后一个查询可能执行得更快,因为它包含使用我们索引的显式提示。此外,这两个查询都只选择 user_ids,而不是在聚合期间浪费内存。现在,您可以将最快查询的结果连接回user_data 表:

SELECT username, title, status
FROM (
SELECT user_id
FROM user_data JOIN user_chars USING (user_id)
WHERE username != '0' AND title != '0' AND (status < '300' OR is_admin = '1')
AND rating_id in ('rating1', 'rating2', 'rating3', 'rating4')
AND rating BETWEEN 55 AND 100
AND (  
    (rating_id = 'rating1' AND rating BETWEEN 55 AND 65)  
    OR (rating_id = 'rating2' AND rating BETWEEN 50 AND 60)  
    OR (rating_id = 'rating3' AND rating BETWEEN 30 AND 40)  
    OR (rating_id = 'rating4' AND rating BETWEEN 90 AND 100)
)
GROUP BY user_id
HAVING COUNT(*) > 3
) as user_ids JOIN user_data USING (user_id);

【讨论】:

  • 我针对 Ilmari Karonen 的建议(在引入您的建议索引之前)尝试了您的前两个查询,它们的执行情况完全相同。然后我介绍了索引 (rating, rating_id),它们的性能都完全一样,但是这次 MySQL 没有使用任何索引,所以性能非常差。我现在将看看我是否可以按照您的建议进行非规范化,并会报告。
  • 我也想问一下SELECT * FROM user_data JOIN user_chars USING (user_id)SELECT * FROM user_chars w, user_data r WHERE w.user_id = r.user_id有什么区别
  • 我在user_data 上添加了一个字段flag,它总结了username != '0' AND title != '0' AND (status &lt; '300' OR is_admin = '1')。不幸的是,即使在 indexig (user_id, flag) 之后,MySQL 仍然只使用 user_id 索引。这是为什么呢?
  • @adrien-hingert,索引应该只是(标志),你不要从这里的user_data表中基于user_id进行选择。关于区别,您正在执行的操作称为连接表,使用 JOIN 和 USING 的表示法允许引用不带前缀的公共列。
  • 嗯,奇怪的是MySQL没有使用任何索引。我会看一下解释,似乎 rating_id 的选择性更好。无论如何,我对user_char 的更改建议是行不通的。但是标志字段应该会有所帮助。
【解决方案2】:

当我查看此查询的 EXPLAIN 输出时,看起来 MySQL 在与 user_data 进行联接之前将内部查询的 WHERE 子句应用于 user_chars。因此,在user_chars 中的(rating_id, rating)(没有user_id)上添加索引应该有助于内部查询的WHERE 子句:

ALTER TABLE user_chars ADD INDEX (rating_id, rating);

编辑:此行为取决于每个表中有多少行,因此发布您的 EXPLAIN 输出会有所帮助:]

Edit2:我还会将查询重写如下:

SELECT username, title, status  
FROM user_chars w, user_data r  
WHERE w.user_id = r.user_id  
AND (status < '300' OR is_admin = '1')    
AND (  
    (rating_id = 'rating1' AND rating BETWEEN 55 AND 65)  
    OR (rating_id = 'rating2' AND rating BETWEEN 50 AND 60)  
    OR (rating_id = 'rating3' AND rating BETWEEN 30 AND 40)  
    OR (rating_id = 'rating4' AND rating BETWEEN 90 AND 100)
    ...
)  
AND username != '0'  
AND title != '0'
GROUP BY w.user_id  
HAVING COUNT(*) >= 3  

【讨论】:

  • 我已按照您的建议添加了说明。看到 EXPLAIN 后,您对添加索引的建议是否仍然相同?
【解决方案3】:

这是一个有趣的执行计划。恐怕我无法提供任何特别具体的建议,主要是因为我没有设法提出任何简单的测试数据来说服我的 MySQL 服务器使用相同的计划。

不过,我确实有一些随机建议:

  • 您实际上并不需要嵌套查询 - 您可以使用 HAVING COUNT(*) &gt;= 3 AND username != '0' AND title != '0' 获得相同的效果。或者您可以尝试将usernametitle 条件移动到内部WHERE 子句中。

  • 我的测试表明,即使我在 (is_admin, status) 上创建索引,MySQL 也不够聪明,无法对 status &lt; '300' OR is_admin = '1' 条件使用 index merge 和/或范围优化。创建一个对这两个值都进行编码的列可能是个好主意,最好采用这样一种方式,即您只需要对其进行单个范围比较。

  • 您也可以考虑删除您不需要的任何索引,除非其他查询需要它们。未使用的索引只会占用空间,减慢INSERTs 并混淆查询规划器。

  • 如果您最近没有这样做,请在您的表上运行 ANALYZE TABLE 并查看执行计划是否更改。

【讨论】:

  • [quote]“你真的不需要嵌套查询”...太棒了,这就是我要找的!
猜你喜欢
  • 2015-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多