有没有一种方法可以优化按联接表的列排序?


10

这是我的慢查询:

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;

我的数据集平均查询时间为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 |                                              |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+

表结构:

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;

MySQL服务器信息:

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

3
您可以为SQL Fiddle提供索引,表架构和测试数据吗?另外,您的目标时间是几点?您是否希望在3秒,1秒,50毫秒内完成?您在各个表1k,100k,100M中有多少条记录?
Erik

如果您要排序的那些字段没有被索引,并且数据集真的很大,您可能正在查看sort_buffer_size问题吗?您可以尝试在会话上修改值,然后运行查询以查看它是否有所改善。
Brian Efting 2015年

您是否尝试在上添加索引(inflow, product_id)
ypercubeᵀᴹ

确保你体面innodb_buffer_pool_size。通常,大约70%的可用RAM是好的。
里克·詹姆斯

Answers:


6

查看表定义将显示您具有相关表之间匹配的索引。这应该导致联接在联接MySQL's逻辑的范围内尽快发生。

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

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

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

从上面显示的表定义和连接中,您可以看到,您永远不会得到最快的排序。这意味着您将依赖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/zh/order-by-optimization.html

物化视图 -对联接表进行排序的另一种方法

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

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

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

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

但这显然是一个繁重的工作,无法在每次更新管理数据的基表后运行。这就是触发器在起作用,以在进行更改时使数据保持最新。这样每一个insertupdate以及delete将传播他们的变化,使用触发器,对物化视图

位于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;

这将在刷新时为您提供物化视图。但是,由于您拥有一个快速移动的数据库,因此您还希望使该视图保持最新。

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

示例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 ;

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

最后:如何使连接表的排序更快?

物化视图正在不断建成的更新向它提出。因此,你可以定义索引(或指数)要用于排序的数据物化视图

如果维护数据的开销不是太重,那么您将花费一些资源(CPU / IO / etc)来进行每个相关的数据更改,以保持物化视图,因此索引数据是最新的并且随时可用。因此,由于您:

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

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

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


6

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

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

这意味着您的临时表和排序文件可能会很大,因为在创建临时表时,将以MAX长度创建字段,而对记录进行排序时,所有字段均以MAX长度创建(UTF8每个字符3个字节)。这些也可能会排除使用内存中临时表的可能性。有关更多信息,请参见内部临时表详细信息

LIMIT在这里也无济于事,因为我们需要物化并排序整个结果集,然后才能知道前三行是什么。

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

[mysqld]
...
tmpdir=/dev/shm  

然后,您将需要重新启动mysqld。

这可能会产生巨大的变化。如果您可能要承受系统的内存压力,则可能需要限制大小(通常,Linux发行版默认将tmpfs限制为总RAM的50%),以避免将内存段交换到磁盘上,甚至OOM情况更糟。您可以通过在以下行中进行编辑来做到这一点/etc/fstab

tmpfs                   /dev/shm                tmpfs   rw,size=2G,noexec,nodev,noatime,nodiratime        0 0

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

mount -o remount,size=2G,noexec,nodev,noatime,nodiratime /dev/shm

您还可以升级到MySQL 5.6(具有高性能的子查询和派生表),并进一步处理查询。从我所见,我认为我们不会看到那条路会取得重大胜利。

祝好运!


感谢您的回答。将tmpdir移到tmpfs可以很好地提高性能。
Stanislav Gamayunov
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.