9

MySql には約 3,000 万レコードのテーブルがあります。以下はテーブル構造です。

CREATE TABLE `campaign_logs` (
  `domain` varchar(50) DEFAULT NULL,
  `campaign_id` varchar(50) DEFAULT NULL,
  `subscriber_id` varchar(50) DEFAULT NULL,
  `message` varchar(21000) DEFAULT NULL,
  `log_time` datetime DEFAULT NULL,
  `log_type` varchar(50) DEFAULT NULL,
  `level` varchar(50) DEFAULT NULL,
  `campaign_name` varchar(500) DEFAULT NULL,
  KEY `subscriber_id_index` (`subscriber_id`),
  KEY `log_type_index` (`log_type`),
  KEY `log_time_index` (`log_time`),
  KEY `campid_domain_logtype_logtime_subid_index` (`campaign_id`,`domain`,`log_type`,`log_time`,`subscriber_id`),
  KEY `domain_logtype_logtime_index` (`domain`,`log_type`,`log_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |

以下は私の質問です

IN 操作を使用する代わりに UNION ALL を実行しています

SELECT log_type,
       DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
       count(DISTINCT subscriber_id) AS COUNT,
       COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_type = 'EMAIL_OPENED'
  AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date

UNION ALL

SELECT log_type,
       DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
       COUNT(DISTINCT subscriber_id) AS COUNT,
            COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_type = 'EMAIL_SENT'
  AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date

UNION ALL

SELECT log_type,
       DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
       COUNT(DISTINCT subscriber_id) AS COUNT,
            COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_type = 'EMAIL_CLICKED'
  AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date,

以下は私の説明文です

+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
| id | select_type  | table         | type  | possible_keys                             | key                                       | key_len | ref  | rows   | Extra                                    |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
|  1 | PRIMARY      | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL |  55074 | Using where; Using index; Using filesort |
|  2 | UNION        | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL | 330578 | Using where; Using index; Using filesort |
|  3 | UNION        | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL |   1589 | Using where; Using index; Using filesort |
| NULL | UNION RESULT | <union1,2,3>  | ALL   | NULL                                      | NULL                                      | NULL    | NULL |   NULL |                                          |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
  1. COUNT(subscriber_id) を COUNT(*) に変更しましたが、パフォーマンスの向上は見られませんでした。

2. クエリから COUNT(DISTINCT subscriber_id) を削除したところ、パフォーマンスが大幅に向上しました。以前は 50 秒から 1 分かかっていた結果が約 1.5 秒で得られます。しかし、クエリからのsubscriber_idの個別のカウントが必要です

以下は、クエリから COUNT(DISTINCT subscriber_id) を削除したときの説明です

+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
| id | select_type  | table         | type  | possible_keys                             | key                                       | key_len | ref  | rows   | Extra                                                     |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
|  1 | PRIMARY      | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL |  55074 | Using where; Using index; Using temporary; Using filesort |
|  2 | UNION        | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL | 330578 | Using where; Using index; Using temporary; Using filesort |
|  3 | UNION        | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL |   1589 | Using where; Using index; Using temporary; Using filesort |
| NULL | UNION RESULT | <union1,2,3>  | ALL   | NULL                                      | NULL                                      | NULL    | NULL |   NULL |                                                           |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
  1. UNION ALL を削除して、3 つのクエリを個別に実行しました。1 つのクエリは 32​​ 秒かかり、他のクエリはそれぞれ 1.5 秒かかりますが、最初のクエリは約 350K レコードを処理し、他のクエリは 2k 行しか処理しません

除外することでパフォーマンスの問題を解決できCOUNT(DISTINCT...)ますが、それらの値が必要です。値を取得するためにクエリをリファクタリングしたり、インデックスなどを追加したりする方法はありますCOUNT(DISTINCT...)が、はるかに高速ですか?

UPDATE 次の情報は、上記の表のデータ分布に関するものです

1 ドメイン 1 キャンペーン 20 log_types 1k-200k サブスクライバー

私が実行している上記のクエリは、18 万人以上のサブスクライバーを持つドメインです。

4

6 に答える 6

5

を使用しないクエリのcount(distinct)方がはるかに高速である場合は、ネストされた集計を行うことができます。

SELECT log_type, log_date,
       count(*) AS COUNT, sum(cnt) AS total
FROM (SELECT log_type,
             DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
             subscriber_id, count(*) as cnt
      FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
      WHERE DOMAIN = 'xxx' AND
            campaign_id = '123' AND
            log_type IN ('EMAIL_SENT', 'EMAIL_OPENED', 'EMAIL_CLICKED') AND
            log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND 
                             CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
      GROUP BY log_type, log_date, subscriber_id
     ) l
GROUP BY logtype, log_date;

運が良ければ、50 秒ではなく 2 ~ 3 秒かかります。ただし、完全なパフォーマンスを得るには、これをサブクエリに分割する必要がある場合があります。そのため、パフォーマンスが大幅に向上しない場合は、inバックを=いずれかのタイプに変更してください。それが機能する場合は、union allが必要になる場合があります。

編集:

別の試みは、変数を使用して、の前に値を列挙することですgroup by

SELECT log_type, log_date, count(*) as cnt,
       SUM(rn = 1) as sub_cnt
FROM (SELECT log_type,
             DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
             subscriber_id,
             (@rn := if(@clt = concat_ws(':', campaign_id, log_type, log_time), @rn + 1,
                        if(@clt := concat_ws(':', campaign_id, log_type, log_time), 1, 1)
                       )
              ) as rn
      FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index) CROSS JOIN
           (SELECT @rn := 0)
      WHERE DOMAIN = 'xxx' AND
            campaign_id = '123' AND
            log_type IN ('EMAIL_SENT', 'EMAIL_OPENED', 'EMAIL_CLICKED') AND
            log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00', '+00:00', '+05:30') AND 
                             CONVERT_TZ('2015-03-01 23:59:58', '+00:00', '+05:30')
      ORDER BY log_type, log_date, subscriber_id
     ) t
GROUP BY log_type, log_date;

これにはまだ別の種類のデータが必要ですが、役立つかもしれません。

于 2015-03-21T01:40:08.303 に答える
3

あなたの質問に答えるには:

クエリをリファクタリングしたり、インデックスなどを追加して COUNT(DISTINCT...) 値を取得する方法はありますが、はるかに高速ですか?

はい、計算フィールドでグループ化しないでください (関数の結果でグループ化しないでください)。代わりに、事前に計算して永続列に保存し、この永続列をインデックスに含めます。

以下を実行して、パフォーマンスが大幅に変化するかどうかを確認します。

1) クエリを簡素化し、1 つの部分に焦点を当てます。3 つの中で最も長いものを 1 つだけ残して、チューニング期間SELECTのために取り除きます。UNION最長SELECTが最適化されたら、さらに追加して、完全なクエリがどのように機能するかを確認します。

2) 関数の結果によるグループ化では、エンジンはインデックスを効率的に使用できません。この関数の結果を使用して、テーブルに別の列を追加します (最初は一時的に、アイデアを確認するためだけに)。私が見る限り、あなたは1時間ごとにグループ化したいので、列を追加して、最も近い時間log_time_hour datetimeに丸め/切り捨てに設定します(日付コンポーネントを保持します)。log_time

新しい列を使用してインデックスを追加: (domain, campaign_id, log_type, log_time_hour, subscriber_id). インデックス内の最初の 3 つの列の順序は重要ではありません (範囲ではなく、クエリ内の定数と等値比較を使用するため) が、クエリ内と同じ順序にします。または、より良いのは、それらをインデックス定義とクエリで選択性の順に作成することです。100,000キャンペーン、1000ドメイン、および3ログの種類がある場合は、次の順序で並べてください: campaign_id, domain, log_type. 大した問題ではありませんが、確認する価値があります。インデックス定義の 4 番目で、最後log_time_hourに来る必要があります。subscriber_id

WHEREクエリでは、と で新しい列を使用しますGROUP BY。必要なすべての列がGROUP BY: bothlog_typeとに含まれていることを確認してくださいlog_time_hour

COUNTとの両方が必要COUNT(DISTINCT)ですか? 最初だけ残しCOUNTて、性能を測定します。放置COUNT(DISTINCT)して性能測定。両方を残して、パフォーマンスを測定します。それらがどのように比較されるかを見てください。

SELECT log_type,
       log_time_hour,
       count(DISTINCT subscriber_id) AS distinct_total,
       COUNT(subscriber_id) AS total
FROM stats.campaign_logs
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_type = 'EMAIL_OPENED'
  AND log_time_hour >= '2015-02-01 00:00:00' 
  AND log_time_hour <  '2015-03-02 00:00:00'
GROUP BY log_type, log_time_hour
于 2015-03-19T05:39:10.460 に答える
1
SELECT log_type,
       DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
       count(DISTINCT subscriber_id) AS COUNT,
       COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_type, log_date

必要に応じて追加AND log_type IN ('EMAIL_OPENED', 'EMAIL_SENT', 'EMAIL_CLICKED')します。

于 2015-03-16T12:28:20.267 に答える
1
  1. subscriber_id個別のサブスクライバーをカウントする前に、キー (log_date) の外側の計算フィールドでグループ化しているため、キーでは役に立ちません。MySQL はキーを使用せずに重複したサブスクライバーをソートおよびフィルター処理する必要があるため、これが非常に遅い理由を説明しています。

  2. log_time 条件にエラーがある可能性があります。select の逆のタイムゾーン変換 (つまり'+05:30','+00:00') が必要ですが、クエリ時間に大きな影響はありません。

  3. log_type IN (...)a and group byを実行することで、「すべてを結合」を回避できます。log_type, log_date

最も効果的な解決策は、データベース スキーマにミッドアワー フィールドを追加し、そこに 1 日の 48 のミッドアワーのうちの 1 つを設定することです (そしてミッドアワーのタイムゾーンに注意してください)。campaign_idしたがって、domainlog_type、 、 でlog_mid_hourインデックスを使用できる可能性があります。subscriber_id

これはかなり冗長になりますが、速度は向上します。

したがって、これにより、テーブルでいくつかの初期化が行われるはずです: 注意してください: これを本番テーブルでテストしないでください

ALTER TABLE campaign_logs
   ADD COLUMN log_mid_hour TINYINT AFTER log_time;

UPDATE campaign_logs SET log_mid_hour=2*HOUR(log_time)+IF(MINUTE(log_time)>29,1,0);

ALTER TABLE campaign_logs
ADD INDEX(`campaign_id`,`domain`,`log_time`,`log_type`,`log_mid_hour`,`subscriber_id`);

また、将来の記録のために、スクリプトで log_mid_hour を設定する必要があります。

クエリは次のようになります(11 時半のタイムシフトの場合) :

SELECT log_type,
   MOD(log_mid_hour+11, 48) tz_log_mid_hour,
   COUNT(DISTINCT subscriber_id) AS COUNT,
   COUNT(subscriber_id) AS total
FROM stats.campaign_logs
WHERE DOMAIN='xxx'
   AND campaign_id='123'
   AND log_type IN('EMAIL_SENT', 'EMAIL_OPENED','EMAIL_CLICKED')
   AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+05:30','+00:00')   
   AND CONVERT_TZ('2015-03-01 23:59:58','+05:30','+00:00')
GROUP BY log_type, log_mid_hour;

これにより、インデックスを最大限に活用して、各ミッドアワーのカウントが得られます。

于 2015-03-20T21:00:17.670 に答える
1

使用しているインデックスの他の順序を試し、subscriber_id を移動して、その効果を確認します。カーディナリティの高い列を上に移動すると、より良い結果が得られる可能性があります。

最初は、インデックスの一部しか使用していない可能性があると思いました (subscriber_id にはまったくアクセスしていません)。Subscriber_id を使用できない場合、インデックス ツリーを上に移動すると実行速度が遅くなりますが、少なくとも使用できないことがわかります。

他に遊べるものが思い浮かびません。

于 2015-03-19T05:02:31.720 に答える
0

私は非常によく似た問題を抱えていて、ここSOに投稿され、大きな助けになりました。スレッドは次のとおりです。インデックスをカバーしているにもかかわらず、MySQL MyISAM slow count() query

一言で言えば、私の問題はクエリやインデックスとは関係なく、すべてテーブルと MySQL の設定方法に関係していることがわかりました。次の場合、まったく同じクエリがはるかに高速になりました。

  1. InnoDB に切り替えました (既に使用しています)。
  2. CHARSET を ASCII に切り替えました。utf8 が必要ない場合は、3 倍のスペース (および検索時間) が必要になります。
  3. 各列のサイズをできるだけ小さくし、可能であれば null にしないでください。
  4. MySQL の InnoDB バッファ プール サイズを増やしました。これが専用マシンである場合、多くの推奨事項は、RAM の 70% に増やすことです。
  5. テーブルをカバリング インデックスで並べ替え、SELECT INTO OUTFILE で書き出し、新しいテーブルに再挿入しました。これにより、すべてのレコードが検索順に物理的に並べ替えられます。

これらの変更のどれが問題を解決したかはわかりません (私は非科学的で、一度に 1 つずつ試していなかったため) が、クエリが 50 ~ 100 倍速くなりました。YMMV。

于 2015-03-26T02:31:22.697 に答える