次の構造を持つ3つのテーブルで構成されるデータベースがあります。
レストランテーブル:restaurant_id、location_id、rating。 例:1325、77、4.5
restaurant_nameテーブル:restaurant_id、言語、名前。 例:1325、 'en'、 'Pizza Express'
location_nameテーブル:location_id、言語、名前。 例:77、 'en'、 'New York'
レストランの情報を英語で取得し、場所名とレストラン名で並べ替え、LIMIT句を使用して結果にページ番号を付けたいと思います。だから私のSQLは:
SELECT ln.name, rn.name
FROM restaurant r
INNER JOIN location_name ln
ON r.location_id = ln.location_id
AND ln.language = 'en'
INNER JOIN restaurant_name rn
ON r.restaurant_id = rn.restaurant_id
AND rn.language = 'en'
ORDER BY ln.name, rn.name
LIMIT 0, 50
これはひどく遅いので、据え置きJOINを使用してSQLを調整しました。これにより、処理が大幅に速くなります(10秒以上から2秒)。
SELECT ln.name, rn.name
FROM restaurant r
INNER JOIN (
SELECT r.restaurant_id
FROM restaurant r
INNER JOIN location_name ln
ON r.location_id = ln.location_id
AND ln.language = 'en'
INNER JOIN restaurant_name rn
ON r.restaurant_id = rn.restaurant_id
AND rn.language = 'en'
ORDER BY ln.name, rn.name
LIMIT 0, 50
) r1
ON r.restaurant_id = r1.restaurant_id
INNER JOIN location_name ln
ON r.location_id = ln.location_id
AND ln.language = 'en'
INNER JOIN restaurant_name rn
ON r.restaurant_id = rn.restaurant_id
AND rn.language = 'en'
ORDER BY ln.name, rn.name
残念ながらまだ2秒はユーザーには受け入れられないので、クエリのEXPLAINを確認すると、遅い部分がORDER BY句にあるように見えます。 ORDER BY最適化 に関する公式リファレンスマニュアルを確認したところ、次のステートメントに遭遇しました。
MySQLはインデックスを使用してORDER BYを解決できない場合もありますが、WHERE句に一致する行を見つけるためにインデックスを使用する場合があります。例:
クエリは多くのテーブルを結合し、ORDER BYの列はすべて、行の取得に使用される最初の非定数テーブルからのものではありません。 (これは、const結合タイプを持たないEXPLAIN出力の最初のテーブルです。)
したがって、私の場合、順序付けする2つの列が非定数結合テーブルからのものであることを考えると、インデックスは使用できません。 私の質問は、物事をスピードアップするために私が取ることができる他のアプローチはありますか、または私がこれまでに行ったことはすでに達成できる最高のものですか?ソートする列をプライマリテーブルに移動する必要がありますか? (しかし、私のサイトは実際にデータを並べ替える複数の方法を提供しているので、結局6から7列を移動する必要があり、多くのデータの冗長性を引き起こします...)
以下はテーブルのDDLです。この問題を説明するためだけに作成しました。実際のテーブルには、さらに多くの列があります。
CREATE TABLE restaurant (
restaurant_id INT NOT NULL AUTO_INCREMENT,
location_id INT NOT NULL,
rating INT NOT NULL,
PRIMARY KEY (restaurant_id),
INDEX idx_restaurant_1 (location_id)
);
CREATE TABLE restaurant_name (
restaurant_id INT NOT NULL,
language VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
INDEX idx_restaurant_name_1 (restaurant_id, language),
INDEX idx_restaurant_name_2 (name)
);
CREATE TABLE location_name (
location_id INT NOT NULL,
language VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
INDEX idx_location_name_1 (location_id, language),
INDEX idx_location_name_2 (name)
);
以下は、ORDER BY句を使用したEXPLAIN出力です。
+----+-------------+------------+--------+--------------------------+-----------------------+---------+--------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+--------------------------+-----------------------+---------+--------------------------------+------+----------------------------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 50 | |
| 1 | PRIMARY | rn | ref | idx_restaurant_name_1 | idx_restaurant_name_1 | 1538 | r1.restaurant_id,const,const | 1 | Using where |
| 1 | PRIMARY | r | eq_ref | PRIMARY,idx_restaurant_1 | PRIMARY | 4 | r1.restaurant_id | 1 | |
| 1 | PRIMARY | ln | ref | idx_location_name_1 | idx_location_name_1 | 1538 | test.r.location_id,const,const | 1 | Using where |
| 2 | DERIVED | rn | ALL | idx_restaurant_name_1 | NULL | NULL | NULL | 8484 | Using where; Using temporary; Using filesort |
| 2 | DERIVED | r | eq_ref | PRIMARY,idx_restaurant_1 | PRIMARY | 4 | test.rn.restaurant_id | 1 | |
| 2 | DERIVED | ln | ref | idx_location_name_1 | idx_location_name_1 | 1538 | test.r.location_id | 1 | Using where |
+----+-------------+------------+--------+--------------------------+-----------------------+---------+--------------------------------+------+----------------------------------------------+
以下は、ORDER BY句のないEXPLAIN出力です。
+----+-------------+------------+--------+--------------------------+-----------------------+---------+--------------------------------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+--------------------------+-----------------------+---------+--------------------------------+------+--------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 50 | |
| 1 | PRIMARY | rn | ref | idx_restaurant_name_1 | idx_restaurant_name_1 | 1538 | r1.restaurant_id,const,const | 1 | Using where |
| 1 | PRIMARY | r | eq_ref | PRIMARY,idx_restaurant_1 | PRIMARY | 4 | r1.restaurant_id | 1 | |
| 1 | PRIMARY | ln | ref | idx_location_name_1 | idx_location_name_1 | 1538 | test.r.location_id,const,const | 1 | Using where |
| 2 | DERIVED | rn | index | idx_restaurant_name_1 | idx_restaurant_name_1 | 1538 | NULL | 8484 | Using where; Using index |
| 2 | DERIVED | r | eq_ref | PRIMARY,idx_restaurant_1 | PRIMARY | 4 | test.rn.restaurant_id | 1 | |
| 2 | DERIVED | ln | ref | idx_location_name_1 | idx_location_name_1 | 1538 | test.r.location_id | 1 | Using where; Using index |
+----+-------------+------------+--------+--------------------------+-----------------------+---------+--------------------------------+------+--------------------------+
よろしくお願いします!
「過正規化」が主な問題だと思います。
このクエリの焦点は「言語」にあるようですが、それが最後に確認されたものです。
解決策の1つの試みとして、スキーマを次の2つのテーブルに再配置します。
CREATE TABLE restaurant_attributes (
restaurant_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
rating INT NOT NULL,
PRIMARY KEY (restaurant_id),
);
CREATE TABLE restaurants_by_lang (
restaurant_id INT UNSIGNED NOT NULL,
language VARCHAR(5) NOT NULL CHARACTER SET ascii, -- see note
name VARCHAR(255) NOT NULL,
location VARCHAR(255) NOT NULL,
PRIMARY KEY(language, restaurant_id),
INDEX (language, location, name), -- perfect for the query
INDEX (name),
INDEX (location)
);
今のクエリは単純です:
SELECT location, name
FROM restaurants_by_lang
WHERE language = 'en'
ORDER BY location, name
LIMIT 0, 50;
このスキーマでは、次の問題となる効率的な「ページ付け」も可能になります。 ( ここ を参照してください。)
100万のレストランがある場合、最初の50を場所別にリストすることは、次に役に立たないUIデザインであることをお勧めします。クエリの必要性を再考することをお勧めします。たとえば、リスト全体にページ番号を付けるのではなく、国、州、都市、地域、レストランなど、何らかの形式のドリルダウンを提案します。ユーザーがジンバブエでZahirを見つける方がはるかに速くなります。
しかし...残りのSELECTs
が見えないので、他の何が私のために悪化させたのかわかりません。
私はそのような標準に基づいて5文字の言語を選びました。本当に必要でない限り、255は使用しないでください。
テスト
最終的に必要なデータよりも少ないデータでテストする場合、この手法はしばしば便利です...
FLUSH STATUS;
SELECT ...;
SHOW SESSION STATUS LIKE 'Handler%';
次に、数字を見てください。テーブルの行数(またはその倍数)の概数は、テーブルスキャンを示します。 LIMIT
の値に近い数値は、クエリがタスクを効率的に削減できることを示しています。
私のアプローチでは「50」と表示され、少なくとも7K(20K?)と表示されます。
注:ページ4(LIMIT 150, 50
を使用)では、200と表示されます。あなたのものは変わりません。私のページネーションリンクのテクニックを使用すると、ページ4でも50になります。
(GROUP BY
またはORDER BY
による)各一時テーブルには、行数(tmpテーブルの数)を示すHandler_write
が表示されます。