web-dev-qa-db-ja.com

JOINクエリでインデックスを使用する方法

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が結合と並べ替えに同時にインデックスを使用するようにする方法はありますか?

===更新===

私はこれに本当に絶望しています。たぶん、何らかの非正規化がここで役立ちますか?

18
Silver Light

カテゴリが多い場合、このクエリを効率的にすることはできません。 MySQLでは、単一のインデックスが一度に2つのテーブルをカバーすることはできません。

非正規化を行う必要があります:last_updatedhas_commentsおよびdeletedarticle_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はこれをサポートしていないため、このようなテーブルを手動で作成し、追加のコードを記述して、ベーステーブルとの同期を維持する必要があります。

16
Quassnoi

特定のクエリに到達する前に、インデックスのしくみを理解することが重要です。

適切な統計で、このクエリは:

_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)。トリガーを使用してそれを維持すれば、大丈夫です。

10

非カバーインデックスの使用はコストがかかります。行ごとに、主キーを使用して、カバーされていない列をベーステーブルから取得する必要があります。だから私は最初に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

主キーを適用するインデックスの名前は常に「主」です。

2
Andomar

MySQLを使用して[〜#〜] keys [〜#〜]または[〜#〜 ]インデックス[〜#〜]

For

  • 順序付け、または
  • グループ化、または
  • 参加する

詳細については、 このリンク に従ってください。これを結合に使用するつもりでした(つまりUSE INDEX FOR JOIN (My_Index)ですが、期待どおりに機能しませんでした。FOR JOINの部分を削除すると、クエリが3.5時間以上から1〜2秒に大幅にスピードアップしました。 MySQLが正しいインデックスを使用することを余儀なくされたからです。

2

まず、記事 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列が含まれていません;)

1
newtover

次のインデックスを利用できます

記事テーブル-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)

1
DRapp