MySQL Multi JOIN非常慢

时间:2017-12-02 02:23:53

标签: mysql database performance

这是一个问题,我不确定根本原因在哪里,所以我将提供详细信息和我想到的问题点。任何帮助都会很棒(如果你住在附近的话我会喝啤酒)。我有这三个表:

做法:

`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(125) NOT NULL,
`description` text,
`deleted` int(11) unsigned DEFAULT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` varchar(70) NOT NULL,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`last_update_by` varchar(70) NOT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`)

位置:

`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`practice_fk` int(11) unsigned NOT NULL,
`phone` char(12) DEFAULT NULL,
`fax` char(12) DEFAULT NULL,
`address` varchar(125) DEFAULT NULL,
`address_two` varchar(125) DEFAULT NULL,
`city` varchar(40) NOT NULL,
`state` char(2) NOT NULL,
`zip` char(5) DEFAULT NULL,
`lat` decimal(7,5) DEFAULT NULL,
`lng` decimal(7,5) DEFAULT NULL,
`deleted` int(11) unsigned DEFAULT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` varchar(70) NOT NULL,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`last_update_by` varchar(70) NOT NULL,
`email` varchar(150) DEFAULT NULL,
`practice_name_temp` text,
PRIMARY KEY (`id`)

联系人:

`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`location_fk` int(11) unsigned NOT NULL,
`practice_fk` int(11) unsigned NOT NULL,
`fname` varchar(25) NOT NULL,
`lname` varchar(45) NOT NULL,
`phone` varchar(35) DEFAULT NULL,
`mobile` char(12) DEFAULT NULL,
`email` varchar(125) DEFAULT NULL,
`title` varchar(100) DEFAULT NULL,
`description` text,
`deleted` int(11) unsigned DEFAULT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` varchar(70) NOT NULL,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`last_update_by` varchar(70) NOT NULL,
PRIMARY KEY (`id`)

架构背后的基本思想是有一系列实践。练习可以有多个位置,但如果没有与练习相关联,则不能存在位置。然后,练习也可以有多个联系人,但联系人必须与练习和位置相关联。 [这是问题的一部分可能开始的地方]。所以,我有这个问题:

SELECT DISTINCT p.id AS practice_id, 
                p.name, 
                l.id AS location_id, 
                address AS location_address, 
                l.phone AS disp_phone, 
                CONCAT(pc.fname, ' ', pc.lname) AS practiceContact, 
                CONCAT(lc.fname, ' ', lc.lname) AS locationContact,
                pcc.qty AS practice_only_contact_qty,
                lcc.qty AS location_contact_qty,
                (pcc.qty + lcc.qty) AS contactQty
            FROM practices p
            LEFT JOIN practice_locations l on l.practice_fk=p.id
            LEFT JOIN (
                SELECT count(id) AS qty, practice_fk 
                FROM practice_contacts 
                GROUP BY practice_fk
            ) pcc ON pcc.practice_fk=p.id
            LEFT JOIN practice_contacts pc ON pc.practice_fk=pcc.practice_fk AND pcc.qty=1
            LEFT JOIN (
                SELECT count(id) AS qty, location_fk 
                FROM practice_contacts 
                GROUP BY location_fk
            ) lcc ON lcc.location_fk=l.id
            LEFT JOIN practice_contacts lc ON lc.location_fk=lcc.location_fk AND lcc.qty=1
            WHERE p.name IS NOT NULL AND p.deleted IS NULL
            GROUP BY p.id
            ORDER BY p.name ASC, l.state, l.city, l.address;

这个查询应该做的是:

  • 收集练习ID和姓名。
  • 如果只有一个位置,请抓住它的地址。否则,抓住第一个位置的地址
  • 如果有与该练习相关的单个联系人,请抓住他们的名字。否则,抓住第一个联系人的姓名
  • 计算与练习相关联的联系人数
  • 计算与练习相关的位置数
  • 通过练习ID将所有这些组合在一起,然后根据练习名称按字母顺序排序,然后按位置排序

所以,它现在就做了所有这些。非常缓慢。当我在实践表中只有五个记录,而在其他两个表中只有不到20个时,查询效果很好。现在我已经将数据导入到这些表中(实践中约9,000条记录,位置中有14,000多条记录,联系人中有25,000多条记录),此查询需要28秒才能返回我需要的内容。如果我把小组拉出去,我们会看到33秒以上。拧我,对吧?!

显然,这是不可接受的。这个数据集相对较小,而且这个应用程序只会增长,可能有数百万个联系人在这里。所以,我想知道这是否实际上是一个三加分的问题:

  • 第一部分:我应该引入一个参考表[类似于视图]来存储这些关系 - 例如:

    `id` int(11) unsigned not null,
    `practice_fk` int(11) unsigned not null,
    `location_fk` int(11) unsigned not null,
    `contact_fk` int(11) unsigned not null,
    PRIMARY KEY(id),
    KEY(practice_fk),
    KEY(location_fk),
    KEY(contact_fk)
    

    但是,如果我这样做,我不确定如何构建查询以根据需要提取数据?它会提供任何性能优势。

  • 第二部分:我没有适当的索引。在搜索了MySQL文档并绊倒了这篇文章(https://dba.stackexchange.com/questions/75091/why-are-simple-selects-on-innodb-100x-slower-than-on-myisam)后,我开始明白InnoDB是一头慢猪。从UX的角度来看,这是不可接受的,但从架构的角度来看,我被锁定在这个引擎中。如何正确设置索引以使此查询返回到亚秒范围?

  • 第三部分:我的查询是垃圾。我认为这可能是最大的罪魁祸首。我还在学习如何构建这些更复杂的SQL查询,这需要花费一些精力来制作,所以任何关于如何使这个东西不那么猪的指针都会很棒。

我已经尝试过对我的查询进行各种操作(逐个删除,删除顺序等)并且几乎没有任何变化。查询始终在28到33秒之间运行。任何指导都将无法理解。

3 个答案:

答案 0 :(得分:2)

并非所有这些都可以修复,但他们会跳出来作为性能红旗:

  • 不要混用DISTINCTGROUP BY。他们也做同样的事情。
  • 使用InnoDB;你引用的那个链接遭到了极大的反驳 - 作者承认了这一点。
  • 如果LEFT JOIN为您提供所需内容,请勿使用JOINLEFT意味着'权利'表可能缺少行。
  • LEFT JOIN ( SELECT ... )通常无法优化,但JOIN 可能
  • 效率特别低:( SELECT ... ) JOIN ( SELECT ... )
  • " explode-implode":JOINing膨胀行数; GROUP BY然后放气。这是性能问题的常见原因。 (也许我随着时间的推移可以更具体。)
  • COUNT(x)检查x是否为NULL通常,您真正想要的是COUNT(*)

为了清晰阅读,以及"正确性"使用LEFT JOIN时,只需添加' ON中的条件; put'过滤' WHERE中的条件。我认为 AND pcc.qty=1应该从ON移到WHERE。 (我认为可能会更改结果集。)

可能的索引:

p: INDEX(deleted, name, id)
l: INDEX(practice_fk)

EXPLAIN SELECT ...。如果你没有看到" auto-key",那么你有一个旧版本的MySQL;考虑升级。 "自动键"说我对( SELECT ... ) JOIN ( SELECT ... )的评论不适用。否则,请考虑两个CREATE TEMPORARY TABLE并在..._fk上添加索引。然后使用tmp表而不是LEFT JOIN ( SELECT ... )两次。

根据我的评论尽你所能,然后返回修改后的查询,加上EXPLAIN进一步批评(如有必要)。

有关创建索引的更多信息:http://mysql.rjweb.org/doc.php/index_cookbook_mysql

答案 1 :(得分:2)

您似乎并不需要按子查询中的任何内容进行排序。因此,您可以显式设置ORDER BY NULL以提高子查询的性能。

修改后的查询:

SELECT
        DISTINCT p.id AS practice_id,
        p.name,
        l.id AS location_id,
        l.address AS location_address,
        l.phone AS disp_phone,
        CONCAT(pc.fname,
        ' ',
        pc.lname) AS practiceContact,
        CONCAT(lc.fname,
        ' ',
        lc.lname) AS locationContact,
        pcc.qty AS practice_only_contact_qty,
        lcc.qty AS location_contact_qty,
        (pcc.qty + lcc.qty) AS contactQty 
    FROM
        practices p 
    LEFT JOIN
        practice_locations l 
            ON l.practice_fk = p.id 
    LEFT JOIN
        (
            SELECT
                COUNT(practice_contacts.id) AS qty,
                practice_contacts.practice_fk 
            FROM
                practice_contacts 
            GROUP BY
                practice_contacts.practice_fk 
            ORDER BY
                NULL
        ) pcc 
            ON pcc.practice_fk = p.id 
    LEFT JOIN
        practice_contacts pc 
            ON pc.practice_fk = pcc.practice_fk 
            AND pcc.qty = 1 
    LEFT JOIN
        (
            SELECT
                COUNT(practice_contacts.id) AS qty,
                practice_contacts.location_fk 
            FROM
                practice_contacts 
            GROUP BY
                practice_contacts.location_fk 
            ORDER BY
                NULL
        ) lcc 
            ON lcc.location_fk = l.id 
    LEFT JOIN
        practice_contacts lc 
            ON lc.location_fk = lcc.location_fk 
            AND lcc.qty = 1 
    WHERE
        p.name IS NOT NULL 
        AND p.deleted IS NULL 
    GROUP BY
        p.id 
    ORDER BY
        p.name ASC,
        l.state,
        l.city,
        l.address

此外,添加以下可能优化您的查询的索引:

ALTER TABLE `practices` ADD INDEX `practices_index_1` (`deleted`,`id`,`name`);
ALTER TABLE `practice_contacts` ADD INDEX `practice_contacts_index_1` (`practice_fk`,`location_fk`);
ALTER TABLE `practice_contacts` ADD INDEX `practice_contacts_index_2` (`location_fk`);
ALTER TABLE `practice_locations` ADD INDEX `practice_locations_index_1` (`practice_fk`,`id`);

答案 2 :(得分:1)

您可以使用ROW LIMIT语法始终返回第一行,而不是计算联系人和实践的数量。我无法保证它会对性能有所帮助,但担心这两个联接的次数会减少。

        LEFT JOIN (
            SELECT * 
            FROM practice_contacts 
            GROUP BY practice_fk LIMIT 1
        ) pcc ON pcc.practice_fk=p.id
        LEFT JOIN (
            SELECT *
            FROM practice_locations
            GROUP BY practicec_fk LIMIT 1
        ) lcc ON lcc.practice_fk = p.id

我没有验证sql是否可行,但你明白了。如果您需要特定的联系人或位置(例如,最新的),则可以在子选择中包含ORDER BY子句。

请参阅Does MySQL "SELECT LIMIT 1" with multiple records select first record from the top?