21

更新:私は解決策を見つけました。以下の私の答えを参照してください。

私の質問

このクエリを最適化してダウンタイムを最小限に抑えるにはどうすればよいですか?チケットの数が100,000から200万の範囲で、50を超えるスキーマを更新する必要があります。ticket_extraのすべてのフィールドを同時に設定することをお勧めしますか?私はここに私が見ていなかった解決策があると感じています。私はこの問題に1日以上頭をぶつけてきました。

また、最初はサブSELECTを使用せずに試しましたが、現在のパフォーマンスよりもはるかにパフォーマンスが低下しました。

バックグラウンド

実行する必要のあるレポート用にデータベースを最適化しようとしています。集計する必要のあるフィールドは計算に非常にコストがかかるため、このレポートに対応するために既存のスキーマを少し非正規化しています。数十の無関係な列を削除することで、チケットテーブルをかなり単純化したことに注意してください。

私のレポートは、作成時マネージャーと解決時のマネージャーごとにチケット数を集計します。この複雑な関係をここに示します。

EAV
(出典:mosso.com

このオンザフライで計算するために必要な半ダースの厄介な結合を回避するために、次のテーブルをスキーマに追加しました。

mysql> show create table tickets_extra\G
*************************** 1. row ***************************
       Table: tickets_extra
Create Table: CREATE TABLE `tickets_extra` (
  `ticket_id` int(11) NOT NULL,
  `manager_created` int(11) DEFAULT NULL,
  `manager_resolved` int(11) DEFAULT NULL,
  PRIMARY KEY (`ticket_id`),
  KEY `manager_created` (`manager_created`,`manager_resolved`),
  KEY `manager_resolved` (`manager_resolved`,`manager_created`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

現在の問題は、このデータをどこにも保存していないことです。マネージャーは常に動的に計算されました。同じスキーマを持つ複数のデータベースに数百万のチケットがあり、このテーブルにデータを入力する必要があります。できるだけ効率的な方法でこれを実行したいのですが、使用しているクエリを最適化できませんでした。

INSERT INTO tickets_extra (ticket_id, manager_created)
SELECT
  t.id, 
  su.user_id
FROM (
  SELECT 
    t.id, 
    shift_times.shift_id AS shift_id 
  FROM tickets t
  JOIN shifts ON t.shop_id = shifts.shop_id 
  JOIN shift_times ON (shifts.id = shift_times.shift_id
  AND shift_times.dow = DAYOFWEEK(t.created)
  AND TIME(t.created) BETWEEN shift_times.start AND shift_times.end)
) t
LEFT JOIN shifts_users su ON t.shift_id = su.shift_id
LEFT JOIN shift_positions ON su.shift_position_id = shift_positions.id
WHERE shift_positions.level = 1

このクエリは、170万を超えるチケットを持つスキーマで実行するのに1時間以上かかります。これは、私が持っているメンテナンスウィンドウには受け入れられません。また、manager_resolvedフィールドの計算も処理しません。これを同じクエリに結合しようとすると、クエリ時間が成層圏にプッシュされるためです。私の現在の傾向は、それらを分離し、UPDATEを使用してmanager_resolvedフィールドに入力することですが、よくわかりません。

最後に、そのクエリのSELECT部分​​のEXPLAIN出力を次に示します。

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: <derived2>
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 167661
        Extra: 
*************************** 2. row ***************************
           id: 1
  select_type: PRIMARY
        table: su
         type: ref
possible_keys: shift_id_fk_idx,shift_position_id_fk_idx
          key: shift_id_fk_idx
      key_len: 4
          ref: t.shift_id
         rows: 5
        Extra: Using where
*************************** 3. row ***************************
           id: 1
  select_type: PRIMARY
        table: shift_positions
         type: ALL
possible_keys: PRIMARY
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 6
        Extra: Using where; Using join buffer
*************************** 4. row ***************************
           id: 2
  select_type: DERIVED
        table: t
         type: ALL
possible_keys: fk_tickets_shop_id
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 5. row ***************************
           id: 2
  select_type: DERIVED
        table: shifts
         type: ref
possible_keys: PRIMARY,shop_id_fk_idx
          key: shop_id_fk_idx
      key_len: 4
          ref: dev_acmc.t.shop_id
         rows: 1
        Extra: 
*************************** 6. row ***************************
           id: 2
  select_type: DERIVED
        table: shift_times
         type: ref
possible_keys: shift_id_fk_idx
          key: shift_id_fk_idx
      key_len: 4
          ref: dev_acmc.shifts.id
         rows: 4
        Extra: Using where
6 rows in set (6.30 sec)

読んでくれてありがとう!

4

4 に答える 4

13

さて、私は解決策を見つけました。それは多くの実験を要しました、そして私はかなりの盲目的な運を思います、しかしここにそれはあります:

CREATE TABLE magic ENGINE=MEMORY
SELECT
  s.shop_id AS shop_id,
  s.id AS shift_id,
  st.dow AS dow,
  st.start AS start,
  st.end AS end,
  su.user_id AS manager_id
FROM shifts s
JOIN shift_times st ON s.id = st.shift_id
JOIN shifts_users su ON s.id = su.shift_id
JOIN shift_positions sp ON su.shift_position_id = sp.id AND sp.level = 1

ALTER TABLE magic ADD INDEX (shop_id, dow);

CREATE TABLE tickets_extra ENGINE=MyISAM
SELECT 
  t.id AS ticket_id,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.created) = m.dow
    AND TIME(t.created) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_created,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.resolved) = m.dow
    AND TIME(t.resolved) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_resolved
FROM tickets t;
DROP TABLE magic;

長い説明

ここで、これが機能する理由と、ここに到達するためのプロセスと手順を説明します。

最初に、私が試みていたクエリが巨大な派生テーブルのために苦しんでいることを知っていました、そしてそれに続くJOINはこれに。インデックスが適切なチケットテーブルを取得し、すべてのshift_timesデータをそのテーブルに結合してから、MySQLがshiftsおよびshift_positionsテーブルを結合しようとしている間、それを噛み砕きました。この派生した巨大なものは、最大200万行のインデックス付けされていない混乱になります。

今、私はこれが起こっていることを知っていました。しかし、私がこの道を進んだ理由は、厳密にJOINを使用してこれを行う「適切な」方法には、さらに長い時間がかかっていたためです。これは、特定のシフトのマネージャーが誰であるかを判断するために必要な厄介な混乱によるものです。正しいシフトが何であるかを知るためにshift_timesに参加すると同時に、ユーザーのレベルを把握するためにshift_positionsに参加する必要があります。MySQLオプティマイザはこれをうまく処理できないと思います。最終的には、結合の一時テーブルの巨大な怪物を作成し、適用されないものを除外します。

それで、派生したテーブルが「進むべき道」であるように思われたので、私はしばらくこれに頑固に固執しました。私はそれをJOIN句にパントしてみましたが、改善はありませんでした。派生テーブルを含む一時テーブルを作成しようとしましたが、一時テーブルのインデックスが作成されていないため、速度が遅すぎました。

このシフト、時間、位置の計算を適切に処理する必要があることに気づきました。たぶん、VIEWが行く方法だろうと思いました。この情報を含むVIEWを作成した場合はどうなりますか:(shop_id、shift_id、dow、start、end、manager_id)。次に、shop_idとDAYOFWEEK / TIMEの計算全体でチケットテーブルに参加するだけで、ビジネスを開始できます。もちろん、MySQLがVIEWをかなり慎重に処理することを思い出せませんでした。それらはまったく具体化されません。ビューを取得するために使用したクエリを実行するだけです。したがって、これにチケットを結合することにより、私は基本的に元のクエリを実行していました-改善はありません。

そこで、VIEWの代わりにTEMPORARYTABLEを使用することにしました。これは、一度に1つのマネージャー(作成または解決済み)のみをフェッチした場合はうまく機能しましたが、それでもかなり低速でした。また、MySQLでは、同じクエリで同じテーブルを2回参照できないことがわかりました(manager_createdとmanager_resolvedを区別できるようにするには、一時テーブルを2回結合する必要があります)。これは大きなWTFであり、「TEMPORARY」を指定しない限り実行できます。ここで、CREATETABLEマジックENGINE=MEMORYが機能します。

この疑似一時テーブルを使用して、manager_createdだけでJOINを再試行しました。それはうまく機能しましたが、それでもかなり遅いです。それでも、同じクエリでmanager_resolvedを取得するために再度参加したとき、クエリ時間は成層圏に戻ってきました。EXPLAINを見ると、予想どおり、チケットの全表スキャン(行〜2mln)が示され、それぞれ〜2,087でマジックテーブルに結合されています。繰り返しますが、私は失敗に直面しているようでした。

私は今、JOINを完全に回避する方法について考え始めました。そのとき、誰かが副選択を使用することを提案した、あいまいな古代の掲示板の投稿を見つけました(私の履歴にリンクが見つかりません)。これが、上記の2番目のSELECTクエリ(tickets_extra作成クエリ)につながったものです。単一のマネージャーフィールドのみを選択した場合、それはうまく機能しましたが、両方ともそれはがらくたでした。EXPLAINを見て、これを見ました:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 2. row ***************************
           id: 3
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
3 rows in set (0.00 sec)

ああ、恐ろしい依存サブクエリ。MySQLは通常、これらを外部から実行し、外部のすべての行に対して内部クエリを実行するため、これらを回避することをお勧めします。私はこれを無視して、「まあ...このばかげた魔法のテーブルにインデックスを付けたらどうなるだろうか」と疑問に思いました。このようにして、ADDインデックス(shop_id、dow)が生まれました。

これをチェックしてください:

mysql> CREATE TABLE magic ENGINE=MEMORY
<snip>
Query OK, 3220 rows affected (0.40 sec)

mysql> ALTER TABLE magic ADD INDEX (shop_id, dow);
Query OK, 3220 rows affected (0.02 sec)

mysql> CREATE TABLE tickets_extra ENGINE=MyISAM
<snip>
Query OK, 1933769 rows affected (24.18 sec)

mysql> drop table magic;
Query OK, 0 rows affected (0.00 sec)

今それが私が話していることです

結論

単一のクエリを効率的に実行するために、非TEMPORARYテーブルをその場で作成し、その場でインデックスを作成するのは、これが初めてです。その場でインデックスを追加することは、法外に費用のかかる操作だといつも思っていたと思います。(2mln行のチケットテーブルにインデックスを追加すると、1時間以上かかる場合があります)。それでも、たった3,000行の場合、これは簡単なことです。

DEPENDENT SUBQUERIESを恐れないでください。実際にはそうではない一時テーブルを作成したり、その場でインデックスを作成したり、エイリアンを作成したりします。それらはすべて、適切な状況で良いものになる可能性があります。

StackOverflowのすべてのヘルプに感謝します。:-D

于 2009-07-26T01:19:45.733 に答える
2

あなたはPostgresを使うべきでした、笑。ディスクのスラッシングを回避するのに十分な RAM があれば、このような単純なクエリは数十秒以上かかることはありません。

ともかく。

=> 問題は SELECT または INSERT にありますか?

(テスト サーバーで SELECT を単独で実行し、時間を計ります)。

=> クエリのディスク バウンドまたは CPU バウンドはありますか?

テスト サーバーで起動し、vmstat の出力を確認します。CPU バウンドの場合は、これをスキップしてください。ディスクにバインドされている場合は、ワーキング セットのサイズ (つまり、データベースのサイズ) を確認してください。ワーキング セットが RAM よりも小さい場合は、ディスクにバインドしないでください。SELECT sum( some column ) FROM table のようなダミー選択を起動することにより、クエリを実行する前に OS キャッシュにテーブルを強制的にロードできます。これは、クエリが RAM にキャッシュされていないテーブルからランダムな順序で多くの行を選択する場合に役立ちます...テーブルのシーケンシャル スキャンをトリガーし、キャッシュにロードすると、ランダム アクセスがはるかに高速になります。いくつかの策略で、インデックスをキャッシュすることもできます (または、データベース ディレクトリを >/dev/null に tar するだけです (笑))。

もちろん、RAM を追加すると効果があります (ただし、最初に、クエリがディスクまたは CPU を強制終了しているかどうかを確認する必要があります)。または、構成 (key_buffer など) でより多くの RAM を使用するように MySQL に指示します。

何百万ものランダムな HDD シークを行っている場合は、苦痛です。

=> OK クエリ

まず、テーブルを分析します。

LEFT JOIN shift_positions ON su.shift_position_id = shift_positions.id WHERE shift_positions.level = 1

LEFT JOIN に WHERE を追加するのはなぜですか? 左は意味がありません。shift_positions に行がない場合、LEFT JOIN は NULL を生成し、WHERE はそれを拒否します。

解決策: LEFT JOIN の代わりに JOIN を使用し、JOIN ON() 条件で移動 (レベル = 1) します。

あなたがそれに取り組んでいる間、それらすべての NULL に本当に興味がない限り、他の LEFT JOIN (JOIN に置き換えます) も取り除きますか? (あなたはそうではないと思います)。

これで、おそらく副選択を取り除くことができます。

次。

WHERE TIME(t.created) BETWEEN shift_times.start AND shift_times.end)

これはインデックス可能ではありません。条件に関数 TIME() があるためです (Postgres を使用してください、笑)。それを見てみましょう:

JOIN shift_times ON (shifts.id = shift_times.shift_id AND shift_times.dow = DAYOFWEEK(t.created) AND TIME(t.created) BETWEEN shift_times.start AND shift_times.end)

理想的には、shift_times(shift_id, DAYOFWEEK(t.created),TIME(t.created)) に複数列のインデックスを作成して、この JOIN にインデックスを付けることができます。

解決策 : DAYOFWEEK(t.created)、TIME(t.created) を含む列 'day'、'time' を shift_times に追加し、INSERT または UPDATE で起動するトリガーを使用して正しい値を入力します。

(shift_id,day,time) に複数列のインデックスを作成する

于 2009-07-25T10:42:00.800 に答える
0

これにより、変更期間中は読み取り専用アクセスが可能になります。

create table_new (new schema);
insert into table_new select * from table order by primary_key_column;
rename table to table_old;
rename table_new to table;
-- recreate triggers if necessary

InnoDBテーブルにデータを挿入するときは、主キーの順序でこれを行うことが重要です(そうでない場合、大きなデータセットでは数桁遅くなります)。

于 2009-07-25T02:11:29.123 に答える
0

BETWEENについて

SELECT * FROM a WHERE a.column BETWEEN x AND y 
  • インデックス可能であり、インデックス a.column の範囲ルックアップに対応します (ある場合)
  • は 100% に等しいa.column >= x AND a.column <= y

これの間:

SELECT * FROM a WHERE somevalue BETWEEN a.column1 AND a.column2
  • は 100% に等しいsomevalue >= a.column1 AND somevalue <= a.column2
  • 上記の最初のものとは非常に異なるものです
  • 範囲ルックアップではインデックス付けできません (範囲がありません。ここには 2 つの列があります)
  • 一般に、ひどいクエリパフォーマンスにつながります

上記の「間」の議論では、これについて混乱があったと思います。

OPは第1種なのでご安心を。

于 2009-07-25T21:57:39.740 に答える