0

以下に説明する2つのMySQLデータベーステーブルがあります。1つのテーブルはデバイス情報を保持し、もう1つは各デバイスに関する1対多のログです。

CREATE TABLE  `device` (
  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(255) NOT NULL,
  `active` INT NOT NULL DEFAULT 1,
  INDEX (`active`)
);

CREATE TABLE  `log` (
  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `device_id` INT NOT NULL,
  `message` VARCHAR(255) NOT NULL,
  `when` DATETIME NOT NULL,
  INDEX (`device_id`)
);

私がやりたいのは、単一のクエリで各デバイスの最新のログエントリとともにデバイス情報を取得することです(可能な場合)。これまでのところ、私が持っているのは次のとおりです。

SELECT d.id, d.name, l.message
FROM device AS d
LEFT JOIN (
  SELECT l1.device_id, l1.message
  FROM log AS l1
  LEFT JOIN log AS l2 ON (l1.device_id = l2.device_id AND l1.when < l2.when)
  WHERE l2.device_id IS NULL
) AS l ON (d.id = l.device_id)
WHERE d.active = 1
GROUP BY d.id
ORDER BY d.id ASC;

これらのクエリは、実際の設定を簡略化して再現したもので、ログテーブルは10万行を超えています(実際には、いくつかのログテーブルがあります)。ただし、クエリの実行は非常に遅くなります(たとえば、2分以上)。このクエリを作成して必要なデータを取得するための、より簡潔でエレガントな「SQL」の方法があると確信していますが、まだ見つけていません。

醜いサブSELECTと自己結合なしでも私がやりたいことは可能ですか?別の戦略で仕事を終わらせることはできますか?または、クエリの性質自体が還元不可能なほど複雑なものですか?

繰り返しになりますが、アプリケーションロジックは、これが機能しない場合にテーブルを「手動で参加」できるようになっていますが、MySQLは窒息することなくこのようなものを処理できるはずですが、それが実現したときは確かに環境に配慮しています。この種の複雑な集合の代数に。

編集:これは不自然な例なので、インデックスを追加するのを忘れていましたdevice.active

4

3 に答える 3

3

自己結合を回避するクエリへのわずかに異なるアプローチは次のとおりです。

SELECT d.id, d.name, l.message
FROM device AS d
LEFT JOIN (
  SELECT l1.device_id, l1.message
  FROM log AS l1
  WHERE l1.when = (
        SELECT MAX(l2.when)
        FROM log AS l2
        WHERE l2.device_id = l1.device_id
  ) l ON l.device_id = d.id
WHERE d.active = 1
ORDER BY d.id ASC;

100kはそれほど大きなテーブルではないため、適切なインデックスがなくても、このクエリに数秒以上かかることはないと思います。ただし、コメントが示唆しているように、の結果に基づいてインデックスを追加することを検討してくださいexplain plan

于 2012-07-30T21:39:39.157 に答える
1

ログテーブルのインスタンスを1つだけ必要とする代替方法は次のとおりです。

SELECT    d.id, d.name, 
          SUBSTRING_INDEX(
              GROUP_CONCAT(
                  l.message 
                  SEPARATOR '~' 
                  ORDER BY l.when DESC
              ) 
          ,   '~'
          ,   1
          )
FROM      device d
LEFT JOIN log    l
ON        d.id = l.device_id
WHERE     d.active = 1
GROUP BY  d.id

このクエリは、日付の降順で並べ替えられた、チルダで区切られたメッセージのリストを作成することにより、最後のログメッセージを検索します。それはによって行われますGROUP_CONCAT。そのSUBSTRING_INDEXリストの最初のエントリのチップ。

このアプローチには2つの欠点があります。

  • を使用しますGROUP_CONCAT。その関数の結果が長くなりすぎると、結果は切り捨てられます。あなたがそうするならば、あなたはそれを直すことができます

    SET @@group_concat_max_len = @@max_allowed_packet;

クエリを実行する前に。それよりもさらにうまくいくことができます。メッセージを1つだけ取得することに関心がgroup_concat_max_lenあるため、列の最大文字長と同じ大きさに設定できmessageます。これにより、を使用する場合と比較してかなりのメモリを節約できます@@max_alowed_packet

  • '~'メッセージテキスト内に表示してはならない特別な区切り文字(この例では、チルダ())に依存しています。メッセージテキスト内に表示されないことが確実である限り、これを任意の区切り文字列に変更できます。

これらの制限に耐えられる場合は、このクエリがおそらく最速です。

これは、あなたと同じくらい複雑ですが、パフォーマンスが向上する可能性のある、より多くの選択肢があります。

SELECT    d.id
,         d.name
,         l.message
FROM      (
          SELECT    d.id, d.name, MAX(l.when) lmax
          FROM      device d
          LEFT JOIN log    l
          ON        d.id = l.device_id
          WHERE     d.active  = 1
          GROUP BY  d.id
          ) d
LEFT JOIN log       l
ON        d.id   = l.device_id
AND       d.lmax = l.when
ORDER BY d.id ASC;

別の選択肢:

SELECT    d.id
,         d.name
,         l2.message
FROM      device d
LEFT JOIN (
          SELECT   l.device_id
          ,        MAX(l.when) lmax
          FROM     log l
          GROUP BY l.device_id
          ) l1
ON        d.id = l1.device_id 
LEFT JOIN log       l2
ON        l1.device_id = l2.device_id
AND       l1.lmax      = l2.when
WHERE     d.active     = 1
ORDER BY  d.id ASC;
于 2012-07-30T22:07:53.780 に答える
0

あなたの質問、そして以下の戦略はインデックスから利益を得るでしょうON log(device_id,when)ON log(device_id)そのインデックスは冗長になるため、そのインデックスを置き換えることができます。


デバイスごとに大量のログエントリがある場合、クエリのJOINは適切なサイズの中間結果セットを生成し、デバイスごとに1行にフィルターされます。MySQLオプティマイザにその反結合操作の「ショートカット」があるとは思いません(少なくとも5.1では)...しかし、クエリが最も効率的かもしれません。

Q:別の戦略で仕事を終わらせることはできますか?

はい、他にも戦略がありますが、これらのいずれかがクエリよりも「優れている」かどうかはわかりません。


アップデート:

検討する可能性のある戦略の1つは、各デバイスの最新のログエントリを保持する別のテーブルをスキーマに追加することです。これは、テーブルで定義されたTRIGGERによって維持できlogます。挿入のみを実行している場合(最新のログエントリのUPDATEおよびDELETEがない場合、これはかなり簡単です。logテーブルに対して挿入が実行されるたびに、トリガーが起動され、ログテーブルに挿入されている値がAFTER INSERT FOR EACH ROW比較されます。 whendevice_idをテーブルの現在のwhen値に変更し、log_latestテーブルの行を挿入/更新して、log_latest最新の行が常に存在するようにします。また、デバイス名をテーブルに(冗長に)格納することもできます(または、latest_whenlatest_messageデバイステーブルへの列、およびそれらをそこで維持します。)

しかし、この戦略は元の質問を超えています...しかし、「すべてのデバイスの最新のログメッセージ」クエリを頻繁に実行する必要があるかどうかを検討することは実行可能な戦略です。欠点は、余分なテーブルがあり、logテーブルへの挿入を実行するとパフォーマンスが低下することです。このテーブルは、元のクエリのようなクエリ、または以下の代替手段を使用して完全に更新できます。


1つのアプローチは、テーブルdevicelogテーブルの単純な結合を実行し、デバイス順および降順で行を取得するクエリですwhen。次に、メモリ変数を使用して行を処理し、「最新の」ログエントリを除くすべてを除外します。このクエリは余分な列を返すことに注意してください。(この余分な列は、クエリ全体をインラインビューとしてラップすることで削除できますが、追加の列が返される状態で生きることができれば、パフォーマンスが向上する可能性があります。

SELECT IF(s.id = @prev_device_id,0,1) AS latest_flag
     , @prev_device_id := s.id AS id
     , s.name
     , s.message
  FROM (SELECT d.id
             , d.name
             , l.message
          FROM device d
          LEFT
          JOIN log l ON l.device_id = d.id
         WHERE d.active = 1
         ORDER BY d.id, l.when DESC
       ) s
  JOIN (SELECT @prev_device_id := NULL) i
HAVING latest_flag = 1

SELECTリストの最初の式が実行しているのは、その行のデバイスID値が前の行のデバイスIDと異なる場合は常に、その行を「マーク」することです。HAVING句は、1でマークされていないすべての行を除外します(HAVING句を省略して、その式がどのように機能するかを確認できます)。

(構文エラーについてはテストしていません。エラーが発生した場合はお知らせください。詳しく調べます。デスクチェックで問題ないと表示されます...ただし、パレンまたはコンマを見逃した可能性があります)

(別のクエリでラップすることで、その余分な列を「取り除く」ことができます

SELECT r.id,r.name,r.message FROM (
/* query from above */
) r

(ただし、これはパフォーマンスに影響を与える可能性があります。追加の列を使用できる場合は、パフォーマンスが向上する可能性があります。)

もちろん、最も外側のクエリにORDER BYを追加して、結果セットが必要な方法で順序付けられるようにします。

このアプローチは、多数のデバイスでかなりうまく機能し、ログ内の関連する行は2、3行のみです。そうしないと、(ログテーブルの行数のオーダーで)中間結果セットの巨大な混乱が発生し、一時的なMyISAMテーブルにスピンアウトする必要があります。

アップデート:

基本的にすべての行を取得している場合device(述語があまり選択されていない場合)、テーブル内のすべてのdevice_idの最新のログエントリを取得し、logテーブルへの結合を延期することで、パフォーマンスを向上させることができdeviceます。(ただし、結合を行うためにその中間結果セットでインデックスを使用できないことに注意してください。そのため、パフォーマンスを測定するために実際にテストする必要があります。)

SELECT d.id
     , d.name
     , t.message
  FROM device d 
  LEFT
  JOIN (SELECT IF(s.device_id = @prev_device_id,0,1) AS latest_flag
             , @prev_device_id := s.device_id AS device_id
             , s.messsage
          FROM (SELECT l.device_id
                     , l.message
                  FROM log l
                 ORDER BY l.device_id DESC, l.when DESC
               ) s
          JOIN (SELECT @prev_device_id := NULL) i
        HAVING latest_flag = 1
       ) t
    ON t.device_id = d.id

注: inlineビューのORDER BY句のdevice_idと列の両方に降順を指定します。これは、device_idの降順で行が必要なためではなく、MySQLが「逆」を実行できるようにすることでファイルソート操作を回避できるようにするためです。先頭の列(device_id、when)を持つインデックスに対する「スキャン」操作。whens

注:このクエリは、中間結果セットを一時的なMyISAMテーブルとしてスプールし、それらにインデックスはありません。したがって、これは元のクエリほどには機能しない可能性があります。


もう1つの戦略は、SELECTリストで相関サブクエリを使用することです。ログテーブルから返される列は1つだけなので、これは非常に簡単に理解できるクエリです。

SELECT d.id
     , d.name
     , ( SELECT l.message
           FROM log l
          WHERE l.device_id = d.id
          ORDER BY l.when DESC 
          LIMIT 1
       ) AS message
  FROM device d
 WHERE d.active = 1
 ORDER BY d.id ASC;

注:idはテーブル内のPRIMARY KEY(またはUNIQUE KEY)であり、device余分な行を生成するJOINを実行していないため、句を省略できますGROUP BY

注:このクエリは「ネストされたループ」操作を使用します。つまり、deviceテーブルから返された行ごとに、(基本的に)個別のクエリを実行して、ログから関連する行を取得する必要があります。ほんのdevice数行(テーブル上でより選択的な述語で返されるようにdevice)、および各デバイスのログエントリのボートロードがあれば、パフォーマンスはそれほど悪くはありません。ただし、ログメッセージが数個しかない多くのデバイスの場合、他のアプローチの方がはるかに効率的である可能性が非常に高くなります。)

また、このアプローチでは、SELECTリストに別のサブクエリ(最初のサブクエリと同様)を追加し、LIMIT句をスキップするように変更するだけで、2番目に新しいログメッセージを別の列として返すように簡単に拡張できることに注意してください。最初の行を取得し、代わりに2番目の行を取得します。

     , ( SELECT l.message
           FROM log l
          WHERE l.device_id = d.id
          ORDER BY l.when DESC 
          LIMIT 1,1
       ) AS message_2

基本的にデバイスからすべての行を取得するには、JOIN操作を使用して最高のパフォーマンスを得る可能性があります。このアプローチの1つの欠点は、デバイスの最新のwhen値が一致する2つ(またはそれ以上)の行がある場合に、デバイスに対して複数の行を返す可能性があることです。(基本的に、このアプローチは、一意の保証がある場合に「正しい」結果セットを返すことが保証されていlog(device_id,when)ます。

このクエリをインラインビューとして使用して、次の場合に「最新」の値を取得します。

SELECT l.device_id
     , MAX(l.when)
  FROM log l
 GROUP BY l.device_id 

これをログテーブルとデバイステーブルに結合できます。

SELECT d.id
     , d.name
     , m.messsage
  FROM device d
  LEFT
  JOIN (
         SELECT l.device_id
              , MAX(l.when) AS `when`
           FROM log l
          GROUP BY l.device_id 
       ) k
    ON k.device_id = d.id
  LEFT
  JOIN log m 
    ON m.device_id = d.id
       AND m.device_id = k.device_id
       AND m.when = k.when
 ORDER BY d.id 

これらはすべて代替戦略です(これはあなたが尋ねた質問だと思います)が、どちらもあなたの特定のニーズに適しているかどうかはわかりません。(ただし、必要に応じて使用するために、ツールベルトにいくつかの異なるツールを含めることは常に良いことです。)

于 2012-07-30T21:39:18.623 に答える