2つのテーブルがあります。
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インデックスを使用しません(explainのtype: index
)。これは非常に頻繁なクエリであるため、パフォーマンスに非常に悪影響を及ぼし、データベースサーバー全体を強制終了します。
STRAIGHT_JOIN
を使用してテーブルの順序を逆にしてみましたが、これによりfilesort, using_temporary
が得られ、さらに悪い結果になります。
Mysqlが結合と並べ替えに同時にインデックスを使用するようにする方法はありますか?
===更新===
私はこれに本当に絶望しています。たぶん、何らかの非正規化がここで役立ちますか?
カテゴリが多い場合、このクエリを効率的にすることはできません。 MySQL
では、単一のインデックスが一度に2つのテーブルをカバーすることはできません。
非正規化を行う必要があります: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)
のインデックスはほとんど意味がありません。これを使用すると、ディスクページで前後に行を順番に読み取ることになります。非常に高価です。
ただし、limit句を投げると、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)
のインデックスを使用した後者のクエリを想定します。インデックスは左から右に読み取られます。 1つはこの潜在的なクエリに対して非常に意味があり、もう1つはまったく意味がない場合があります。次のように考えてください。
_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)
のインデックスは意味があります。有効な行はすべての場所にあるため、一致するものが十分に見つかるまで順番に行を読み取り、voilà。
より一般的なケースでは、それは漠然と選択的です。私の知る限りでは、収集された統計は相関関係をあまり調べていません。したがって、プランナーは、後者のインデックスを読む価値があるほど速く適切なカテゴリーの記事が見つかるかどうかを推定する良い方法はありません。メモリ内での結合と並べ替えは通常は安くなるため、プランナーはそれに応じます。
とにかく、インデックスの使用を強制する2つのオプションがあります。
1つは、クエリプランナーが完全ではないことを認め、ヒントを使用することです。
http://dev.mysql.com/doc/refman/5.5/en/index-hints.html
ただし、プランナーは、希望するインデックスまたはその逆のバージョンを使用したくない場合があるため、実際には正しい場合があるので注意してください。また、MySQLの将来のバージョンでは正しくなる可能性があるため、何年にもわたってコードを保守する場合は、そのことを覚えておいてください。
編集:_STRAIGHT_JOIN
_、DRapによる指摘も同様に機能しますが、同様の警告があります。
もう1つは、頻繁に選択される記事(例: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
主キーを適用するインデックスの名前は常に「主」です。
MySQLを使用して[〜#〜] keys [〜#〜]または[〜#〜 ]インデックス[〜#〜]
For
詳細については、 このリンク に従ってください。これを結合に使用するつもりでした(つまりUSE INDEX FOR JOIN (My_Index)
ですが、期待どおりに機能しませんでした。FOR JOIN
の部分を削除すると、クエリが3.5時間以上から1〜2秒に大幅にスピードアップしました。 MySQLが正しいインデックスを使用することを余儀なくされたからです。
まず、記事 MySQLがインデックスを使用する3つの方法 を読むことをお勧めします。
そして今、あなたが基本を知っているとき、あなたはこの特定のクエリを最適化することができます。
MySQLは順序付けにインデックスを使用できません。インデックスの順序でデータを出力できます。 MySQLは結合にネストされたループを使用するため、順序付けするフィールドは結合の最初のテーブルにある必要があります(EXPLAINの結果に結合の順序が表示され、特定のインデックスを作成することで影響を受ける可能性があります(それが役に立たない場合) )必要なインデックスを強制する)。
もう1つの重要なことは、順序付けする前に、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);
追伸テーブルa
のテーブル定義にcomment_cnt
列が含まれていません;)
次のインデックスを利用できます
記事テーブル-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
この場合、私はarticlesテーブルのインデックスを確認します
インデックス-(id、deleted、last_updated)