Existe uma maneira de otimizar a classificação por colunas de tabelas unidas?

10

Esta é minha consulta lenta:

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;

O tempo médio de consulta é de 4,5 segundos no meu conjunto de dados e isso é inaceitável.

Soluções que eu vejo:

Adicione todas as colunas da cláusula order à products_countstabela. Mas eu tenho ~ 10 tipos de pedidos no aplicativo, por isso devo criar muitas colunas e índices. Além disso, products_countstenho atualizações / inserções / exclusões muito intensivas, por isso preciso executar imediatamente a atualização de todas as colunas relacionadas a pedidos (usando gatilhos?).

Existe outra solução?

Explicar:

+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+
| 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 |                                              |
+----+-------------+-----------------+--------+---------------------------------------------+------------------------+---------+----------------------------------+------+----------------------------------------------+

Estrutura de tabelas:

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;

Informações do servidor MySQL:

mysqld  Ver 5.5.45-1+deb.sury.org~trusty+1 for debian-linux-gnu on i686 ((Ubuntu))
Stanislav Gamayunov
fonte
3
Você pode fornecer um SQL Fiddle com índices, esquema de tabela e dados de teste? Também qual é o seu tempo alvo? Você deseja concluir em 3 segundos, 1 segundo, 50 milissegundos? Quantos registros você tem nas várias tabelas 1k, 100k, 100M?
Erik
Se esses campos pelos quais você está classificando não são indexados e o conjunto de dados é realmente grande, talvez você esteja procurando um problema de sort_buffer_size? Você pode tentar modificar o valor da sua sessão e executar a consulta para ver se ela melhora.
18715 Brian Efting #
Você já tentou adicionar um índice (inflow, product_id)?
precisa saber é o seguinte
Certifique-se de ter um decente innodb_buffer_pool_size. Normalmente, cerca de 70% da RAM disponível é boa.
Rick James

Respostas:

6

A revisão das definições da tabela mostra que você possui índices correspondentes nas tabelas envolvidas. Isso deve fazer com que as junções aconteçam o mais rápido possível dentro dos limites da MySQL'slógica de junção.

No entanto, classificar de várias tabelas é mais complexo.

Em 2007, Sergey Petrunia descreveu os três MySQLalgoritmos de classificação em ordem de velocidade para MySQL: http://s.petrunia.net/blog/?m=201407

  1. Use o método de acesso baseado em índice que produz saída ordenada
  2. Use filesort()na 1ª tabela não constante
  3. Coloque o resultado da junção em uma tabela temporária e use filesort()nela

Nas definições e junções da tabela mostradas acima, você pode ver que nunca obterá a classificação mais rápida . Isso significa que você dependerá filesort()dos critérios de classificação que estiver usando.

No entanto, se você projetar e usar uma Visualização Materializada , poderá usar o algoritmo de classificação mais rápido.

Para ver os detalhes definidos para os MySQL 5.5métodos de classificação, consulte: http://dev.mysql.com/doc/refman/5.5/en/order-by-optimization.html

Para MySQL 5.5(neste exemplo) aumentar a ORDER BYvelocidade se você não conseguir MySQLusar índices em vez de uma fase de classificação extra, tente as seguintes estratégias:

• Aumente o sort_buffer_sizevalor da variável.

• Aumente o read_rnd_buffer_sizevalor da variável.

• Use menos RAM por linha declarando as colunas apenas o tamanho necessário para que os valores reais sejam armazenados. [Por exemplo, reduza um varchar (256) para varchar (ActualLongestString)]

• Altere a tmpdirvariável do sistema para apontar para um sistema de arquivos dedicado com grandes quantidades de espaço livre. (Outros detalhes são oferecidos no link acima.)

Há mais detalhes fornecidos na MySQL 5.7documentação para aumentar a ORDERvelocidade, alguns dos quais podem ser comportamentos levemente atualizados :

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

Vistas materializadas - uma abordagem diferente para classificar tabelas unidas

Você aludiu a Visualizações materializadas com sua pergunta referente ao uso de gatilhos. O MySQL não possui funcionalidade embutida para criar uma Visualização Materializada, mas você possui as ferramentas necessárias. Ao usar gatilhos para espalhar a carga, você pode manter a Visualização Materializada até o momento.

A Visualização Materializada é na verdade uma tabela que é preenchida por meio de código processual para criar ou reconstruir a Visualização Materializada e mantida por gatilhos para manter os dados atualizados.

Como você está criando uma tabela que terá um índice , a Visualização Materializada, quando consultada, pode usar o método de classificação mais rápida : Use o método de acesso baseado em índice que produz saída ordenada

Como MySQL 5.5usa acionadores para manter uma Visualização Materializada , você também precisará de um processo, script ou procedimento armazenado para criar a Visualização Materializada inicial .

Mas esse é obviamente um processo muito pesado para ser executado após cada atualização nas tabelas base em que você gerencia os dados. É aí que os gatilhos entram em ação para manter os dados atualizados à medida que as alterações são feitas. Dessa forma insert, cada ,, updatee deletepropagará suas alterações, usando seus gatilhos, para a Visualização Materializada .

A organização FROMDUAL em http://www.fromdual.com/ possui um código de amostra para manter uma Visualização Materializada . Então, ao invés de escrever minhas próprias amostras, vou apontar para as amostras deles:

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

Exemplo 1: Criando uma vista materializada

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;

Isso fornece a visualização materializada no momento da atualização. No entanto, como você possui um banco de dados em movimento rápido, também deseja manter essa visualização o mais atualizada possível.

Portanto, as tabelas de dados base afetadas precisam ter gatilhos para propagar as alterações de uma tabela base para a tabela Vista materializada . Como um exemplo:

Exemplo 2: Inserindo novos dados em uma visão materializada

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 ;

Obviamente, você também precisará de gatilhos para manter a Exclusão de dados de uma visão materializada e atualizar dados em uma visão materializada . Também estão disponíveis amostras para esses gatilhos.

NO ÚLTIMO: Como isso torna mais rápida a classificação das tabelas unidas?

A Visualização Materializada está sendo construída constantemente à medida que as atualizações são feitas. Portanto, você pode definir o índice (ou índices ) que deseja usar para classificar os dados na exibição ou tabela materializada .

Se a sobrecarga de manutenção dos dados não for muito pesada, você estará gastando alguns recursos (CPU / IO / etc) para cada alteração de dados relevante para manter a Visualização Materializada e, portanto, os dados do índice estão atualizados e prontamente disponíveis. Portanto, a seleção será mais rápida, pois você:

  1. Já gastou CPU e IO incrementais para preparar os dados para o seu SELECT.
  2. O índice na Visualização Materializada pode usar o método de classificação mais rápido disponível para o MySQL, a saber Usar o método de acesso baseado em índice que produz saída ordenada .

Dependendo das suas circunstâncias e de como você se sente em relação ao processo geral, convém reconstruir as Visualizações Materializadas todas as noites durante um período lento.

Nota: Nas Microsoft SQL Server Vistas materializadas são referidas as Vistas indexadas e são atualizadas automaticamente com base nos metadados da Vistas indexadas .

RLF
fonte
6

Não há muito o que fazer aqui, mas acho que o principal problema é que você está criando uma tabela temporária bastante grande e classifica o arquivo no disco a cada vez. O motivo é:

  1. Você está usando UTF8
  2. Você está usando alguns campos grandes varchar (255) para classificar

Isso significa que a tabela temporária e o arquivo de classificação podem ser razoavelmente grandes, pois, ao criar a tabela temporária, os campos são criados no comprimento MAX e na classificação dos registros, no comprimento MAX (e UTF8 é de 3 bytes por caractere). Isso provavelmente também impede o uso de uma tabela temporária na memória. Para mais informações, consulte os detalhes das tabelas temporárias internas .

O LIMIT também não nos serve aqui, pois precisamos materializar e ordenar todo o conjunto de resultados antes de saber quais são as três primeiras linhas.

Você já tentou mover seu tmpdir para um sistema de arquivos tmpfs ? Se / tmp ainda não estiver usando tmpfs (o MySQL usa tmpdir=/tmppor padrão no * nix), você pode usar / dev / shm diretamente. No seu arquivo my.cnf:

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

Então você precisaria reiniciar o mysqld.

Isso pode fazer uma enorme diferença. Se você estiver sob pressão de memória no sistema, provavelmente desejará limitar o tamanho (normalmente as distribuições linux limitam os tmpfs a 50% da RAM total por padrão) para evitar a troca de segmentos de memória para o disco, ou mesmo pior uma situação OOM . Você pode fazer isso editando a linha em /etc/fstab:

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

Você também pode redimensioná-lo "online". Por exemplo:

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

Você também pode atualizar para o MySQL 5.6 - que possui subconsultas de desempenho e tabelas derivadas - e brincar com a consulta um pouco mais. Não acho que veremos grandes vitórias nesse caminho, pelo que vejo.

Boa sorte!

Matt Lord
fonte
Obrigado pela sua resposta. Mover tmpdir para tmpfs deu um bom ganho de desempenho.
Stanislav Gamayunov 13/10/2015