2012-01-30 6 views
25

На веб-сайте, я использую Джанго, чтобы сделать некоторые запросы:медленно MySQL "INNER JOIN"

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

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 секунды для обработки.

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

Самое смешное, что если я заменил последний INNER JOIN LEFT JOIN, запрос будет в 10 раз быстрее! К сожалению, поскольку я использую django для запроса, у меня нет доступа к запросам SQL, которые он генерирует (я не хочу сам делать сам SQL).

для последнего присоединиться как "INNER JOIN" 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 использует временную таблицу. Этот шаг не происходит с LEFT JOIN, только с INNER JOIN. Я пытался избежать этого, включив в «индекс силы для присоединиться к» в запросе, но это не помогло ...

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

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, добавив в моем .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 настолько медленный?

EDIT 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; 

EDIT 2:

Предыдущий запрос может быть решена с помощью «СИЛЫ УКАЗАТЕЛЬ ДЛЯ ЗАКАЗА BY (имя). К сожалению, этот совет не работает с первым запросом в моей теме ...

EDIT 3:

я восстановил базу данных с заменой «UUID» первичные ключами от VARCHAR до целого: это не помогает все ... плохие новости.

EDIT 4:

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

Я изменил немного о resquest (удален Т5 присоединиться):

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 

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

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

+1

установить и настроить панель инструментов django-debug, и вы сможете легко увидеть, как sql django генерирует http://pypi.python.org/pypi/django-debug-toolbar , Вам определенно нужен select_related для предполагаемого использования? – shawnwall

+0

Да, мне абсолютно нужен select_related, и Да, я уже использую панель инструментов django-debug: запрос SQL, который я дал, поступает из этого инструмента. (для удобства чтения в запросе я просто помещаю «*» вместо длинного списка запрошенных столбцов) – Eric

+0

В вашем объяснении есть индекс «inventory_cinodeinventory.name», но его нет в вашей схеме. Что-то не в порядке. –

ответ

9

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

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

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

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

Если вы уже не имеете один, вы должны иметь индекс inventory_cinodeinventory для (company_id, name)

т.е.

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

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

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

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

+2

Re: Django - похоже, что OP специально переопределяет автоматически сгенерированное поле ID и использует поле varbar (50) 'slug' в качестве PK. Это определенно закреплено в его определениях модели. –

+0

Мне нужно это «slug» как ПК, по некоторым причинам, все PK должны быть «естественными», ака, быть varchar ... – Eric

+1

@ Конкурирующий компилятор: я пробовал ваши индексы и shorterned slugs, но MySQL по-прежнему имеет «Копирование» to tmp table 'в профилировании. – Eric

6

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

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

SELECT STRAIGHT_JOIN * FROM ... 

В обоих случаях ваши «Объяснения» запросов, он по какой-то причине застрял на факте companyprofile будучи один рекорд и может пытаться использовать это в качестве основы объединения и работает стек в противном случае. Выполняя straight_join, вы говорите MySQL, что вы ЗНАЕТ, что основная таблица - «Inventory_CINodeInventory» и использует ее сначала ... другие таблицы - это скорее таблица поиска или «ссылки» других простых элементов, которые вы также хотите. Я видел только одно ключевое слово для запроса, который не будет выполняться полностью (убил задачу через 30 часов), против данных gov't контрактов более 14 миллионов записей менее чем за 2 часа ... NOTHING ELSE в запросе изменен, только этот КЛЮЧЕВОЙ. (но обязательно включите другой индекс, если он еще не был выполнен).

К.ПА за последние правки вопроса ...

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

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

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

+0

Я пробовал ваш совет, но он не работает ... – Eric

+0

@ Эрик, обновленный с другим вариантом, чтобы попробовать. – DRapp

+0

Хорошая идея, но запрос все еще медленный. – Eric

2

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

ENGINE = MyISAM

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

ДВИГАТЕЛЬ = InnoDB

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

+0

У вас есть источник утверждения о том, что InnoDB работает быстрее с несколькими запросами JOIN? Я знаю, что быстрее для DB, которые довольно сильно пишут, но, как правило, индивидуальный запрос из разгруженной БД будет медленнее с InnoDB, поскольку он блокирует строку за строкой против MyISAM, которая для сопоставимых запросов либо не делает, t заблокировать вообще или заблокировать всю таблицу. –

+1

Хмм, я не должен обобщать. В некоторых случаях это происходит быстрее: [ссылка] (http://www.mysqlperformanceblog.com/2006/05/29/join-performance-of-myisam-and-innodb/). Поиск и сравнение общих индексов в большинстве случаев быстрее: [article] (http: //www.mysqlperformanceblog.com/2007/01/08/innodb-vs-myisam-vs-falcon-benchmarks-part-1 /) и [данные] (http://www.mysqlperformanceblog.com/files/benchmarks/innodb-myisam-falcon .html). – Meonester

+0

К сожалению, я уже пробовал InnoDB: все еще использую tmp table: slow – Eric

2

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

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

Да, левое соединение работает намного лучше, как я сказал в своем вопросе. Я просто ожидаю, что использование «внутреннего соединения» работает так же быстро, как левое соединение, но как ... это вопрос ... – Eric

+0

В моем примере речь шла не о «левом объединении». Речь шла об использовании представления и непосредственном обращении к представлению. – frugi

0

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

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

Это будет указывать MySQL для ключей держать его в стиле ОРМ Джанго

0

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