有没有办法按连接表的列优化排序?

Sta*_*nov 11 mysql order-by

这是我的慢查询:

SELECT `products_counts`.`cid`
FROM
  `products_counts` `products_counts`

  LEFT OUTER JOIN `products` `products` ON (
  `products_counts`.`product_id` = `products`.`id`
  )
  LEFT OUTER JOIN `trademarks` `trademark` ON (
  `products`.`trademark_id` = `trademark`.`id`
  )
  LEFT OUTER JOIN `suppliers` `supplier` ON (
  `products_counts`.`supplier_id` = `supplier`.`id`
  )
WHERE
  `products_counts`.product_id IN
  (159, 572, 1075, 1102, 1145, 1162, 1660, 2355, 2356, 2357, 3236, 6471, 6472, 6473, 8779, 9043, 9095, 9336, 9337, 9338, 9445, 10198, 10966, 10967, 10974, 11124, 11168, 16387, 16689, 16827, 17689, 17920, 17938, 17946, 17957, 21341, 21352, 21420, 21421, 21429, 21544, 27944, 27988, 30194, 30196, 30230, 30278, 30699, 31306, 31340, 32625, 34021, 34047, 38043, 43743, 48639, 48720, 52453, 55667, 56847, 57478, 58034, 61477, 62301, 65983, 66013, 66181, 66197, 66204, 66407, 66844, 66879, 67308, 68637, 73944, 74037, 74060, 77502, 90963, 101630, 101900, 101977, 101985, 101987, 105906, 108112, 123839, 126316, 135156, 135184, 138903, 142755, 143046, 143193, 143247, 144054, 150164, 150406, 154001, 154546, 157998, 159896, 161695, 163367, 170173, 172257, 172732, 173581, 174001, 175126, 181900, 182168, 182342, 182858, 182976, 183706, 183902, 183936, 184939, 185744, 287831, 362832, 363923, 7083107, 7173092, 7342593, 7342594, 7342595, 7728766)
ORDER BY
  products_counts.inflow ASC,
  supplier.delivery_period ASC,
  trademark.sort DESC,
  trademark.name ASC
LIMIT
  0, 3;
Run Code Online (Sandbox Code Playgroud)

我的数据集的平均查询时间为 4.5 秒,这是不可接受的。

我看到的解决方案:

将 order 子句中的所有列添加到products_counts表中。但是我在应用程序中有大约 10 种订单类型,所以我应该创建很多列和索引。另外products_counts有非常密集的更新/插入/删除,所以我需要立即执行更新所有与订单相关的列(使用触发器?)。

还有其他解决方案吗?

解释:

+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
| id | select_type | table           | type   | possible_keys                               | key                    | key_len | ref                              | rows | Extra                                        |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
|  1 | SIMPLE      | products_counts | range  | product_id_supplier_id,product_id,pid_count | product_id_supplier_id | 4       | NULL                             |  227 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | products        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.product_id  |    1 |                                              |
|  1 | SIMPLE      | trademark       | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products.trademark_id       |    1 |                                              |
|  1 | SIMPLE      | supplier        | eq_ref | PRIMARY                                     | PRIMARY                | 4       | uaot.products_counts.supplier_id |    1 |                                              |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
Run Code Online (Sandbox Code Playgroud)

表结构:

CREATE TABLE `products_counts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_id` int(11) unsigned NOT NULL,
  `supplier_id` int(11) unsigned NOT NULL,
  `count` int(11) unsigned NOT NULL,
  `cid` varchar(64) NOT NULL,
  `inflow` varchar(10) NOT NULL,
  `for_delete` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `cid` (`cid`),
  UNIQUE KEY `product_id_supplier_id` (`product_id`,`supplier_id`),
  KEY `product_id` (`product_id`),
  KEY `count` (`count`),
  KEY `pid_count` (`product_id`,`count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `category_id` int(11) unsigned NOT NULL,
  `trademark_id` int(11) unsigned NOT NULL,
  `photo` varchar(255) NOT NULL,
  `sort` int(11) unsigned NOT NULL,
  `otech` tinyint(1) unsigned NOT NULL,
  `not_liquid` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `applicable` varchar(255) NOT NULL,
  `code_main` varchar(64) NOT NULL,
  `code_searchable` varchar(128) NOT NULL,
  `total` int(11) unsigned NOT NULL,
  `slider` int(11) unsigned NOT NULL,
  `slider_title` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`),
  KEY `category_id` (`category_id`),
  KEY `trademark_id` (`trademark_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `trademarks` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `name` varchar(255) NOT NULL,
  `country_id` int(11) NOT NULL,
  `sort` int(11) unsigned NOT NULL DEFAULT '0',
  `sort_list` int(10) unsigned NOT NULL DEFAULT '0',
  `is_featured` tinyint(1) unsigned NOT NULL,
  `is_direct` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `suppliers` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `external_id` varchar(36) NOT NULL,
  `code` varchar(64) NOT NULL,
  `name` varchar(255) NOT NULL,
  `delivery_period` tinyint(1) unsigned NOT NULL,
  `is_default` tinyint(1) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `external_id` (`external_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Run Code Online (Sandbox Code Playgroud)

MySQL服务器信息:

mysqld  Ver 5.5.45-1+deb.sury.org~trusty+1 for debian-linux-gnu on i686 ((Ubuntu))
Run Code Online (Sandbox Code Playgroud)

RLF*_*RLF 6

查看您的表定义表明您在所涉及的表中具有匹配的索引。这应该会导致连接在MySQL's连接逻辑的限制内尽快发生。

但是,从多个表中排序更为复杂。

2007 年,Sergey PetruniaMySQL按速度顺序描述了 3 种排序算法MySQLhttp : //s.petrunia.net/blog/?m=201407

  1. 使用生成有序输出的基于索引的访问方法
  2. 使用filesort()1日非恒定表
  3. 将连接结果放入临时表并filesort()在其上使用

从上面显示的表定义和连接中,您可以看到您永远不会获得最快的 sort。这意味着您将依赖于filesort()您使用的排序标准。

但是,如果您设计并使用物化视图,您将能够使用最快的排序算法。

要查看为MySQL 5.5排序方法定义的详细信息,请参阅:http : //dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html

为了MySQL 5.5(在本例中)ORDER BY如果您不能MySQL使用索引而不是额外的排序阶段来提高速度,请尝试以下策略:

• 增加sort_buffer_size变量值。

• 增加read_rnd_buffer_size变量值。

• 通过将列声明为存储实际值所需的大小,从而减少每行的 RAM。[例如将 varchar(256) 减少到 varchar(ActualLongestString)]

• 更改tmpdir系统变量以指向具有大量可用空间的专用文件系统。(其他详细信息在上面的链接中提供。)

MySQL 5.7文档中提供了更多细节来提高ORDER速度,其中一些可能是稍微升级的行为:

http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

物化视图- 排序连接表的不同方法

您提到使用触发器的问题时提到了物化视图。MySQL 没有创建物化视图的内置功能,但您拥有所需的工具。通过使用触发器来分散负载,您可以一直保持物化视图

物化视图实际上是一个,其填充通过程序代码来构建或重建物化视图维护由触发器保持数据上的更新。

由于您正在构建一个将具有索引,因此查询时的物化视图可以使用最快的排序方法使用基于索引的访问方法生成有序输出

由于MySQL 5.5使用触发器来维护实体化视图,因此您还需要一个流程、脚本或存储过程来构建初始实体化视图

但这显然太繁重了,无法在每次更新管理数据的基表后运行。这就是触发器发挥作用的地方,以在进行更改时使数据保持最新。这样,每个insertupdatedelete将使用您的触发器将它们的更改传播到物化视图

http://www.fromdual.com/上的 FROMDUAL 组织有用于维护物化视图的示例代码。因此,与其编写我自己的示例,不如将您指向他们的示例:

http://www.fromdual.com/mysql-materialized-views

示例 1:构建物化视图

DROP TABLE sales_mv;
CREATE TABLE sales_mv (
    product_name VARCHAR(128)  NOT NULL
  , price_sum    DECIMAL(10,2) NOT NULL
  , amount_sum   INT           NOT NULL
  , price_avg    FLOAT         NOT NULL
  , amount_avg   FLOAT         NOT NULL
  , sales_cnt    INT           NOT NULL
  , UNIQUE INDEX product (product_name)
);

INSERT INTO sales_mv
SELECT product_name
    , SUM(product_price), SUM(product_amount)
    , AVG(product_price), AVG(product_amount)
    , COUNT(*)
  FROM sales
GROUP BY product_name;
Run Code Online (Sandbox Code Playgroud)

这会在刷新时为您提供物化视图。但是,由于您拥有一个快速移动的数据库,您还希望尽可能保持此视图的最新状态。

因此,受影响的基本数据表需要有触发器来将更改从基本表传播到物化视图表。举个例子:

示例 2:将新数据插入物化视图

DELIMITER $$

CREATE TRIGGER sales_ins
AFTER INSERT ON sales
FOR EACH ROW
BEGIN

  SET @old_price_sum = 0;
  SET @old_amount_sum = 0;
  SET @old_price_avg = 0;
  SET @old_amount_avg = 0;
  SET @old_sales_cnt = 0;

  SELECT IFNULL(price_sum, 0), IFNULL(amount_sum, 0), IFNULL(price_avg, 0)
       , IFNULL(amount_avg, 0), IFNULL(sales_cnt, 0)
    FROM sales_mv
   WHERE product_name = NEW.product_name
    INTO @old_price_sum, @old_amount_sum, @old_price_avg
       , @old_amount_avg, @old_sales_cnt
  ;

  SET @new_price_sum = @old_price_sum + NEW.product_price;
  SET @new_amount_sum = @old_amount_sum + NEW.product_amount;
  SET @new_sales_cnt = @old_sales_cnt + 1;
  SET @new_price_avg = @new_price_sum / @new_sales_cnt;
  SET @new_amount_avg = @new_amount_sum / @new_sales_cnt;

  REPLACE INTO sales_mv
  VALUES(NEW.product_name, @new_price_sum, @new_amount_sum, @new_price_avg
       , @new_amount_avg, @new_sales_cnt)
  ;

END;
$$
DELIMITER ;
Run Code Online (Sandbox Code Playgroud)

当然,您还需要触发器来维护从物化视图中删除数据在物化视图中更新数据。样本也可用于这些触发器。

最后:这如何加快对连接表的排序?

随着对其进行更新,物化视图正在不断构建。因此,您可以定义要用于对物化视图表中的数据进行排序的索引(或索引)。

如果维护数据的开销不是太大,那么您会为每个相关的数据更改花费一些资源(CPU/IO/等)来保持物化视图,从而使索引数据保持最新且随时可用。因此,选择会更快,因为您:

  1. 已经花费了增量 CPU 和 IO 来为您的 SELECT 准备数据。
  2. 实体化视图上的索引可以使用MySQL 可用的最快的排序方法,即使用基于索引的访问方法,产生有序的输出

根据您的情况以及您对整个过程的感受,您可能希望在缓慢期间每晚重建物化视图

注意:Microsoft SQL Server 实体化视图中,索引视图被称为索引视图,并根据索引视图的元数据自动更新。


Mat*_*ord 6

这里没有很多事情要做,但我猜主要问题是您每次都在磁盘上创建一个相当大的临时表和排序文件。原因是:

  1. 您正在使用 UTF8
  2. 您正在使用一些大型 varchar(255) 字段进行排序

这意味着您的临时表和排序文件可能相当大,因为在创建临时表时,字段以 MAX 长度创建,而在对记录进行排序时,所有记录都以 MAX 长度创建(UTF8 是每个字符 3 个字节)。这些也可能妨碍使用内存中的临时表。有关详细信息,请参阅内部临时表详细信息

LIMIT 在这里也对我们没有好处,因为我们需要在知道前 3 行是什么之前对整个结果集进行具体化和排序。

您是否尝试将tmpdir移动到tmpfs文件系统?如果 /tmp 尚未使用 tmpfs(MySQLtmpdir=/tmp在 *nix 上默认使用),那么您可以直接使用 /dev/shm。在您的 my.cnf 文件中:

[mysqld]
...
tmpdir=/dev/shm  
Run Code Online (Sandbox Code Playgroud)

然后你需要重新启动mysqld。

这可能会产生巨大的影响。如果您可能会遇到系统内存压力,您可能希望限制大小(通常 linux 发行版默认情况下将 tmpfs 限制在总 RAM 的 50%)以避免将内存段交换到磁盘,甚至更糟糕的OOM 情况。您可以通过编辑中的行来做到这一点/etc/fstab

tmpfs                   /dev/shm                tmpfs   rw,size=2G,noexec,nodev,noatime,nodiratime        0 0
Run Code Online (Sandbox Code Playgroud)

您也可以“在线”调整它的大小。例如:

mount -o remount,size=2G,noexec,nodev,noatime,nodiratime /dev/shm
Run Code Online (Sandbox Code Playgroud)

您还可以升级到 MySQL 5.6——它具有高性能的子查询和派生表——并更多地使用查询。不过,从我所见,我认为我们不会在这条路线上取得重大胜利。

祝你好运!