[英]How to make JOIN query use index?
我有兩個表:
CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(1000) DEFAULT NULL,
`last_updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `last_updated` (`last_updated`),
) ENGINE=InnoDB AUTO_INCREMENT=799681 DEFAULT CHARSET=utf8
CREATE TABLE `article_categories` (
`article_id` int(11) NOT NULL DEFAULT '0',
`category_id` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`article_id`,`category_id`),
KEY `category_id` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |
這是我的查詢:
SELECT a.*
FROM
articles AS a,
article_categories AS c
WHERE
a.id = c.article_id
AND c.category_id = 78
AND a.comment_cnt > 0
AND a.deleted = 0
ORDER BY a.last_updated
LIMIT 100, 20
和EXPLAIN
吧:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: a
type: index
possible_keys: PRIMARY
key: last_updated
key_len: 9
ref: NULL
rows: 2040
Extra: Using where
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: c
type: eq_ref
possible_keys: PRIMARY,fandom_id
key: PRIMARY
key_len: 8
ref: db.a.id,const
rows: 1
Extra: Using index
它在第一個表上使用last_updated
的完整索引掃描進行排序,但不使用 y 索引進行連接( type: index
解釋中的type: index
)。 這對性能非常不利並且會殺死整個數據庫服務器,因為這是一個非常頻繁的查詢。
我試過用STRAIGHT_JOIN
反轉表順序,但這給文件filesort, using_temporary
, filesort, using_temporary
,甚至更糟。
有沒有辦法讓mysql同時使用索引進行連接和排序?
=== 更新 ===
我真的很絕望。 也許某種非規范化可以在這里提供幫助?
如果您有很多類別,則無法使此查詢高效。 在MySQL
沒有一個索引可以同時覆蓋兩個表。
你必須做非規范化:添加last_updated
, has_comments
並deleted
到article_categories
:
CREATE TABLE `article_categories` (
`article_id` int(11) NOT NULL DEFAULT '0',
`category_id` int(11) NOT NULL DEFAULT '0',
`last_updated` timestamp NOT NULL,
`has_comments` boolean NOT NULL,
`deleted` boolean NOT NULL,
PRIMARY KEY (`article_id`,`category_id`),
KEY `category_id` (`category_id`),
KEY `ix_articlecategories_category_comments_deleted_updated` (category_id, has_comments, deleted, last_updated)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
並運行此查詢:
SELECT *
FROM (
SELECT article_id
FROM article_categories
WHERE (category_id, has_comments, deleted) = (78, 1, 0)
ORDER BY
last_updated DESC
LIMIT 100, 20
) q
JOIN articles a
ON a.id = q.article_id
當然,每當您更新article
相關列時,您也應該更新article_categories
。 這可以在觸發器中完成。
請注意,列has_comments
是布爾值:這將允許使用相等謂詞對索引進行單個范圍掃描。
還要注意LIMIT
進入子查詢。 這使得MySQL
使用默認情況下不使用的后期行查找。 請參閱我的博客中有關為什么它們會提高性能的文章:
如果你在 SQL Server 上,你可以對你的查詢創建一個可索引的視圖,這基本上會創建一個具有附加字段的article_categories
的非規范化索引副本,由服務器自動維護。
不幸的是, MySQL
不支持此功能,您必須手動創建這樣的表並編寫額外的代碼以使其與基表保持同步。
在進入您的特定查詢之前,了解索引的工作原理很重要。
使用適當的統計信息,此查詢:
select * from foo where bar = 'bar'
...將在foo(bar)
上使用索引,如果它是選擇性的。 這意味着,如果bar = 'bar'
相當於選擇了表格的大部分行,那么讀取表格並消除不適用的行會更快。 相反,如果bar = 'bar'
意味着只選擇少數幾行,那么讀取索引就有意義了。
假設我們現在加入一個 order 子句,並且您已經在foo(bar)
和foo(baz)
上建立了索引:
select * from foo where bar = 'bar' order by baz
如果bar = 'bar'
是非常有選擇性的,那么獲取所有符合的行並在內存中對它們進行排序是很便宜的。 如果它根本不是選擇性的,那么foo(baz)
上的索引就沒有什么意義,因為無論如何您都會獲取整個表:使用它意味着在磁盤頁面上來回按順序讀取行,這非常昂貴。
然而,加入限制子句, foo(baz)
可能會突然變得有意義:
select * from foo where bar = 'bar' order by baz limit 10
如果bar = 'bar'
非常有選擇性,它仍然是一個不錯的選擇。 如果它根本沒有選擇性,您將通過掃描foo(baz)
上的索引快速找到 10 個匹配行——您可能會讀取 10 行或 50 行,但很快就會找到 10 行。
假設后一個查詢使用foo(bar, baz)
和foo(baz, bar)
上的索引代替。 索引從左到右讀取。 一個對這個潛在的查詢非常有意義,另一個可能根本沒有。 像這樣想它們:
bar baz baz bar
--------- ---------
bad aaa aaa bad
bad bbb aaa bar
bar aaa bbb bad
bar bbb bbb bar
如您所見, foo(bar, baz)
上的索引允許從('bar', 'aaa')
開始讀取並從該點開始按順序獲取行。
相反, foo(baz, bar)
上的索引產生按baz
排序的行,而不管bar
可能包含什么。 如果bar = 'bar'
作為條件根本不是選擇性的,您將很快遇到查詢的匹配行,在這種情況下使用它是有意義的。 如果它非常有選擇性,您可能會在找到足夠多的匹配bar = 'bar'
之前迭代無數行 - 它可能仍然是一個不錯的選擇,但它是最佳選擇。
解決這個問題后,讓我們回到您的原始查詢......
您需要將文章與類別連接起來,以過濾特定類別中的文章,其中有多個評論,未刪除,然后按日期對它們進行排序,然后抓取其中的一小部分。
我認為大多數文章都不會被刪除,因此該標准的索引不會有太大用處——它只會減慢寫入和查詢計划的速度。
我認為大多數文章都有評論或更多評論,因此也不會有選擇性。 即幾乎不需要對其進行索引。
如果沒有您的類別過濾器,索引選項相當明顯: articles(last_updated)
; 可能右側是評論計數列,左側是已刪除標志。
使用您的類別過濾器,這一切都取決於...
如果您的類別過濾器非常有選擇性,那么選擇該類別內的所有行、在內存中對它們進行排序並選擇最匹配的行實際上是非常有意義的。
如果您的類別過濾器根本沒有選擇性並且產生幾乎所有文章,則articles(last_update)
上的索引是有道理的:有效的行到處都是,所以按順序讀取行,直到找到足夠的匹配和瞧。
在更一般的情況下,它只是模糊的選擇性。 據我所知,收集的統計數據並沒有過多地研究相關性。 因此,規划器沒有很好的方法來估計它是否會以足夠快的速度找到具有正確類別的文章,值得閱讀后一個索引。 在內存中加入和排序通常會更便宜,所以計划者會這樣做。
無論如何,您有兩個選項可以強制使用索引。
一種是承認查詢計划器並不完美並使用提示:
http://dev.mysql.com/doc/refman/5.5/en/index-hints.html
不過要小心,因為有時計划者實際上是正確的,不想使用您想要的索引或副版本。 此外,它可能會在 MySQL 的未來版本中變得正確,因此在多年來維護代碼時請記住這一點。
編輯: STRAIGHT_JOIN
,正如 DRap 所指出的那樣,也有類似的警告。
另一種是維護一個額外的列來標記經常選擇的文章(例如,tinyint 字段,當它們屬於您的特定類別時設置為 1),然后在例如articles(cat_78, last_updated)
上添加索引。 使用觸發器維護它,你會做得很好。
使用非覆蓋索引是昂貴的。 對於每一行,必須使用主鍵從基表中檢索任何未覆蓋的列。 因此,我首先嘗試對涵蓋的articles
進行索引。 這可能有助於說服 MySQL 查詢優化器該索引是有用的。 例如:
KEY IX_Articles_last_updated (last_updated, id, title, comment_cnt, deleted),
如果這沒有幫助,您可以使用FORCE INDEX
:
SELECT a.*
FROM article_categories AS c FORCE INDEX (IX_Articles_last_updated)
JOIN articles AS a FORCE INDEX (PRIMARY)
ON a.id = c.article_id
WHERE c.category_id = 78
AND a.comment_cnt > 0
AND a.deleted = 0
ORDER BY
a.last_updated
LIMIT 100, 20
強制執行主鍵的索引的名稱始終是“primary”。
首先,我建議閱讀文章MySQL 使用索引的 3 種方式。
現在,當您了解基礎知識后,就可以優化此特定查詢。
MySQL 不能使用索引進行排序,它只能按照索引的順序輸出數據。 由於 MySQL 使用嵌套循環進行連接,您要排序的字段應該在連接的第一個表中(您可以在 EXPLAIN 結果中看到連接的順序,並且可以通過創建特定索引和(如果它沒有幫助)影響它) 通過強制所需的索引)。
另一個重要的事情是,在訂貨之前,你從獲取的所有過濾的行中的所有列a
表,然后可能跳過其中的大多數。 獲取所需行 id 的列表並僅獲取那些行要高效得多。
為了完成這項工作,您需要在表a
上建立一個覆蓋索引(deleted, comment_cnt, last_updated)
,現在您可以按如下方式重寫查詢:
SELECT *
FROM (
SELECT a.id
FROM articles AS a,
JOIN article_categories AS c
ON a.id = c.article_id AND c.category_id = 78
WHERE a.comment_cnt > 0 AND a.deleted = 0
ORDER BY a.last_updated
LIMIT 100, 20
) as ids
JOIN articles USING (id);
PS您表a
表定義不包含comment_cnt
列;)
您可以使用影響 MySQL 來使用KEYS或INDEXES
為了
如需更多信息,請點擊此鏈接。 我打算用它來加入(即USE INDEX FOR JOIN (My_Index)
但它沒有按預期工作。刪除FOR JOIN
部分顯着加快了我的查詢速度,從超過 3.5 小時到 1-2 秒。僅僅是因為MySQL 被迫使用正確的索引。
我將有以下索引可用
文章表 -- INDEX(已刪除、last_updated、comment_cnt)
article_categories 表 -- INDEX ( article_id, category_id ) -- 你已經有了這個索引
然后添加 Straight_Join 以強制執行列出的查詢,而不是嘗試通過任何可能有助於查詢的統計信息使用 article_categories 表。
SELECT STRAIGHT_JOIN
a.*
FROM
articles AS a
JOIN article_categories AS c
ON a.id = c.article_id
AND c.category_id = 78
WHERE
a.deleted = 0
AND a.comment_cnt > 0
ORDER BY
a.last_updated
LIMIT
100, 20
根據評論/反饋,如果類別記錄小得多,我會考慮根據集合進行反轉......例如
SELECT STRAIGHT_JOIN
a.*
FROM
article_categories AS c
JOIN articles as a
ON c.article_id = a.id
AND a.deleted = 0
AND a.Comment_cnt > 0
WHERE
c.category_id = 78
ORDER BY
a.last_updated
LIMIT
100, 20
在這種情況下,我將通過以下方式確保文章表上的索引
索引 -- (id, 刪除, last_updated)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.