【问题标题】:MySQL left join with group by - optimisation of indexesMySQL left join with group by - 索引优化
【发布时间】:2017-05-14 06:11:50
【问题描述】:

我正在尝试优化涉及两个表的左连接,但我无法通过可能的索引来加快速度。 表 1 包含 2171289 行:

text_metadata_for_nzcorpus | CREATE TABLE `text_metadata_for_nzcorpus` (
    `text_id` varchar(255) NOT NULL,
    `newspaper` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
    `year` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
    `month` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
    `day` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
    `section` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
    `subsection` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
    `topics` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
    `words` int(11) NOT NULL DEFAULT '0',
    `cqp_begin` bigint(20) unsigned NOT NULL DEFAULT '0',
    `cqp_end` bigint(20) unsigned NOT NULL DEFAULT '0',
    PRIMARY KEY (`text_id`),
    KEY `newspaper` (`newspaper`),
    KEY `year` (`year`),
    KEY `month` (`month`),
    KEY `day` (`day`),
    KEY `section` (`section`),
    KEY `subsection` (`subsection`),
    KEY `topics` (`topics`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

第二个表只包含8584行:

db_dist_fb8ddyk760 | CREATE TABLE `db_dist_fb8ddyk760` (
    `text_id` varchar(255) COLLATE utf8_bin DEFAULT NULL,
    `beginPosition` int(11) DEFAULT NULL,
    `endPosition` int(11) DEFAULT NULL,
    `refnumber` mediumint(9) NOT NULL AUTO_INCREMENT,
    KEY `refnumber` (`refnumber`),
    KEY `text_id` (`text_id`)
) ENGINE=InnoDB AUTO_INCREMENT=16384 DEFAULT CHARSET=utf8 COLLATE=utf8_bin |

我需要运行以下类型的查询:

SELECT md.day as handle, count(db.text_id) as hits, 
    count(distinct db.text_id) as files FROM text_metadata_for_nzcorpus as md 
    LEFT JOIN db_dist_fb8ddyk760 as db on md.text_id = db.text_id 
    GROUP BY md.day;

这目前需要 5 秒以上的时间来处理。由于它是在网页上显示输出之前需要运行的众多查询之一,因此如果可能的话,我想加快速度。这是“解释”的输出:

+----+-------------+-------+-------+---------------+---------+---------+----------------------+---------+--------------------------+
| id | select_type | table | type  | possible_keys | key     | key_len | ref                  | rows    | Extra                    |
+----+-------------+-------+-------+---------------+---------+---------+----------------------+---------+--------------------------+
|  1 | SIMPLE      | md    | index | day           | day     | 768     | NULL                 | 2452080 | Using index              |
|  1 | SIMPLE      | db    | ref   | text_id       | text_id | 768     | cqpweb_db.md.text_id |       1 | Using where; Using index |
+----+-------------+-------+-------+---------------+---------+---------+----------------------+---------+--------------------------+

任何有用的建议将不胜感激。 (我不是系统的开发人员,也不负责代码本身 - 但如果可以改进,我想向程序员提供输入......)

非常感谢! 塞巴斯蒂安

【问题讨论】:

    标签: mysql optimization indexing left-join


    【解决方案1】:

    您的 EXPLAIN 报告显示您已经为两个表使用了索引,并且您没有为 GROUP BY 使用临时表,并且两个表都使用覆盖索引(“使用索引”)。

    除了创建索引之外,您还可以做一些其他事情:

    • 将 db_dist_fb8ddyk760.text_id 定义为 NOT NULL。这可能会摆脱“使用位置”注释,这意味着它必须评估表达式作为搜索的一部分。这可能会更有效。
    • 将 db_dist_fb8ddyk760.text_id 定义为该表的 PRIMARY KEY,如果这有意义 - 换句话说,如果 text_id 在该表中是唯一的。这样,“type: ref”将变为“type: eq_ref”,这意味着一个唯一的键查找,效率更高一些。但是,如果该表需要为每个 text_id 记录多个命中,当然可以忽略此建议。
    • 足够增加innodb_buffer_pool_size 以便索引可以缓存在内存中。如果您的查询仅从缓冲池中读取索引页,您可以获得更好的性能和更少的磁盘 I/O。
    • 利用MySQL Query Cache,因此如果您再次运行相同的查询,它将重复使用上一个查询的结果。但是,如果这些表中的数据变化比您执行查询的频率高,那么查询缓存可能就没什么用了。
    • 考虑将结果缓存在应用程序内存或 memcached 中。

    你的评论:

    顺便说一句,表 db_dist_fb8ddyk760 很可能只使用一次或两次然后被丢弃。

    那你为什么要把它存储在持久数据库中呢?

    考虑使用内存中的键/值存储,例如 Redis。使每个键对应一天,每个值是一个结构,其中包含点击次数和不同 text_id 的集合。这基本上是在制作一个汇总表(您也可以在 SQL 中这样做),但 Redis 是在内存中的。

    【讨论】:

    • 谢谢。不幸的是, text_id 不能是主键。将尝试您建议的其他事情。
    • 因为它被缓存并且可以在另一个用户执行相同的查询时再次使用 - 这在创建这些数据库时节省了相当多的时间。没有办法提前知道使用特定数据库的频率、使用时间和用户数量。有时 30 个人可能会做同样的事情(这就是缓存有意义的原因),有时用户可能会导致编译一个巨大的表只查看一次输出......我们已经选择了持久数据库选项,因为在总的来说,这似乎是最好的妥协。
    • 另外,“day”并不是我认为你认为的那样...... ;-) “Day”只是一个句柄,可以在文本集合中包含任何级别的注释(在在这种情况下,它确实是一个月中的一天,即 1 到 31 之间的数字)。所有这些都与电子文本语料库的接口有关 - cwb.sourceforge.net/cqpweb.php - 如果您感兴趣的话。
    • 那么“day”是一个名字不好的标识符。
    • 也许 - 但这取决于界面的用户来决定。他们可以称其为“day”或“xyzblabla”或任何他们喜欢的名称 - 在这种情况下,他们选择“day”,因为这就是句柄所包含的内容。然后底层系统根据这个用户输入创建相应的表——它不关心句柄的语义。它只知道它必须是一个 ASCII 字符序列。但我想这已经不在话题范围内了……
    【解决方案2】:

    不要盲目使用VARCHAR(255)。使用对数据有意义的数据类型。其中许多列听起来像数字,而不是字符串。

    假设年+月+日只是DATE 的一部分,请使用数据类型为DATE 的单个列。然后,使用DAY(date_col) 提取日期。

    每个 InnoDB 表都应该有一个PRIMARY KEY。也许(text_id, beginPosition)的组合是独一无二的,可能是PK?

    每一列都是NULL??我对此表示怀疑。将它们设为NOT NULL,除非您有理由使用NULL

    refnumberAUTO_INCREMENT,但不是 PRIMARY KEY??什么给了?

    进行上述更改将有助于一些。但是如上所述的查询注定要扫描整个 2M 行的表并深入到另一个表中。事情是可以做的。但它们将涉及构建和维护一个汇总表。

    【讨论】:

    • 完全同意有一个汇总表......即使它是在给定一天结束时预先汇总的,然后它只完成一次,他们可以合并仅最新一天的条目。
    • 谢谢你 - 一些 cmets:我理解你所说的数字而不是 VARCHAR - 但表格是需要灵活的系统的一部分。从一开始就不清楚在各个列中可以找到哪些类型的数据。是的, (text_id, beginPosition) 的组合是唯一的 - 将对此进行调查,以及有关列为 NULL 的问题。顺便说一句,表 db_dist_fb8ddyk760 很可能只使用一次或两次然后被丢弃。所以我正在寻找第一次工作的优化......
    • 另一个问题...“日”是一个月中的哪一天?或者是其他东西? (我想知道分组的目的是什么。)
    • 你有LEFT,所以可能有很多行以 0 计数返回。如果你跳过这些行怎么办?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-23
    • 1970-01-01
    • 2020-05-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多