【问题标题】:How to optimize count and order by query in millions of row如何在数百万行中通过查询优化计数和排序
【发布时间】:2018-06-08 12:44:50
【问题描述】:

在优化 order by 和 count 查询方面需要帮助,我有数百万(大约 300 万)行的表。

我必须加入 4 个表并获取记录,当我运行简单查询时,它只需要毫秒即可完成,但当我尝试通过离开连接表来计数或排序时,它会无限期地卡住。

请看下面的案例。

数据库服务器配置:

CPU Number of virtual cores: 4
Memory(RAM): 16 GiB
Network Performance: High

每个表中的行:

tbl_customers -  #Rows: 20 million.
tbl_customers_address -  #Row 25 million.
tbl_shop_setting - #Rows 50k
aio_customer_tracking - #Rows 5k

表架构:

CREATE TABLE `tbl_customers` (
    `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    `shopify_customer_id` BIGINT(20) UNSIGNED NOT NULL,
    `shop_id` BIGINT(20) UNSIGNED NOT NULL,
    `email` VARCHAR(225) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
    `accepts_marketing` TINYINT(1) NULL DEFAULT NULL,
    `first_name` VARCHAR(50) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
    `last_name` VARCHAR(50) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
    `last_order_id` BIGINT(20) NULL DEFAULT NULL,
    `total_spent` DECIMAL(12,2) NULL DEFAULT NULL,
    `phone` VARCHAR(20) NULL DEFAULT NULL COLLATE 'latin1_swedish_ci',
    `verified_email` TINYINT(4) NULL DEFAULT NULL,
    `updated_at` DATETIME NULL DEFAULT NULL,
    `created_at` DATETIME NULL DEFAULT NULL,
    `date_updated` DATETIME NULL DEFAULT NULL,
    `date_created` DATETIME NULL DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE INDEX `shopify_customer_id_unique` (`shopify_customer_id`),
    INDEX `email` (`email`),
    INDEX `shopify_customer_id` (`shopify_customer_id`),
    INDEX `shop_id` (`shop_id`)
)
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB;


CREATE TABLE `tbl_customers_address` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `customer_id` BIGINT(20) NULL DEFAULT NULL,
    `shopify_address_id` BIGINT(20) NULL DEFAULT NULL,
    `shopify_customer_id` BIGINT(20) NULL DEFAULT NULL,
    `first_name` VARCHAR(50) NULL DEFAULT NULL,
    `last_name` VARCHAR(50) NULL DEFAULT NULL,
    `company` VARCHAR(50) NULL DEFAULT NULL,
    `address1` VARCHAR(250) NULL DEFAULT NULL,
    `address2` VARCHAR(250) NULL DEFAULT NULL,
    `city` VARCHAR(50) NULL DEFAULT NULL,
    `province` VARCHAR(50) NULL DEFAULT NULL,
    `country` VARCHAR(50) NULL DEFAULT NULL,
    `zip` VARCHAR(15) NULL DEFAULT NULL,
    `phone` VARCHAR(20) NULL DEFAULT NULL,
    `name` VARCHAR(50) NULL DEFAULT NULL,
    `province_code` VARCHAR(5) NULL DEFAULT NULL,
    `country_code` VARCHAR(5) NULL DEFAULT NULL,
    `country_name` VARCHAR(50) NULL DEFAULT NULL,
    `longitude` VARCHAR(250) NULL DEFAULT NULL,
    `latitude` VARCHAR(250) NULL DEFAULT NULL,
    `default` TINYINT(1) NULL DEFAULT NULL,
    `is_geo_fetched` TINYINT(1) NOT NULL DEFAULT '0',
    PRIMARY KEY (`id`),
    INDEX `customer_id` (`customer_id`),
    INDEX `shopify_address_id` (`shopify_address_id`),
    INDEX `shopify_customer_id` (`shopify_customer_id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB;

CREATE TABLE `tbl_shop_setting` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,   
    `shop_name` VARCHAR(300) NOT NULL COLLATE 'latin1_swedish_ci',
     PRIMARY KEY (`id`),
)
COLLATE='utf8mb4_general_ci'
ENGINE=InnoDB;


CREATE TABLE `aio_customer_tracking` (
    `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    `shopify_customer_id` BIGINT(20) UNSIGNED NOT NULL,
    `email` VARCHAR(255) NULL DEFAULT NULL,
    `shop_id` BIGINT(20) UNSIGNED NOT NULL,
    `domain` VARCHAR(255) NULL DEFAULT NULL,
    `web_session_count` INT(11) NOT NULL,
    `last_seen_date` DATETIME NULL DEFAULT NULL,
    `last_contact_date` DATETIME NULL DEFAULT NULL,
    `last_email_open` DATETIME NULL DEFAULT NULL,
    `created_date` DATETIME NOT NULL,
    `is_geo_fetched` TINYINT(1) NOT NULL DEFAULT '0',
    PRIMARY KEY (`id`),
    INDEX `shopify_customer_id` (`shopify_customer_id`),
    INDEX `email` (`email`),
    INDEX `shopify_customer_id_shop_id` (`shopify_customer_id`, `shop_id`),
    INDEX `last_seen_date` (`last_seen_date`)
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB;

正在运行和未运行的查询案例:

1. Running:  Below query fetch the records by joining all the 4 tables, It takes only 0.300 ms.

SELECT `c`.first_name,`c`.last_name,`c`.email, `t`.`last_seen_date`, `t`.`last_contact_date`, `ssh`.`shop_name`, ca.`company`, ca.`address1`, ca.`address2`, ca.`city`, ca.`province`, ca.`country`, ca.`zip`, ca.`province_code`, ca.`country_code`
FROM `tbl_customers` AS `c`
JOIN `tbl_shop_setting` AS `ssh` ON c.shop_id = ssh.id 
LEFT JOIN (SELECT shopify_customer_id, last_seen_date, last_contact_date FROM aio_customer_tracking GROUP BY shopify_customer_id) as t ON t.shopify_customer_id = c.shopify_customer_id
LEFT JOIN `tbl_customers_address` as ca ON (c.shopify_customer_id = ca.shopify_customer_id AND ca.default = 1)
GROUP BY c.shopify_customer_id
LIMIT 20

2. Not running: Simply when try to get the count of these row stuk the query, I waited 10 min but still running.

SELECT 
     COUNT(DISTINCT c.shopify_customer_id)   -- what makes #2 different
FROM `tbl_customers` AS `c`
JOIN `tbl_shop_setting` AS `ssh` ON c.shop_id = ssh.id 
LEFT JOIN (SELECT shopify_customer_id, last_seen_date, last_contact_date FROM aio_customer_tracking GROUP BY shopify_customer_id) as t ON t.shopify_customer_id = c.shopify_customer_id
LEFT JOIN `tbl_customers_address` as ca ON (c.shopify_customer_id = ca.shopify_customer_id AND ca.default = 1)
GROUP BY c.shopify_customer_id
LIMIT 20


3. Not running: In the #1 query we simply put the 1 Order by clause and it get stuck, I waited 10 min but still running. I study query optimization some article and tried by indexing, Right Join etc.. but still not working.

SELECT `c`.first_name,`c`.last_name,`c`.email, `t`.`last_seen_date`, `t`.`last_contact_date`, `ssh`.`shop_name`, ca.`company`, ca.`address1`, ca.`address2`, ca.`city`, ca.`province`, ca.`country`, ca.`zip`, ca.`province_code`, ca.`country_code`
FROM `tbl_customers` AS `c`
JOIN `tbl_shop_setting` AS `ssh` ON c.shop_id = ssh.id 
LEFT JOIN (SELECT shopify_customer_id, last_seen_date, last_contact_date FROM aio_customer_tracking GROUP BY shopify_customer_id) as t ON t.shopify_customer_id = c.shopify_customer_id
LEFT JOIN `tbl_customers_address` as ca ON (c.shopify_customer_id = ca.shopify_customer_id AND ca.default = 1)
GROUP BY c.shopify_customer_id
  ORDER BY `t`.`last_seen_date`    -- what makes #3 different
LIMIT 20

解释查询 #1:

解释查询 #2:

解释查询 #3:

欢迎提出任何优化查询、表结构的建议。

我想做什么:

tbl_customers 表包含客户信息,tbl_customer_address 表包含客户地址(一个客户可能有多个地址),aio_customer_tracking 表包含客户的访问记录,last_seen_date 是访问日期。

现在,我只想获取并统计客户,包括他们的地址之一和访问信息。此外,我可以按这 3 个表中的任何列排序,在我的示例中,我按 last_seen_date 排序(默认顺序)。希望这个解释有助于理解我想要做什么。

【问题讨论】:

  • 很遗憾,您的查询已损坏,并且不清楚您想要得到什么结果,因此我们无法告诉您如何优化(甚至修复)它们。例如:select last_seen_date from table group by id 将为您提供last_seen_date 的随机行(参见例如here),然后您想按它订购。在您的第二个查询中,count(distinct x) group by x 是多余的(它始终为 1,这就是 group by 的点),您对它的任何left join 都没有影响(但同样,您可能想要查询不同的东西)。还有,
  • order by last_seen_date 将首先列出null; 2000 万客户和(最多)5k 客户的last_seen_date 将首先获得 19.995.000 行和null(因此要优化,只需删除order by)。如果您想获取具有实际last_seen_date 的行,您的查询可能会在不到一秒的时间内完成(删除left join 可能已经完成),但同样,我们并不真正知道您想要的结果是什么,所以之前您尝试对其进行优化,尝试使其工作/给出正确的结果。 (如果您添加详细信息/示例数据/所需结果,我们可以为您提供帮助)。
  • @Solarflare,感谢 cmets。实际上,aio_customer_tracking 表包含客户的访问日志,我想获取带有 last_seen_date 的客户,最近看到的客户在顶部,这就是我使用 last_seen_date 订购的原因。希望这是有道理的。
  • 请突出显示这些查询之间的不同代码。
  • 请为每个查询提供EXPLAIN SELECT ...

标签: mysql database database-administration


【解决方案1】:

在查询 #1 中,但不是其他两个,优化器可以使用

UNIQUE INDEX `shopify_customer_id_unique` (`shopify_customer_id`)

将查询缩短为

GROUP BY c.shopify_customer_id
LIMIT 20

这是因为它可以在索引的 20 项之后停止。由于派生表(子查询t)的行数约为 51K,因此查询速度不是很快。

查询 #2 可能很慢,因为优化器未能注意到并删除冗余的DISTINCT。相反,它可能认为它不能在 20 岁之后停止。

查询 #3 必须完全通过表 c 来获得 每个 shopify_customer_id 组。这是因为ORDER BY 阻止了短路到LIMIT 20

GROUP BY 中的列必须包括 SELECT 中的所有非聚合列,但由 group by 列唯一定义的列除外。既然您已经说过单个shopify_customer_id 可以有多个地址,那么获取ca.address1GROUP BY shopify_customer_id 连接是不合适的。同样,对于last_seen_date, last_contact_date,子查询似乎不合适。

aio_customer_tracking 中,这种更改(对“覆盖”索引)可能会有所帮助:

INDEX (`shopify_customer_id`)

INDEX (`shopify_customer_id`, `last_seen_date`, `last_contact_date`)

剖析目标

现在,我只想...数一数顾客

要统计客户,请这样做,但不要尝试将其与“获取”结合起来:

SELECT COUNT(*) FROM tbl_customers;

现在,我只是想获取……客户……

tbl_customers - #Rows:2000 万。

您肯定不想获取 2000 万行!我不想考虑如何尝试做到这一点。请澄清。而且我不会接受通过那么多行进行分页。也许有一个WHERE 子句? WHERE 子句(通常)是优化中最重要的部分!

现在,我只想获取...客户,以及他们的地址和访问信息。

假设WHERE 过滤到“少数”客户,然后JOINing 过滤到另一个表以获取“任何”地址和“任何”访问信息,可能会出现问题和/或效率低下。要求“first”或“last”而不是“any”不会更容易,但可能更有意义。

我可以建议您的用户界面首先找到一些客户,然后如果用户想要,请转到另一个页面,其中包含所有地址和所有访问。或者访问次数可以达到数百次或更多?

另外,我可以按这 3 个表中的任何列排序,在我的示例中,我按 last_seen_date 排序(默认顺序)。

让我们专注于优化WHERE,然后在任何索引的末尾添加last_seen_date

【讨论】:

  • query #1: 是的,这会有所帮助,但我对查询 #1 没问题。 query #2:查询中的任何想法或更改,以便优化器可以注意到限制? query #3:我对地址表中的任何行都没有问题,所以没有问题,我按照建议更改了索引,但没有帮助..我的查询仍然卡住。
【解决方案2】:

shopify_customer_idtbl_customers 表中是唯一的,那么在第二个查询中,为什么在shopify_customer_id 列中使用 distinct 和 group by?

请摆脱它。

【讨论】:

  • LEFT JOIN 表tbl_customers_address 可能包含有关shopify_customer_id 的多条记录,我必须获取唯一的客户行。因此,添加了 group by 和 distinct。
  • @Irfan.gwb - GROUP BY 具有DISTINCT 的效果。 mamun 建议摆脱DISTINCT
  • 如果有多个地址,你要获取哪一个??我的意思是,GROUP BY每个的查询中都是“无效的”。
  • 我对任何地址行都没有问题。
【解决方案3】:

查询 2 包含其他人指出的逻辑错误:count(distinct(c.shopify_customer_id)) 将返回单个值,因此您的 group by 只会使查询复杂化(这可能确实使 MySQL 先按 shopify_customer_id 分组,然后执行 @987654322 @ 这可能是执行时间长的原因

无法优化查询 3 的顺序,因为您正在加入无法索引的子选择。所花费的时间只是系统需要对结果集进行排序的时间。

您的问题的解决方案是:

  1. 将表 tbl_customers_address 的索引shopify_customer_idshopify_customer_id)更改为shopify_customer_idshopify_customer_id,default)以优化以下查询

  2. 使用查询 1(结果)的结果创建一个表,但没有

    LEFT JOIN (SELECT shopify_customer_id, last_seen_date, last_contact_date FROM aio_customer_tracking GROUP BY shopify_customer_id) as t ON t.shopify_customer_id = c.shopify_customer_id.

  3. 更改结果表并为 last_seen_date 和索引添加一列 对于 last_seen_date 和 shopify_customer_id

  4. 为此查询的结果创建一个表(last_Date):

SELECT shopify_customer_id, last_seen_date, last_contact_date FROM aio_customer_tracking GROUP BY shopify_customer_id

  1. 使用表 last_Date 中的值更新结果表

现在您可以使用您创建的索引对按 last_Date 排序的结果表运行查询。

整个过程应该比执行查询 2 或查询 3 花费更少的时间

【讨论】:

    【解决方案4】:

    您的索引太多,这在插入、更新和删除时可能会成为真正的性能杀手,有时还会根据优化设置进行选择。

    另外,删除GROUP BY 声明。

    关于正确使用聚集索引和非聚集索引GROUP BYORDER BYWHERE 和视图,我可以说更多,以进行查询优化。但是,我认为如果您删除一些索引,您的查询会加快很多。 (也许还可以修改您的查询以遵循更严格的 SQL 标准并更加合乎逻辑,但这超出了本问题的范围。)

    还有一件事 - 你对查询结果做了什么?这是否存储在某处并被访问以进行查找、用于计算、用于自动报告、通过 Web 数据库连接显示等?这会有所不同,因为如果您只需要报告/备份或导出到平面文件,那么有更有效的方法来获取这些数据。很多不同的选择取决于你在做什么。

    【讨论】:

    • 删除索引对INSERTs 有一点帮助,但可能会成为SELECTs 的性能杀手。我想不出在 MySQL 中“太多索引”会成为“性能杀手”的情况。 SELECTs 根本不会加速。
    • 完全。我应该更具体地说明查询与插入对性能的影响。老实说,在看到一些我不喜欢的东西以及一些加入和分组后,我只是略过了她/他的问题。答案大多只是解决了 CRUD 的 R,我认为值得解决。在某些情况下,索引过多会降低查询速度,至少在 SQL Server 中是这样,但它们是例外。我应该花更多时间阅读他们的完整问题并在我的答案中提供更多细节。但是,插入肯定会受到比“轻微”更多的影响。
    • 考虑编辑您的答案以反映您刚才所说的内容。
    • 谢谢,刚刚做了。
    猜你喜欢
    • 2018-12-01
    • 2023-01-05
    • 1970-01-01
    • 2015-06-27
    • 1970-01-01
    • 1970-01-01
    • 2021-09-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多