медленный MySQL "INNER JOIN" - PullRequest
       3

медленный MySQL "INNER JOIN"

26 голосов
/ 30 января 2012

На веб-сайте я использую django для выполнения некоторых запросов:

Линия Джанго:

CINodeInventory.objects.select_related().filter(ci_class__type='equipment',company__slug=self.kwargs['company'])

генерирует запрос MySQL следующим образом:

SELECT *
FROM `inventory_cinodeinventory`
INNER JOIN `ci_cinodeclass` ON ( `inventory_cinodeinventory`.`ci_class_id` = `ci_cinodeclass`.`class_name` )
INNER JOIN `accounts_companyprofile` ON ( `inventory_cinodeinventory`.`company_id` = `accounts_companyprofile`.`slug` )
INNER JOIN `accounts_companysite` ON ( `inventory_cinodeinventory`.`company_site_id` = `accounts_companysite`.`slug` )
INNER JOIN `accounts_companyprofile` T5 ON ( `accounts_companysite`.`company_id` = T5.`slug` )
WHERE (
`ci_cinodeclass`.`type` = 'equipment'
AND `inventory_cinodeinventory`.`company_id` = 'thecompany'
)
ORDER BY `inventory_cinodeinventory`.`name` ASC

Проблема заключается в том, что для обработки только 40 000 записей в основной таблице требуется 0,5 секунды.

Я проверил все индексы, создаю те, которые требуются для сортировки или объединения: у меня все еще есть проблема.

Самое смешное, что если я заменю последнее ВНУТРЕННЕЕ СОЕДИНЕНИЕ на ЛЕВОЕ СОЕДИНЕНИЕ, запрос будет в 10 раз быстрее! К сожалению, поскольку я использую django для запросов, у меня нет доступа к SQL-запросам, которые он генерирует (я не хочу сам делать необработанный SQL).

для последнего соединения в качестве "ВНУТРЕННЕГО СОЕДИНЕНИЯ", которое EXPLAIN дает:

+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+
| id | select_type | table                     | type   | possible_keys                                                                                            | key                                | key_len | ref                                            | rows  | Extra                           |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+
|  1 | SIMPLE      | accounts_companyprofile   | const  | PRIMARY                                                                                                  | PRIMARY                            | 152     | const                                          |     1 | Using temporary; Using filesort |
|  1 | SIMPLE      | inventory_cinodeinventory | range  | inventory_cinodeinventory_41ddcf59,inventory_cinodeinventory_543518c6,inventory_cinodeinventory_14fe63e9 | inventory_cinodeinventory_543518c6 | 152     | NULL                                           | 42129 | Using where                     |
|  1 | SIMPLE      | T5                        | ALL    | PRIMARY                                                                                                  | NULL                               | NULL    | NULL                                           |     3 | Using join buffer               |
|  1 | SIMPLE      | accounts_companysite      | eq_ref | PRIMARY,accounts_companysite_543518c6                                                                    | PRIMARY                            | 152     | cidb.inventory_cinodeinventory.company_site_id |     1 | Using where                     |
|  1 | SIMPLE      | ci_cinodeclass            | eq_ref | PRIMARY                                                                                                  | PRIMARY                            | 92      | cidb.inventory_cinodeinventory.ci_class_id     |     1 | Using where                     |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+

За последнее присоединение как «LEFT JOIN» я получил:

+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+---------+---------+------------------------------------------------+------+-------------+
| id | select_type | table                     | type   | possible_keys                                                                                            | key     | key_len | ref                                            | rows | Extra       |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+---------+---------+------------------------------------------------+------+-------------+
|  1 | SIMPLE      | accounts_companyprofile   | const  | PRIMARY                                                                                                  | PRIMARY | 152     | const                                          |    1 |             |
|  1 | SIMPLE      | inventory_cinodeinventory | index  | inventory_cinodeinventory_41ddcf59,inventory_cinodeinventory_543518c6,inventory_cinodeinventory_14fe63e9 | name    | 194     | NULL                                           |  173 | Using where |
|  1 | SIMPLE      | accounts_companysite      | eq_ref | PRIMARY                                                                                                  | PRIMARY | 152     | cidb.inventory_cinodeinventory.company_site_id |    1 |             |
|  1 | SIMPLE      | T5                        | eq_ref | PRIMARY                                                                                                  | PRIMARY | 152     | cidb.accounts_companysite.company_id           |    1 |             |
|  1 | SIMPLE      | ci_cinodeclass            | eq_ref | PRIMARY                                                                                                  | PRIMARY | 92      | cidb.inventory_cinodeinventory.ci_class_id     |    1 | Using where |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+---------+---------+------------------------------------------------+------+-------------+

похоже, что для случая "INNER JOIN" MySQL не находит индексов для соединения T5: почему?

Профилирование дает:

starting                            0.000011
checking query cache for query  0.000086
Opening tables                  0.000014
System lock                     0.000005
Table lock                          0.000052
init                            0.000064
optimizing                          0.000021
statistics                          0.000180
preparing                           0.000024
Creating tmp table                  0.000308
executing                           0.000003
Copying to tmp table            0.353414   !!!
Sorting result                  0.037244
Sending data                    0.035168
end                             0.000005
removing tmp table                  0.550974   !!!
end                             0.000009
query end                           0.000003
freeing items                   0.000113
storing result in query cache   0.000009
logging slow query                  0.000002
cleaning up                     0.000004

Так что, похоже, есть шаг, где mysql использует временную таблицу. Этот шаг не происходит с левым соединением, только с внутренним соединением. Я пытался избежать этого, включив в запрос «индекс принудительного соединения», но это не помогло ...

таблицы соединения:

CREATE TABLE IF NOT EXISTS `accounts_companysite` (
  `slug` varchar(50) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `deleted` tinyint(1) NOT NULL,
  `company_id` varchar(50) NOT NULL,
  `name` varchar(128) NOT NULL,
  `address` longtext NOT NULL,
  `city` varchar(64) NOT NULL,
  `zip_code` varchar(6) NOT NULL,
  `state` varchar(32) NOT NULL,
  `country` varchar(2) DEFAULT NULL,
  `phone` varchar(20) NOT NULL,
  `fax` varchar(20) NOT NULL,
  `more` longtext NOT NULL,
  PRIMARY KEY (`slug`),
  KEY `accounts_companysite_543518c6` (`company_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `accounts_companyprofile` (
  `slug` varchar(50) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `deleted` tinyint(1) NOT NULL,
  `name` varchar(128) NOT NULL,
  `address` longtext NOT NULL,
  `city` varchar(64) NOT NULL,
  `zip_code` varchar(6) NOT NULL,
  `state` varchar(32) NOT NULL,
  `country` varchar(2) DEFAULT NULL,
  `phone` varchar(20) NOT NULL,
  `fax` varchar(20) NOT NULL,
  `contract_id` varchar(32) NOT NULL,
  `more` longtext NOT NULL,
  PRIMARY KEY (`slug`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `inventory_cinodeinventory` (
  `uuid` varchar(36) NOT NULL,
  `name` varchar(64) NOT NULL,
  `synopsis` varchar(64) NOT NULL,
  `path` varchar(255) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `deleted` tinyint(1) NOT NULL,
  `root_id` varchar(36) DEFAULT NULL,
  `parent_id` varchar(36) DEFAULT NULL,
  `order` int(11) NOT NULL,
  `ci_class_id` varchar(30) NOT NULL,
  `data` longtext NOT NULL,
  `serial` varchar(64) NOT NULL,
  `company_id` varchar(50) NOT NULL,
  `company_site_id` varchar(50) NOT NULL,
  `vendor` varchar(48) NOT NULL,
  `type` varchar(64) NOT NULL,
  `model` varchar(64) NOT NULL,
  `room` varchar(30) NOT NULL,
  `rack` varchar(30) NOT NULL,
  `rack_slot` varchar(30) NOT NULL,
  PRIMARY KEY (`uuid`),
  KEY `inventory_cinodeinventory_1fb5ff88` (`root_id`),
  KEY `inventory_cinodeinventory_63f17a16` (`parent_id`),
  KEY `inventory_cinodeinventory_41ddcf59` (`ci_class_id`),
  KEY `inventory_cinodeinventory_543518c6` (`company_id`),
  KEY `inventory_cinodeinventory_14fe63e9` (`company_site_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

Я также попытался настроить MySQL, добавив в my.cnf:

join_buffer_size        = 16M
tmp_table_size          = 160M
max_seeks_for_key       = 100

... но это не помогает.

С django легко использовать Postgresql вместо Mysql, поэтому я попробовал его: с тем же запросом и теми же данными в db postgres намного быстрее, чем Mysql: x10, быстрее при использовании INNER JOIN (анализ показывает, что он использует индексы в отличие от Mysql)

У вас есть идея, почему мой MySQL INNER JOIN такой медленный?

РЕДАКТИРОВАТЬ 1:

после некоторого тестирования я уменьшаю проблему до этого запроса:

SELECT *
FROM `inventory_cinodeinventory`
INNER JOIN `accounts_companyprofile` ON `inventory_cinodeinventory`.`company_id` = `accounts_companyprofile`.`slug`
ORDER BY `inventory_cinodeinventory`.`name` ASC

Этот запрос очень медленный, и я не понимаю, почему. Без предложения ORDER BY это быстро, но не с этим, хотя индекс имени установлен:

CREATE TABLE IF NOT EXISTS `inventory_cinodeinventory` (
  `uuid` varchar(36) NOT NULL,
  `name` varchar(64) NOT NULL,
  `synopsis` varchar(64) NOT NULL,
  `path` varchar(255) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  `deleted` tinyint(1) NOT NULL,
  `root_id` varchar(36) DEFAULT NULL,
  `parent_id` varchar(36) DEFAULT NULL,
  `order` int(11) NOT NULL,
  `ci_class_id` varchar(30) NOT NULL,
  `data` longtext NOT NULL,
  `serial` varchar(64) NOT NULL,
  `company_id` varchar(50) NOT NULL,
  `company_site_id` varchar(50) NOT NULL,
  `vendor` varchar(48) NOT NULL,
  `type` varchar(64) NOT NULL,
  `model` varchar(64) NOT NULL,
  `room` varchar(30) NOT NULL,
  `rack` varchar(30) NOT NULL,
  `rack_slot` varchar(30) NOT NULL,
  PRIMARY KEY (`uuid`),
  KEY `inventory_cinodeinventory_1fb5ff88` (`root_id`),
  KEY `inventory_cinodeinventory_63f17a16` (`parent_id`),
  KEY `inventory_cinodeinventory_41ddcf59` (`ci_class_id`),
  KEY `inventory_cinodeinventory_14fe63e9` (`company_site_id`),
  KEY `inventory_cinodeinventory_543518c6` (`company_id`,`name`),
  KEY `name` (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

РЕДАКТИРОВАТЬ 2:

Предыдущий запрос может быть решен с помощью 'FORCE INDEX FOR ORDER BY (name)'. К сожалению, этот совет не работает с первоначальным запросом в моей теме ...

РЕДАКТИРОВАТЬ 3:

Я перестроил базу данных, заменив первичные ключи 'uuid' с varchar на целое: это совсем не помогает ... плохие новости.

РЕДАКТИРОВАТЬ 4:

Я попробовал Mysql 5.5.20: не лучше. Postgresql 8.4 в 10 раз быстрее для этого конкретного запроса.

Я немного изменил запрос (убрал соединение T5):

SELECT *
FROM `inventory_cinodeinventory`
INNER JOIN `ci_cinodeclass` ON ( `inventory_cinodeinventory`.`ci_class_id` = `ci_cinodeclass`.`class_name` )
INNER JOIN `accounts_companyprofile` ON ( `inventory_cinodeinventory`.`company_id` = `accounts_companyprofile`.`slug` )
INNER JOIN `accounts_companysite` ON ( `inventory_cinodeinventory`.`company_site_id` = `accounts_companysite`.`slug` )
WHERE (
`ci_cinodeclass`.`type` = 'equipment'
AND `inventory_cinodeinventory`.`company_id` = 'thecompany'
)
ORDER BY `inventory_cinodeinventory`.`name` ASC

Это работает нормально, но у меня есть некоторые другие запросы, только немного по-другому, где этот трюк не работает.

Фактически, после поиска кажется, что, как только вы соединяете 2 таблицы, которые имеют «много общего», то есть, скажем, половина строк правой таблицы может быть присоединена к тем, которые находятся в левой таблице (это мой случай): Mysql предпочитает использовать сканирование таблицы вместо индекса: быстрее я где-то нашел (!!)

Ответы [ 6 ]

9 голосов
/ 31 января 2012

Ваша настоящая проблема со второй строкой в ​​вашем первом объяснении:

+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+
| id | select_type | table                     | type   | possible_keys                                                                                            | key                                | key_len | ref                                            | rows  | Extra                           |
+----+-------------+---------------------------+--------+----------------------------------------------------------------------------------------------------------+------------------------------------+---------+------------------------------------------------+-------+---------------------------------+   
|  1 | SIMPLE      | inventory_cinodeinventory | range  | inventory_cinodeinventory_41ddcf59,inventory_cinodeinventory_543518c6,inventory_cinodeinventory_14fe63e9 | inventory_cinodeinventory_543518c6 | 152     | NULL                                           | 42129 | Using where                     |

Вы анализируете 42129 строк, используя это предложение WHERE:

AND `inventory_cinodeinventory`.`company_id` = 'thecompany'

Если у вас его еще нет, у вас должен быть индекс инвентаризации_ инвентаря для (company_id, name)

т.е.

ALTER TABLE `inventory_cinodeinventory`
  ADD INDEX `inventory_cinodeinventory__company_id__name` (`company_id`, `name`);

Таким образом, ваши предложения WHERE и ORDER BY не будут завершеныконфликтует, вызывая неправильный выбор индекса, который, кажется, происходит прямо сейчас.

Если у вас do уже есть индекс с этими столбцами, в таком порядке, я бы предложил запустить OPTIMIZE TABLE inventory_cinodeinventory;чтобы увидеть, заставляет ли MySQL использовать правильный индекс.

В общем, у вас есть большая проблема (которая, как я полагаю, связана с дизайном Django, но у меня нет опыта использования этой инфраструктуры) в том, что выесть эти огромные ключи.Все ключи в вашем EXPLAIN имеют длину 152 и 92 байта.Это приводит к гораздо большим индексам, что означает больший доступ к диску, что означает более медленные запросы.Первичные и внешние ключи в идеале должны быть int с или очень короткими varchar столбцами (например, varchar (10)).varchar(50) для этих ключей значительно увеличит время отклика БД.

6 голосов
/ 01 февраля 2012

Как отметил Conspicuous Compiler, у меня определенно будет индекс для вашей первой таблицы, основанный на идентификаторе и имени компании (поэтому часть имени оптимизирована для заказа по предложению).

Хотя я тоже ничего не делал с django, еще одно оптимизирующее ключевое слово MySQL - «STRAIGHT_JOIN», которое говорит оптимизатору выполнить запрос в том порядке, в котором вы его указали. пример:

SELECT STRAIGHT_JOIN * FROM ...

В обоих случаях ваших запросов "Объяснить" он по какой-то причине застрял в том факте, что companyprofile является одной записью, и может попытаться использовать THAT в качестве основы соединения и иначе обработать стек. Выполняя Straight_join, вы говорите MySQL, что ЗНАЕТЕ, что основной таблицей является «Inventory_CINodeInventory», и сначала используете ее… другие таблицы - это скорее таблица «поиска» или «справочная» других простых элементов, которые вы тоже хотите. Я видел только одно ключевое слово, отвечающее на запрос, который не выполнялся полностью (убил задачу через 30 часов), что противоречит данным контрактов правительства с более чем 14 миллионами записей менее чем за 2 часа ... НИЧЕГО, ЧТО В запросе НИЧЕГО изменилось, только этот ключ. (но обязательно включите другой индекс, если это еще не сделано).

КОММЕНТАРИЙ за последние правки в вопросе ...

Вы упоминаете, что запрос медленный с заказом, но БЫСТРЫЙ без него. Сколько записей фактически возвращается из набора результатов. Еще одна тактика, которую я использовал ранее, заключается в том, чтобы обернуть запрос в качестве выбора, чтобы просто получить ответ, а затем применить порядок к результатам OUTER ... Что-то вроде

select *
   from 
      ( select your Entire Query
           from ...
           Without The Order by clause 
      ) as FastResults
   order by
      FastResults.Name

Это, вероятно, выходит за рамки автоматического построения django вашего оператора SQL, но стоит попробовать для доказательства концепции. У вас уже есть рабочий синтаксис для работы, я бы попробовал.

2 голосов
/ 08 февраля 2012

Вы можете попытаться использовать представление при доступе к своим данным:

CREATE VIEW v AS SELECT *
FROM inventory_cinodeinventory
LEFT JOIN ci_cinodeclass ON ( inventory_cinodeinventory.ci_class_id = ci_cinodeclass.class_name )
LEFT JOIN accounts_companyprofile ON ( inventory_cinodeinventory.company_id = accounts_companyprofile.slug )
LEFT JOIN accounts_companysite ON ( inventory_cinodeinventory.company_site_id = accounts_companysite.slug )
LEFT JOIN accounts_companyprofile T5 ON ( accounts_companysite.company_id = T5.slug )
ORDER BY inventory_cinodeinventory.name ASC

Недостатком здесь является то, что вы должны написать «чистый SQL» на сервере. И вам нужно создать модель для этого нового вида.

Edit:
Вы также можете создать представление с внутренними соединениями. Это также может быть быстрее, чем прямой запрос к таблице.

CREATE VIEW v AS SELECT *
FROM inventory_cinodeinventory
INNER JOIN ci_cinodeclass ON ( inventory_cinodeinventory.ci_class_id = ci_cinodeclass.class_name )
INNER JOIN accounts_companyprofile ON ( inventory_cinodeinventory.company_id = accounts_companyprofile.slug )
INNER JOIN accounts_companysite ON ( inventory_cinodeinventory.company_site_id = accounts_companysite.slug )
INNER JOIN accounts_companyprofile T5 ON ( accounts_companysite.company_id = T5.slug )
ORDER BY inventory_cinodeinventory.name ASC
2 голосов
/ 06 февраля 2012

Я заметил, что вы используете:

ENGINE = MyISAM

Просто предположение, но вы можете попробовать переключить движок таблицы на InnoDB.Это намного быстрее, если используется с несколькими запросами на объединение.

ENGINE = InnoDB

Механизм InnoDB нельзя использовать для выполнения полнотекстового поиска, но имеется большая разница с общей производительностью.

0 голосов
/ 16 апреля 2014

Я реализовал исправление для INNER JOIN для Django ORM, он будет использовать STRAIGHT_JOIN в случае заказа с INNER JOIN.Я поговорил с Django core-devs, и мы решили пока сделать это как отдельный бэкэнд.Так что вы можете проверить это здесь: https://pypi.python.org/pypi/django-mysql-fix

0 голосов
/ 03 марта 2012

Сделайте ваши ключи объединения в int без знака

и добавьте inventory_cinodeinventory. ci_class_id> 0 (ci_class_id__gt = 0) (то же самое, что и остальные ключи в соединениях), где

Он будет указывать MySQL на ваши ключи, сохраняя его в стиле ORM в django

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...