顯示包含「MySQL」標籤的文章。顯示所有文章
顯示包含「MySQL」標籤的文章。顯示所有文章

「使われていないカラムのSelectをやめる」と「ページネーションでのOFFSET問題」を考えて、SubqueryでページのSelectを作りました。「ページネーションでのOFFSET問題」とは、OFFSETで指定する值が大きくなるにつれSQLの実行結果が遅くなるという罠です。なぜなら、MySQLはOFFSETが指定されている場合、そのOFFSETまでのレコードを全て読み込んでしまうからです。
SELECT `user_id`, `user_description`, `name_katagana`, `name_kanji`, `email`, `update_time`, `user_status`
FROM `users`
ORDER BY `update_time` DESC
LIMIT 6 OFFSET 1000
それでJOINのONで取得するレコード数がもっと少なくなるように絞り込む方法を使いました。
SELECT `record_id`,`records`.`update_time`, `description`, `user_pic`, `publisher_id`, `user_loginid`, `user_status`
FROM `users` INNER JOIN ( 
SELECT `user_id` 
FROM `users` 
ORDER BY  users.update_time DESC 
LIMIT 6 
OFFSET 1000 ) AS `a` 
ON (`users`.`user_id` = `a`.`user_id`) ;
また、ページネーションを作ることには「システム全体でどのぐらいのデータ数があるかカウントして表示する」というような機能が必要です。
MySQLでMyISAMを使っていれば Select count(*) from usersという高速なSQL文がありますが、InnoDBではデープル全体をスキャンする必要があるため、レコード数が多ければ多いほど遅くなります。少量のデータのSELECTでは、SELECT SQL_CALC_FOUND_ROWSというprefixを使って、SELECT FOUND_ROWS()でデータ数を取りますが(Select count(*) from usersより早いです!)、やはり多量のデータのSELECTは遅いです。
それで、新しいカウントテブールを作りました。結果として、高速でページを表示することができました。しかし、そこのデメリットはテブールのINSERT/DELETEは前より遅くなりました。
多量のリストは下記のように実行します。
sub search_with_pager {
....
 my ($self, $table_name, $where, $opt) = @_;

    my $table = $self->schema->get_table($table_name) or Carp::croak("'$table_name' is unknown table");

    my $page = $opt->{page};
    my $rows = $opt->{rows};

    for (qw/page rows/) {
        Carp::croak("missing mandatory parameter: $_") unless exists $opt->{$_};
    }
    my $columns = $opt->{'+columns'}
    ? [@{$table->{columns}}, @{$opt->{'+columns'}}]
    : ($opt->{columns} || $table->{columns})
    ;
    
    if ( $opt->{offset_key} ){

       my $id = $where->{'+offset_key'}
        ? [@{$table->{columns}}, @{$opt->{'+offset_key'}}]
        : ($opt->{offset_key} || $table->{columns})
        ;

        my ($sql, @binds) = $self->sql_builder->select(
            $table_name,
            $id,
            {},
            +{
                %$opt,
                limit => $rows + 1,
                offset => $rows*($page-1),
            }
        );

        my $new_table = '( '.$sql.' ) AS a ';

        my $id_name = ${@$id[0]}[0];
        my $joins = $opt->{joins};

        my @new_joins = [ $table_name => {table => \$new_table , condition => $table_name.".".$id_name." = a.".$id_name } ];
        push @$joins, @new_joins;

        ($sql, @binds) = $self->sql_builder->select(
            $table_name,
            $columns,
            {},
            +{
                joins => $joins, 
            }
        );

        my $sth = $self->dbh->prepare($sql) or Carp::croak $self->dbh->errstr;
        $sth->execute(@binds) or Carp::croak $self->dbh->errstr;
        
        my $table_count = $table_name."_count";
        my $total_entries = 0+$self->dbh->selectrow_array("SELECT cnt from $table_count ; ");

        my $ret = [ Teng::Iterator->new(
            teng             => $self,
            sth              => $sth,
            sql              => $sql,
            row_class        => $self->schema->get_row_class($table_name),
            table            => $table,
            table_name       => $table_name,
            suppress_object_creation => $self->suppress_row_objects,
        )->all];

        my $has_next = ( $rows + 1 == scalar(@$ret) ) ? 1 : 0;
        if ($has_next) { pop @$ret }

        my $current_page = $page;
        my $entries_per_page = $rows;
        my $pager = Data::Page->new($total_entries, $entries_per_page, $current_page, $has_next);

        return ($ret, $pager);
  ....
}

レプリケーションとは、多くのデータベース管理システムが採用している概念で、データベースのオリジナルとコピーのマスタースレーブ関係を定義します。マスタース側は更新を記録し、スレーブ群に通知されます。スレーブ側は更新を正しく受け取った問いうメッセージを送り、次の更新を受け付けられる状態であることを通知します。つまり、マスタースの役は更新情報を保持して、スレーブに更新情報を伝播することです。
レプリケーションを利用する最大のメリットはDB負荷(Disk IO, Memory, CPU使用率)を分散することです。参照系(Select)はスレーブで、更新系(INSERT, UPDATE, DELETE)はマスタースです。
そして、レプリケーションのデータモデリングには、マスタ系データとトランザクション系データという二つのデータの分類があります。マスタ系データは一度登録されたら頻繁には変更されないデータで、トランザクション系データは頻繁には変更・追加されるデータです。
レプリケーション遲延を考慮した設計や実装にする必要がある時にはSelectをマスタースに強制的に向ける必要があります。例えば、スレーブの参照処理の実行タイミングは、マスタースがスレーブに更新情報を伝播した後ではなく、伝播する前となると、参照した先のスレーブには更新した情報が反映されません。ここはこう考えばいいです。マスタ系データはスレーブに向けてたSelectクエリで取る、トランザクション系データはトランザクションの内容によってマスタース側に向けます。
しかし、このやり方は絶対正しいとは言えないです。例えば、更新と参照を同じマスタースのトランザクションにすると、下記の問題を発生することが可能です。

1. Lost Update Problem

トランザクションAが時間点t5にX值に更新して、時間点t6にトランザクションBにまた更新されて、トランザクションAのt5で更新する資料は失いました。

2. Uncommitted Dependency Problem

トランザクションBがトランザクションAの中に更新された資料を取ったが、この資料は時間点t6にトランザクションAでロールバックされた、その結果はトランザクションBで取った資料は間違った資料となりました。

3. Inconsistent Analysis Problem

時間点t1とt2にトランザクションAとBは同時にX值を取りました。両方も同じデータで更新にしますが、トランザクションBはトランザクションAの前にコミットしましたから、結果にトランザクションAは古いデータで更新続きました。
上記の状況を忘れないように、マスター・スレーブに向けてたSelectクエリを考えます。
あとは、データが遲延なしで同步することが見えるようにデータの更新をJsonにする方法があります。データがデプロイする時にメモリ上に展開するようにします。それはデータの参照度が一番高く見えるようになります。
レプリケーションについて、メリットやデメリットは下記のようにまとめることができます。

メリット:

  • DB負荷(Disk IO, Memory, CPU使用率)を分散する、特に参照系のクエリ(Select)はスレーブに分担する
  • サビースに大きく影響を与えず、バッグアップすることができる
  • マスタースが障害発生時、スレーブに切り替えることができる
また上記という通り、レプリケーションには遲延というデメリットがあります。

デメリット:

  • スレッドによって、レプリケーションにある程度な遲延を与える(マスタースレーブの間には完璧な同步が実現できないから)
  • 遲延の間にマスタースが障害発生する場合、変更中の資料はリカバーできない

解決法:

  • 必要なデータを探すときはインデックスで探す
  • 無駄なロッグをしないように、インデックスの範囲を縮める
  • リクエスト回数が多すぎないようにする
  • リクエストのWhere条件も多すぎないようにする
  • 切適なシャーディング策略が必要
それで、DBの設計に戻りました。インデックスが張っていれば、レコードが特定されてそこだけ行ロックできます。逆にインデックスがないと、行が特定できないのでテーブルロックが走して、サーバーのCPUを張り付く問題に発生します。それはサビースに大きな遲延を与えます。また、無駄インデックスが残っていると、意図がないインデックスが使われて、予想外のロックが取られるかもしれません。ここは、ExplainでSQLの実行計画を確認するべきです。実行のコストや回数などの発行が異常に高くなると、注意しなければなれません。

Selectする時の異常状況

1. Dirty Read
Session 1Session 2
SELECT v FROM tt WHERE id = 1;//v = 5
BEGIN; UPDATE tt SET v = 100 WHERE id = 1;//v = 100
SELECT v FROM tt WHERE id = 1;//v = 100
//v = 100 (Dirty Read!)ROLLBACK;//v = 5
あるトランザクションが更新されている最中に、他のトランザクションからデータを読み出すことができてしまう現象です。
2. Non-repeatablbe read
Session 1Session 2
SELECT v FROM tt WHERE id = 1;//v = 5
BEGIN; UPDATE tt SET v = 100 WHERE id = 1;//v = 100
COMMIT; //v = 100
SELECT v FROM tt WHERE id = 1; //v = 100
//v = 5? v=100?
同じデータを読み込むたびに値が他のトランザクションから変わってしまう現象です。
3. Phantom read
Session 1Session 2
SELECT * FROM tt WHERE id = 1;//id = 1, v=5
BEGIN; INSERT INTO tt VALUES ( 2, 8);
COMMIT;
SELECT * FROM tt WHERE id = 1; //d = 1, v=5 id = 2, v=8
並行して動作する他のトランザクションが追加したり削除したデータが途中で見えてしまうため、処理の結果が変わってしまう現象です。
また通常の場合SelectはSlaveから取るので、Isolation levelがREAD COMMITTEDと同じくdirty readsのことから防ぎます。だが、Non-repeatablbe readやPhantom readのことは防げません。ここは注意すべきなことです。
補足ですが、innodbのTransactionのisolation levelはRepeatable readにdefaultされます。同じtransaction以内のselectは他のtransactionのupdateに影響されません。それで、Non-repeatablbe readのことが防ぎます。
しかし、それでもphantom readのことを避けられる保障がありません。通常のやり方はNext-key lockingという方法で解決します。または、isolation levelをSERIALIZABLEにする方法です。だが、この方法には全てのselectもSelect ...lock in share modeにするので、Lockについて実装する必要性を検討します。

Select for Update

データを挿入または更新する時、ほかのトランザクションで同じ行のデータを先に更新するまたは削除することもできます。
Session 1Session 2
SELECT v FROM tt WHERE id = 1;//v = 5
v = v - 1; //v = 4SELECT v FROM tt WHERE id = 1;//v = 5
UPDATE tt SET v = v - 1 WHERE id = 1;//v = 4v = v - 1; //v = 4
UPDATE tt SET v = v - 1 WHERE id = 1;//v = 4
例えば、これはお金の取引でttは >0の状況しかお金払えます(v--)。こういう場合は、session 1でお金が0になったでもSession 2では続き払えます。それで、select for updateが必要です(commitまで、デーダを他のtransactionからロックます)。

Dead Lockの異常状況

Dead Lockとは、2つ以上のプログラムが資源の解放を待っていることです。通常はタイムアウトで処理をやり直します(my.cnfinnodb_lock_wait_timeout = n の待ってる時間を設定する)。
DBの設計にはロックの順番を統一する方法もあります。テーブルの間がまたがってる場合、テーブル順をソートします。参照と更新処理をまぜないようにします。
このエラーが出る原因は、サーバーがタイムアウトして接続が閉じられたことです。 そして、間違ったクエリーまたは長すぎるクエリーをサーバーに送信した場合にも、これらのエラーを受け取ることがあります。

1.サーバータイムアウト

サーバがタイムアウトしたから、接続がクローズします。 デフォルトでは、何も起きない状態が 8 時間続くと、サーバは自動的に接続をクローズします。 この時間は、mysqld 開始時に wait_timeout 変数を設定することで変更できます。

対策:

1) タイムアウトの設定を変更する (以下のクエリは8時間のタイムアウトです)
set global wait_timeout = 28800;
wait_timeoutを設定するのは、MySQLは1つの接続で1つのスレッドを占有するので多くの接続があることでそれだけリソースが消費されてしまうことです。また競合をさけるため多くのlockが必要になり性能劣化の可能性もあります。また、idleが貯まることでMySQLに設定した同時接続数(max_connections)に達し、新しい接続ができないことがでてくると思います。そこを考えると、wait_timeoutはどうやって決めるのはまだわからないです。

2.接続をcloseしている

MySQLとのコネクション上で close を発行し、クローズしたコネクションでクエリを実行しようとしたことが考えられます。

対策:

1) 全ての処理が終わるまでcloseしないようにする 2) クローズした後に再コネクトする 3) {mysql_auto_reconnect=>1} を指定する 4) Scope::Containerを使う
my $dbh = DBI->connect( $dsn, $user, 
  $pass, {mysql_auto_reconnect=>1}) 
    or croak 'connection failed:';=
Model::Baseのやり方と同じく、データベースへの接続を細かく管理するため、Scope::Containerに接続情報をキャッシュして、同じDSNやユーザ名で接続の場合、キャッシュからdbhを返します。そして、任意の範囲(スコープ)だけで接続維持を行い、スコープから外れたら自動で接続を切ることになります。 in_scope_container()Scope::Conitanerが有効になっているかどうか確認し、scope_container()dbhを取得します。
 use Scope::Container;

  sub getdb {
      if ( my $dbh = scope_container('db') ) {
          return $dbh;
      } else {
          my $dbh = DBI->connect(...);
          scope_container('db', $dbh)
          return $dbh;
      }
  }

  for (1..2) {
    my $contaier = start_scope_container();
    getdb(); # do connect
    getdb(); # from container
    # $container scope out and disconnect from db
  }

  getdb(); # do connect

3.クエリが大きすぎる

大きすぎるクエリをサーバに送信した場合も、このエラーが発生する可能性があります。 大きなクエリが必要な場合(たとえば、大きな BLOB カラムを使用している場合)、mysqlのmax_allowed_packetの設定を変更して、クエリ制限を引き上げることができます。max_allowed_packetはデフォルト1MBですが、サーバーおよびクライアント間で転送可能なパケットの最大サイズは1GBです。そして、拡張メモリは要求に応じて割り当てられます。そのため、mysql はユーザが大きなクエリを発行するときやmysqlが大きな結果レコードを返す必要のあるときだけメモリを増やして割り当てます。 1GB以内の数値に変更しればいいんですが、max_allowed_packetのデフォルト値が小さいのはクライアントとサーバーの間の不正なパケットを捕捉するための予防措置であり、誤って大きなパケットが使用されてメモリー不足にならないようにするためでもあります。
それで、ここの記事(what is a reasonable value for maxallowedpacket for Drupal 7 with moderate traffic?)はこう書いていますが、データベースで最大なBLOBカラムの11倍はmax_allowed_packetの値となるってどうやって理解するのはわからないです。

対策:

1) maxallowedpacket の設定を変更する (以下のクエリは500MBです)
set global max_allowed_packet = 52428800;

bulk_insertの件数について

できればinsertのループを減たいという目的は変わらないです。何本のinsertは何回Databaseにqueryを短時間で上げることで、特にinsertは何回テプールをロックすることもあります。それで、大量のinsertはDatabaseに大きな負担をかけます。それ以外には、短時間で大量のqueryを上げるのはnetwork trafficになることも可能です。そういう考えると、maxallowedpacketを最大化にしたほうがいいですが、また問題が出ました。
     
      件数が増えると、bulk insertの効果が減っています。
     それで、時間を改善するため、並列処理にします。 またデータベースへのコネクションについて、Forkを使う時にはForkした子供が親のコネクションをそのまま使ってバグになることがあります。 恐らく原因は、DBIはDESTROY時に自動的にdisconnectするようになっているので子供が勝手に親の接続を切ってしまい、その後親がその接続を使おうとすると使えないということになります。なので、ここは作成の部分をたたいてqueryを集約して返したいです。 つまり、queryの作成を並列処理しつつ、結果をまとめて返してから、DBIのINSERTを実行します。やはり時間かかるのはquery作成の部分です。 ちなみに、Bulk Insertについて、前に言ったbulk_insert_buffer_sizeという変数に関係があるそうですが、また調べでみると、これは MyISAMのみのCacheでした。
       Perlで並列処理を実装するとなると、方法はたくさんあると思います。
  • Fork (Parallel::ForkManager、Parallel::Prefork)
  • Coro
  • AnyEvent
  • Job Queue ....など
      まずはForkについて、これはPerl組み込みのfork関数で実現できるって便利だと思いますが、多くの子プロセスを作る時にfork()を呼びまくるのは面倒くて、Parallel::ForkManagerParallel::Preforkなどのモデルで管理します。(ここはParallel::Preforkのことはまだ見てないですが、概念としてParallel::ForkManagerは並列クライアントを作るもので、Parallel::Preforkは並列できるサーバを作るものです...具体的にはまだ研究中です) そして、Parallel::ForkManager では、run_on_finish()というメソッドで子プロセスからデータを受けることができます。 子プロセスのfinish()のタイミングでpid毎にファイルにStorable::store()して、親プロセスではStorable::retrieve()しています。ですが、run_on_finish()で受けれるデータはHashしかなさそうです?(ちなみにtmp fileは.txtとなります)ここは確定してないですが、前はcannot allocate memoryというエラーを発見しました。しかし、プログラムのコビーを作るなので、メモリをたくさん食ってしまうことは確か!!
     下記はParallel::ForkManagerの実装結果です。


スレッドの数とbulk insertの件数はまたトレードオフの問題があります。 メモリとCPUの限りがあるので、大体二分か三分くらいかかると思います。 上記の結果を考えると、每10000件のbulk insertは時間的に安定だと思います。 bulk insertの件数を10000件にします。 他のやり方はまた整理しています。

     小さい量のデータはともかく、大きな量のデータを使えば、データベースのロードは重いです。 それで、データを分散する必要があります。デーダベースを水平や垂直という二つの分けて分割します。 
   
   「水平分割」とは、テーブルのレコード単位で、別のテーブルで扱います。
    例えば、user_idを100で除算して、テーブル自体を100に分割します。 
   
   「垂直分割」とは、テーブル内の一部の列だけを垂直に分割します。特に更新が多い項目だけを別テーブルに切り出します。
     例えば、ユーザー情報の更新頻度が低い項目(user_loginID, user_descriptionなど)と更新頻度が高い項目(session_idなど)を別のテーブルに分割します。  

     Shardingすることによって、DB性能、管理しやすさ、可用性がアップします。ただ、テーブルを簡単にJoinすることができなくなるので、設計面で考慮すべきことが増えるかもしれません。



トランザクション

トランザクションとは、データに対する一つの論理的な操作することです。データベースの操作について、トランザクションを重視する理由は複数人での更新のケースにデータ更新の並列性を保つこと、及び障害発生時にデータの整合性を保障することを可能になるリカバリーです。
つまり、データベースのACID特性を実現することです。
  • 原子性(atomicity)
  • 一貫性(consistency)
  • 独立性(isolation)
  • 永続性(durability)

1. 原子性(atomicity)

トランザクション内での更新作業中に障害発生等で更新を中断した時に更新前の状態にまでロールバックすることです。

A. トランザクションからのロールバック

  • 更新する資料が間違いた
  • ユーザーの暗証番号が間違いた
  • ユーザから更新を中断する 

B. データベースからのロールバック

  • データベースがオーバーロードした
  • デッドロックが発生した

2. 一貫性 (Consistency)

トランザクション前後のデータは整合性を保ちます。もっと簡単といえば、AがBに100円を渡す時、Aの口座には100円が減ったのに、Bの口座には100円が増えないという状況から避けることです。

3.独立性 (Isolation)

データベースのロックによる排他制御があります。それは同一のデータに対して複数のスレッドが操作をする可能性があるとき(複数人のユーザーが操作するとき)、データの整合性を保つため、複数のスレッドが同一のデータを操作しないように制御することです。

4. 永続性(Durability)

これはサーバーがフリーズした(ロールバック指示がない)場合、サーバーが再起動する時にデータベースがTransaction Logを利用して、自動的にトランザクションをロールバックすることを判断することです。

デッドロック問題

障害時のリカバリを迅速に行う、整合性を保証する、トランザクションはかなにいい提案ですが、これはシングルユーザーだけでの話です。マルチユーザーの場合は、複数のトランザクションが同時に同じデータを更新することが可能です。それで、デッドロックという問題が発生します。
デッドロックとは、2つ以上のプログラムが資源の解放を待っていることです。通常はタイムアウトで処理をやり直しますが、DBの設計にはロックの順番を統一する方法もあります。テーブルの間がまたがってる場合、テーブル順をソートします。参照と更新処理をまぜないようにします。
ちなみに、MySQLは親切です。同じDB内のデッドロックを検知したら、自動的にロールバックにしてロックを解除してくれますよ。


explain SELECT user_id, user_loginID, user_pic 
FROM users 
WHERE user_loginID='user1234' AND user_password_encrypted='aaa1234' 
AND user_status=1;
+----+-------------+-------+-------+-------------------------------------------------------------+---------------------+---------+-------+------+-------+
| id | select_type | table | type  | possible_keys                                               | key                 | key_len | ref   | rows | Extra |
+----+-------------+-------+-------+-------------------------------------------------------------+---------------------+---------+-------+------+-------+
|  1 | SIMPLE      | users | const | user_loginID_UNIQUE,loginID_pw_index,loginID_emailcrc_index | user_loginID_UNIQUE | 50      | const |    1 | NULL  |
+----+-------------+-------+-------+-------------------------------------------------------------+---------------------+---------+-------+------+-------+
1 row in set (0.00 sec)

以上面的SELECT為例,EXPLAIN後的資料大約有這些欄位:

id: 
MySQL Query Optimizer 選定的執行計劃中查詢的序列號。
表示查詢中執行select子句或操作表的順序,id 值越大優先級越高,越先被執行。
id 相同,執行順序由上至下。

select_type
查詢的類型


查詢的類型 說明
SIMPLE 簡單的select 查詢,不使用union 及子查詢
PRIMARY Content Cell 最外層的select 查詢
UNION UNION 中的第二個或隨後的select 查詢,不依賴於外部查詢的結果集
DEPENDENT UNION UNION 中的第二個或隨後的select 查詢,依賴於外部查詢的結果集
SUBQUERY 子查詢中的第一個select 查詢,不依賴於外部查詢的結果集
DEPENDENT SUBQUERY 子查詢中的第一個select 查詢,依賴於外部查詢的結果集
DERIVED 用於from 子句裡有子查詢的情況。MySQL 會遞歸執行這些子查詢, 把結果放在臨時表裡
UNCACHEABLE SUBQUERY 結果集不能被緩存的子查詢,必須重新為外層查詢的每一行進行評估
UNCACHEABLE UNION UNION 中的第二個或隨後的select 查詢,屬於不可緩存的子查詢

table
關連到的資料表

type
使用關聯查詢的類型
(效率由好至壞排序)


關聯查詢的類型 說明
System 表僅有一行(=系統表),此為const連接類型的特殊情況
Const 資料表中的一個記錄的最大值能夠符合這個查詢。因為只有一行,這個值就是常數,因為MySQL會先讀這個值然後把它當做常數
eq_ref MySQL在連接查詢時,會從最前面的資料表,對每一個記錄的聯合,從資料表中讀取一個記錄,在查詢時會使用索引為主鍵或唯一鍵的全部
ref 只有在查詢使用了非唯一鍵或主鍵時才會發生。連接不能基於關鍵字選擇單個行,可能查找到多個符合條件的行。叫做ref 是因為索引要跟某個參考值相比較。這個參考值或者是一個常數,或者是來自一個表裡的多表查詢的結果值
fulltext
ref_ or_ null 同ref, 但是MySQL 必須在初次查找的結果裡找出null 條目,然後進行二次查找
index_merge 說明索引合併優化被使用了
unique_subquery 在某些IN 查詢中使用此種類型,而不是常規的ref:value IN (SELECT primary_key FROM single_table WHERE some_expr)
index_subquery 在某些IN 查詢中使用此種類型, 與unique_subquery 類似,但是查詢的是非唯一性索引: value IN (SELECT key_column FROM single_table WHERE some_expr)
range 只檢索給定範圍的行,使用一個索引來選擇行。key 列顯示使用了哪個索引。當使用=、 <>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者IN 操作符,用常量比較關鍵字列時,可以使用range
index 全表掃描,只是掃描表的時候按照索引次序進行而不是行。主要優點就是避免了排序, 但是開銷仍然非常大
ALL 針對每一筆記錄進行完全掃描,此為最壞的情況,應該盡量避免

possible_keys
可能使用到的索引,從WHERE語法選擇出一個適合的欄位

key
MySQL 實際從possible_key 選擇使用的索引。
如果為NULL,則沒有使用索引。
很少的情況下,MYSQL 會選擇優化不足的索引。
這種情況下,可以在SELECT 語句中使用USE INDEX (indexname)來強制使用一個索引
或者用IGNORE INDEX(indexname)來強制MYSQL 忽略索引

key_len
使用索引的長度,長度越短,準確性越高

ref
顯示那一列的索引被使用,一般是一個常數(const)

rows
MySQL認為必須檢查的用來返回請求數據的行數,可以簡單的把rows視為執行效能,
越少越好

Extra
MySQL用來解析額外的查詢訊息
(效率由好至壞排序)


額外的查詢訊息 說明
Using where 使用WHERE語法中的欄位來返回結果
Using index 返回的資料是從索引中資料,而不是從實際的資料中返回,當返回的資料都出現在索引中的資料時就會發生此情況
Using filesort 表示MySQL會對結果使用一個外部索引排序,而不是從表裡按索引次序讀到相關內容。可能在內存或者磁盤上進行排序。MySQL 中無法利用索引完成的排序操作稱為“文件排序”。MySQL必須進行額外的步驟來進行查詢
Using temporary 此為MySQL必須建立一個暫時的資料表(Table)來儲存結果,此情況會發生在針對不同的資料進行ORDER BY和GROUP BY。

SELECT * FROM demotable ORDER BY id LIMIT 2,4
LIMIT後面接 [index, count]:
  • Index:從哪個index開始傳回,Index是從0開始算,若以這個範例來看,資料會由第3筆資料開始回傳(2+1=3)
  • count:由index開始,總共要傳回幾筆資料,以此範例來說,總共會傳回第3、4、5、6共四筆資料
SELECT * FROM demotable ORDER BY id LIMIT 4 OFFSET 2
有些SQL的資料庫不支援[index,count]的格式(例如PostgreSQL),必需要用OFFSET來取代,OFFSET可以把它想成要略過筆數,以OFFSET 2為例,便可想成在找到的資料筆數中略過前二筆,即是由第三筆開始回傳。
但是隨著OFFSET (index) 的數量越大,其檢索性能便會越差。例如頭10萬或30萬筆數只需0.01秒,而去到50萬筆以上則需要3秒以上。為什麼會出現這樣的時間差異呢?其原因就在於表的掃描!假設我們要去執行LIMIT 5 OFFSEST 200000的資料讀取,MySQL便需要掃描出滿足條件的200005(200000+5)行的資料,然後再扔掉前面的200000行,返回最後的5行,OFFSET愈大當然會掃描得愈多。因此針對OFFSET問題我們需作出更多的優化考慮。

優化前SQL:
SELECT * FROM demoTable ORDER BY time
LIMIT 200000,5

優化後SQL:
SELECT * FROM demoTable
JOIN 
(
  SELECT id FROM DemoTable ORDER BY times
  LIMIT 200000,5
)
USING (id)

另外,IN也是其中一個方法,但可惜我的MySQL還未更新。
SELECT * FROM demoTable
WHERE id IN
(
  SELECT id FROM demoTable ORDER BY times
  LIMIT 200000,5
);
//ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery’

這個是不用OFFSET直接用WHERE來指向讀取index位置,但不適用於Time或Like Count的排序。
SELECT id
FROM demoTable
WHERE id > 200000
ORDER BY id desc
LIMIT 6 OFFSET 0 

分別在於,優化前的SQL需要浪費更多IO,而優化後的SQL則是預先讀取Primary key,後再以Primary key讀取數據,使其聽取速度加快。

首先是只讀取Primary key的部分。

除了INDEX會影響讀取的速度外,讀取的欄位也會一定程度的影響(有時遠比INDEX所帶來的影響更甚!),欄位讀取愈多愈會加重CPU以及內存的負擔,大大減低了整體IO效能。
SELECT * FROM demotable ORDER BY time DESC LIMIT 200000,5; //執行2秒
SELECT id FROM demotable ORDER BY time DESC LIMIT 200000,5; //執行0.5秒

而之後的部分便是以Primary Key來讀取真正需要的資料。
由於Subquery已幫我們篩選出了5筆資料的id,而且還是以primary key 當條件,讀取起來並不會費事。

整體而言,我們只需處理好subquery𥚃的index就可以了。
但需要注意的是MySQL會遞歸執行的subquery結果放在臨時表裡,因此太大的LIMIT也會影響效能。而且subquery本身也讓SQL的執行更加複雜化,太多的subquery反而不能達到預期效果。