web-dev-qa-db-ja.com

mySQLクエリの最適化—複数の結合または選択…ない場所(個別の選択…)?

バックグラウンド

私はDrupalインストールで大規模なユーザーデータベース(約20万行)にアクセスし、 "People Finder"機能はそれらのすべての行に(ランダムな順序で)アクセスする必要があります。)ないようです。 DrupalのUI内からLIMITOFFSETを使用できる(そして、Drupal.SEで Slow query with large dataset with Drupalビュー— SQLまたはPHPで処理する方が良いですか? 、この問題とこの問題の一部に対処します)しかしmySQL固有の質問次のとおりです

別のテーブルのデータに基づいて一部の行を除外する必要があります(「ロールB、C、またはDに属していない、ロールAのすべてのユーザーを含める)。クエリDrupal is generated is

SELECT
    users.uid AS uid,
    /* some columns */,
    Rand() AS random_field
FROM
    users users
    INNER JOIN users_roles users_roles ON users.uid = users_roles.uid
    LEFT JOIN users_roles users_roles2
        ON users.uid = users_roles2.uid
        AND (users_roles2.rid = :views_join_condition_0
          OR users_roles2.rid = :views_join_condition_1
          OR users_roles2.rid = :views_join_condition_2)
WHERE
    (( (users.status <> :db_condition_placeholder_3)      -- Active users only
    AND (users_roles.rid = :db_condition_placeholder_4)   -- Must be in rôle A
    AND (users_roles2.rid IS NULL)                        -- Must not be in rôles B, C, D
    AND (users.uid != :users_uid OR users.uid IS NULL) )) -- Must not be current user
ORDER BY random_field ASC

users.uid IS NULLへの参照は赤字です。決してそうであってはならず、このクエリに密接な関係はありません。)

質問

Drupalのフィルター基準(DrupalのUIの制約内で)を手動でローリングすると役立つかもしれません。そして、ほとんどすべての:db_condition_placeholdersをハードコードできます—パフォーマンスに大きな違いがあるかどうかわかりません次の2つのオプション:

  1. FROM句を

    FROM users INNER JOIN users_roles
        ON users.uid = users_roles.uid AND users_roles.rid NOT IN (6, 8, 9)
    

    (そして、以前と同様にWHERE users_roles.rid = 5を実行し、users_roles2参照を削除するだけです);または

  2. JOINを完全に削除し、WHERE句を次のように変更します。

    WHERE users.status = 1                                             -- Active users only
        AND users.uid IN
           (SELECT DISTINCT uid FROM users_roles WHERE rid = 5)        -- Must be in rôle A
        AND users.uid NOT IN
           (SELECT DISTINCT uid FROM users_roles WHERE rid IN (6,8,9)) -- Not rôles B, C, D
        AND users.uid != :users_uid                                    -- Not current user
    

追加情報

それが役立つ場合、mySQLのバージョン番号は5.1.50-enterprise-gpl-proであり、すべてのテーブルはInnoDBストレージエンジンを使用し、テーブルusers_rolesにはすでに2つの列にクラスター化された主キーがあります。

mysql> describe users_roles;
+-------+------------------+------+-----+---------+-------+
| Field | Type             | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+-------+
| uid   | int(10) unsigned | NO   | PRI | 0       |       |
| rid   | int(10) unsigned | NO   | PRI | 0       |       |
+-------+------------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

私が目にしている実際のパフォーマンスの問題は2つあります。サーバーはRAMで限界に達しており、ここで説明するクエリの実行には2秒以上かかります。私はできないと思いますRAMをLIMITOFFSETを参照せずに処理しますが、このクエリの高速化は間違いなく良いスタートです。

要求に応じて追加情報

mysql> describe users;
+------------------+------------------+------+-----+---------+-------+
| Field            | Type             | Null | Key | Default | Extra |
+------------------+------------------+------+-----+---------+-------+
| uid              | int(10) unsigned | NO   | PRI | 0       |       |
| name             | varchar(60)      | NO   | UNI |         |       |
| pass             | varchar(128)     | NO   |     |         |       |
| mail             | varchar(254)     | YES  | MUL |         |       |
| theme            | varchar(255)     | NO   |     |         |       |
| signature        | varchar(255)     | NO   |     |         |       |
| signature_format | varchar(255)     | YES  |     | NULL    |       |
| created          | int(11)          | NO   | MUL | 0       |       |
| access           | int(11)          | NO   | MUL | 0       |       |
| login            | int(11)          | NO   |     | 0       |       |
| status           | tinyint(4)       | NO   |     | 0       |       |
| timezone         | varchar(32)      | YES  |     | NULL    |       |
| language         | varchar(12)      | NO   |     |         |       |
| picture          | int(11)          | NO   |     | 0       |       |
| init             | varchar(254)     | YES  |     |         |       |
| data             | longblob         | YES  |     | NULL    |       |
+------------------+------------------+------+-----+---------+-------+
16 rows in set (0.00 sec)

mysql> EXPLAIN EXTENDED SELECT
    users.uid AS uid,
    /* some columns */,
    Rand() AS random_field
FROM
    users users
    INNER JOIN users_roles users_roles ON users.uid = users_roles.uid
    LEFT JOIN users_roles users_roles2
        ON users.uid = users_roles2.uid
        AND (users_roles2.rid = 6 OR users_roles2.rid = 8 OR users_roles2.rid = 9)
WHERE
    (( (users.status <> 0)                           -- Active users only
    AND (users_roles.rid = 5)                        -- Must be in rôle A
    AND (users_roles2.rid IS NULL)                   -- Not in rôles B, C, D
    AND (users.uid != 35635 OR users.uid IS NULL) )) -- Not (random valid UID)
ORDER BY random_field ASC
+----+-------------+--------------+--------+---------------+---------+---------+------------------------+-------+-----------------------------------------------------------+
| id | select_type | table        | type   | possible_keys | key     | key_len | ref                    | rows  | Extra                                                     |
+----+-------------+--------------+--------+---------------+---------+---------+------------------------+-------+-----------------------------------------------------------+
|  1 | SIMPLE      | users_roles  | ref    | PRIMARY,rid   | rid     | 4       | const                  | 69985 | Using where; Using index; Using temporary; Using filesort |
|  1 | SIMPLE      | users        | eq_ref | PRIMARY       | PRIMARY | 4       | dbname.users_roles.uid |     1 | Using where                                               |
|  1 | SIMPLE      | users_roles2 | ref    | PRIMARY,rid   | PRIMARY | 4       | dbname.users.uid       |     1 | Using where; Using index; Not exists                      |
+----+-------------+--------------+--------+---------------+---------+---------+-----------------------+-------+-----------------------------------------------------------+
3 rows in set, 1 warning (0.01 sec)
6
Owen Blacker
  • users_roles (rid, uid)にインデックスを追加します。 2つの列_(a,b)_を持つ多対多のテーブルでは、ほとんどの場合、両方のインデックスが必要になります:どちらかのクエリで_(a,b)_と_(b,a)_。このインデックスはこのクエリに役立つと思います。

  • クエリとそれらが生成する_EXPLAIN EXTENDED_をさまざまに書き換えてみてください。

  • あなたの提案について、最初は正しくありません(同じ結果は表示されません)。 2番目の提案:

_WHERE users.status = 1                                           -- Active users only
_

はい、それは_users.status <> 0_よりも優れています。 users (status)にインデックスがある場合(およびアクティブなユーザーが少ない場合はさらに)、この変更はより良い効果をもたらす可能性があります。ブール列(またはブールとして機能する列)を使用したクエリの最適化は、Bツリーでは簡単ではありません。

_  AND users.uid IN
     (SELECT DISTINCT uid FROM users_roles WHERE rid = 5)        -- Must be in rôle A
_

いいえ。MySQLはcolumn IN (SELECT ...)に問題があることがわかっています。特に、外部テーブルが大きい場合(そして、200Kカラムなので、ダメ、良くありません)。

_  AND users.uid NOT IN
     (SELECT DISTINCT uid FROM users_roles WHERE rid IN (6,8,9)) -- Not rôles B, C, D
_

はい、それは書き換えの1つの方法です。ただし、DISTINCTは冗長です。

_  AND users.uid <> :users_uid                                    -- Not current user
_

はい、_users.uid IS NOT NULL_を削除しても問題は解決し、結果は変わりません。

  • あなたが試すことができる他のこと:

_rid = 5_条件をON句に移動します。

_INNER JOIN users_roles users_roles 
  ON  users.uid = users_roles.uid
  AND users_roles.rid = 5
_

_NOT IN_への(書き換え)は、_NOT EXISTS_でも記述できます。

_  AND NOT EXISTS 
      ( SELECT * 
        FROM users_roles ur 
        WHERE ur.uid = users.uid 
          AND ur.rid IN (6,8,9)
      )
_
3
ypercubeᵀᴹ