web-dev-qa-db-ja.com

なぜインデックスはorder byで使用されないのですか?

inner joinを作成するときにMySQLがインデックスを使用しない理由に関する情報を取得しようとしていて、最後にORDER BYを試行しています。

ここにSQLクエリがあります:

SELECT
    *           
FROM
    product p INNER JOIN productStore ps ON p.productUUID = ps.productUUID       
ORDER BY
    ps.storeTitle 
LIMIT 50;

この選択による順序を使用している場合、3,5秒以上かかります。同じSQLを実行するために、1.6msのようにして順序を削除すると、説明SQLは次のようになります。

ORDER BYの場合:

id  select_type table   type    possible_keys   key key_len ref rows    Extra
1   SIMPLE  ps  ALL PRIMARY NULL    NULL    NULL    942187  Using filesort
1   SIMPLE  p   eq_ref  PRIMARY PRIMARY 16  foeniks_core.ps.productUUID 1   NULL

ORDER BYなし:

id  select_type table   type    possible_keys   key key_len ref rows    Extra
1   SIMPLE  ps  ALL PRIMARY NULL    NULL    NULL    942187  NULL
1   SIMPLE  p   eq_ref  PRIMARY PRIMARY 16  foeniks_core.ps.productUUID 1   NULL

インデックス作成権がないフィールドは、長さが282のvarcharです。

私のテーブルデザインはここにあります:

CREATE TABLE `productStore` (
  `productUUID` binary(16) NOT NULL,
  `storeUUID` binary(16) NOT NULL,
  `distributorLastUsed` binary(16) DEFAULT NULL,
  `storeTitle` varchar(282) DEFAULT NULL,
  `storeUrl` varchar(282) DEFAULT NULL,
  `storeDescription` text,
  `storeDescriptionDemo` text,
  `storePrice` int(11) NOT NULL DEFAULT '0',
  `storePriceNext` int(11) NOT NULL DEFAULT '0',
  `storePriceCost` int(11) NOT NULL DEFAULT '0',
  `overwrites` int(11) NOT NULL DEFAULT '0',
  `updated` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
  `added` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
  `allowDisplay` tinyint(1) NOT NULL DEFAULT '0',
  `activated` tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (`productUUID`,`storeUUID`),
  KEY `productStoreLanguageToStore_idx` (`storeUUID`),
  KEY `productStoreToDistributor_idx` (`distributorLastUsed`),
  KEY `storeUrl` (`storeUrl`(180)) USING BTREE,
  KEY `testStoreTitle` (`storeTitle`(182)),
  CONSTRAINT `productStoreToDistributor` FOREIGN KEY (`distributorLastUsed`) REFERENCES `distributor` (`distributorUUID`) ON DELETE SET NULL ON UPDATE CASCADE,
  CONSTRAINT `productStoreToProduct` FOREIGN KEY (`productUUID`) REFERENCES `product` (`productUUID`) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT `productStoreToStore` FOREIGN KEY (`storeUUID`) REFERENCES `store` (`storeUUID`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

製品表:

CREATE TABLE `product` (
  `productUUID` binary(16) NOT NULL,
  `productManufacturerUUID` binary(16) NOT NULL,
  `productManufacturerSKU` varchar(40) DEFAULT NULL,
  `productEan` varchar(40) DEFAULT NULL,
  `cnetID` varchar(10) DEFAULT NULL,
  `edbID` int(10) DEFAULT NULL,
  `overwrites` int(10) NOT NULL DEFAULT '0',
  `updated` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
  `added` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
  `activated` tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (`productUUID`),
  KEY `manufacturerSKU` (`productManufacturerSKU`(16)),
  KEY `productToManufacturer_idx` (`productManufacturerUUID`),
  KEY `cnetID` (`cnetID`),
  KEY `productEAN` (`productEan`),
  CONSTRAINT `productToManufacturer` FOREIGN KEY (`productManufacturerUUID`) REFERENCES `manufacturer` (`manufacturerUUID`) ON DELETE NO ACTION ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
5

デビッド・スピレットの答えは、「励まし」の提案を除いて、すべての点で正しいです。

これは、推奨するだけでなく、(ほとんどすべてのバージョンで)オプティマイザに必要なインデックスを使用して50行を検索するプランを選択させる方法です。その後、結合を実行します。常に使用できるわけではありませんが、FOREIGN KEY制約は、この場合、2つのクエリが同じ結果を生成することを保証します。
私はこの手法を呼び出します "最初にLIMIT、次にJOIN" =:

SELECT     p.*, ps.*           
FROM       ( SELECT     *
             FROM       productStore 
             ORDER BY   storeTitle 
             LIMIT 50 
           ) ps 
    INNER JOIN product p 
        ON p.productUUID = ps.productUUID       
ORDER BY   ps.storeTitle ;
5
ypercubeᵀᴹ

本当の答えは、「プレフィックス」インデックスは事実上役に立たないということです。私は言及しています

_KEY `testStoreTitle` (`storeTitle`(182))
_

インデックスには切り捨てられた値のみが含まれるため、notには完全に順序付けされたタイトルのリストが含まれるため、_ORDER BY_を行うために簡単に使用することはできません。

InnoDBには767の制限がありますbytes(utf8 VARCHAR(255)の最大値)。これは、複雑な一連の手順で増やすことができます。

  1. 5.6.3以降を入手してください。
  2. _SET GLOBAL innodb_file_format=Barracuda;_
  3. _SET GLOBAL innodb_file_per_table=ON;_
  4. ALTER TABLE tbl DROP INDEX testStoreTitle, ADD INDEX(storeTitle) ROW_FORMAT=DYNAMIC;またはCOMPRESSED

Ypercubeが提案する「JOINの前のLIMIT(またはGROUP BY)」に同意します。そのソリューションは、ほとんどこのソリューションに直交しています。私のソリューションは---(おそらく 942187をスキャンする必要がないため、大幅に高速になります。

4
Rick James

「なぜこんなに時間がかかるのか」はUsing filesort-これは、すべての結果をスプールして、インデックスなしでソートすることを意味します。

問題は、SQLエンジンがテーブル参照ごとに1つのインデックスしか使用できないことです。この場合、結合述語に最適なインデックス(主キーのインデックス)を使用します。

IIRC mySqlのクエリプランナーは、効率を上げるために賢く並べ替えようとするのではなく、クエリの順序に従います。したがって、結果を50に制限したとしても、最初にすべてを結合する必要があります(他のキーを使用)、並べ替え、次に最初の50を選択します。productStoreを順にスキャンしてから、次のようにして製品に必要なものを検索できます。

SELECT     *           
FROM       productStore ps 
INNER JOIN product p 
        ON p.productUUID = ps.productUUID       
ORDER BY   ps.storeTitle 
LIMIT 50;

productStore.storeTitle最初の50を取得するためのインデックスthenそれぞれをproductの関連する行に結合します。そうでない場合は、ypercubeの派生テーブルベースのソリューションが望ましい効果をもたらす可能性があります(私はMS SQL Serverを使用する傾向があり、そのクエリプランナーは、私のクエリと彼の両方が同等であり、同じ方法で両方を実行できるように十分明るいと思います、しかしmySQLはおそらくそのようなトリックをあまり知っていません)。

列は異なる順序で出力されるため、呼び出しコードがこれに敏感である場合は注意してください。 * select句では、他の理由からこの理由により回避することは実用的ではありません。

2
David Spillett

インデックスは、行を選択(検索)するためにのみ使用されます。それらが最終的な結果セットに入れられると、それらはどこに来たかに関係なく、単に行と列のセットになります。 (ソート)による順序付けが、この結果セットの最後の操作として適用されます。あなたのケースでは、非常に大きな結果セットを取得しています。明らかに、それを並べ替えようとすると、クエリ時間が増加します。

1
jujiro