web-dev-qa-db-ja.com

このMySQLクエリを最適化する方法は?数百万行

次のクエリがあります。

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
FROM analytics
LEFT JOIN transactions ON analytics.id = transactions.analytics
WHERE analytics.user_id = 52094 
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10 

分析テーブルには60Mの行があり、トランザクションテーブルには3Mの行があります。

このクエリでEXPLAINを実行すると、次のようになります。

+------+--------------+-----------------+--------+---------------------+-------------------+----------------------+---------------------------+----------+-----------+-------------------------------------------------+
| # id |  select_type |      table      |  type  |    possible_keys    |        key        |        key_len       |            ref            |   rows   |   Extra   |                                                 |
+------+--------------+-----------------+--------+---------------------+-------------------+----------------------+---------------------------+----------+-----------+-------------------------------------------------+
| '1'  |  'SIMPLE'    |  'analytics'    |  'ref' |  'analytics_user_id | analytics_source' |  'analytics_user_id' |  '5'                      |  'const' |  '337662' |  'Using where; Using temporary; Using filesort' |
| '1'  |  'SIMPLE'    |  'transactions' |  'ref' |  'tran_analytics'   |  'tran_analytics' |  '5'                 |  'dijishop2.analytics.id' |  '1'     |  NULL     |                                                 |
+------+--------------+-----------------+--------+---------------------+-------------------+----------------------+---------------------------+----------+-----------+-------------------------------------------------+

このクエリは既に非常に基本的なものであるため、このクエリを最適化する方法を理解できません。このクエリの実行には約70秒かかります。

存在するインデックスは次のとおりです。

+-------------+-------------+----------------------------+---------------+------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
|   # Table   |  Non_unique |          Key_name          |  Seq_in_index |    Column_name   |  Collation |  Cardinality |  Sub_part |  Packed |  Null  |  Index_type |  Comment |  Index_comment |
+-------------+-------------+----------------------------+---------------+------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
| 'analytics' |  '0'        |  'PRIMARY'                 |  '1'          |  'id'            |  'A'       |  '56934235'  |  NULL     |  NULL   |  ''    |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_user_id'       |  '1'          |  'user_id'       |  'A'       |  '130583'    |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_product_id'    |  '1'          |  'product_id'    |  'A'       |  '490812'    |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_affil_user_id' |  '1'          |  'affil_user_id' |  'A'       |  '55222'     |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_source'        |  '1'          |  'source'        |  'A'       |  '24604'     |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_country_name'  |  '1'          |  'country_name'  |  'A'       |  '39510'     |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_gordon'        |  '1'          |  'id'            |  'A'       |  '56934235'  |  NULL     |  NULL   |  ''    |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_gordon'        |  '2'          |  'user_id'       |  'A'       |  '56934235'  |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'analytics' |  '1'        |  'analytics_gordon'        |  '3'          |  'source'        |  'A'       |  '56934235'  |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
+-------------+-------------+----------------------------+---------------+------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+


+----------------+-------------+-------------------+---------------+-------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
|    # Table     |  Non_unique |      Key_name     |  Seq_in_index |    Column_name    |  Collation |  Cardinality |  Sub_part |  Packed |  Null  |  Index_type |  Comment |  Index_comment |
+----------------+-------------+-------------------+---------------+-------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
| 'transactions' |  '0'        |  'PRIMARY'        |  '1'          |  'id'             |  'A'       |  '2436151'   |  NULL     |  NULL   |  ''    |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'tran_user_id'   |  '1'          |  'user_id'        |  'A'       |  '56654'     |  NULL     |  NULL   |  ''    |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'transaction_id' |  '1'          |  'transaction_id' |  'A'       |  '2436151'   |  '191'    |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'tran_analytics' |  '1'          |  'analytics'      |  'A'       |  '2436151'   |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'tran_status'    |  '1'          |  'status'         |  'A'       |  '22'        |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'gordon_trans'   |  '1'          |  'status'         |  'A'       |  '22'        |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
| 'transactions' |  '1'        |  'gordon_trans'   |  '2'          |  'analytics'      |  'A'       |  '2436151'   |  NULL     |  NULL   |  'YES' |  'BTREE'    |  ''      |  ''            |
+----------------+-------------+-------------------+---------------+-------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+

状況を改善しなかったため、提案されたように追加のインデックスを追加する前に2つのテーブルのスキーマを簡略化しました。

CREATE TABLE `analytics` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `affil_user_id` int(11) DEFAULT NULL,
  `product_id` int(11) DEFAULT NULL,
  `medium` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `source` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `terms` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `is_browser` tinyint(1) DEFAULT NULL,
  `is_mobile` tinyint(1) DEFAULT NULL,
  `is_robot` tinyint(1) DEFAULT NULL,
  `browser` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `mobile` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `robot` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `platform` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `referrer` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `domain` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `ip` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `continent_code` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `country_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `city` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `analytics_user_id` (`user_id`),
  KEY `analytics_product_id` (`product_id`),
  KEY `analytics_affil_user_id` (`affil_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=64821325 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `transactions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `transaction_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `user_id` int(11) NOT NULL,
  `pay_key` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `sender_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `amount` decimal(10,2) DEFAULT NULL,
  `currency` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `status` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `analytics` int(11) DEFAULT NULL,
  `ip_address` varchar(46) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `session_id` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `eu_vat_applied` int(1) DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `tran_user_id` (`user_id`),
  KEY `transaction_id` (`transaction_id`(191)),
  KEY `tran_analytics` (`analytics`),
  KEY `tran_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=10019356 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

上記の場合、これ以上最適化できません。サマリーテーブルに関する実装のアドバイスはすばらしいでしょう。 AWSではLAMPスタックを使用しています。上記のクエリはRDS(m1.large)で実行されています。

30
Abs

次のインデックス(bツリーインデックス)を作成します。

analytics(user_id, source, id) 
transactions(analytics, status)

これはゴードンの提案とは異なります。

インデックス内の列の順序は重要です。

特定のanalytics.user_idでフィルタリングするため、このフィールドはインデックスの最初にする必要があります。次に、analytics.sourceでグループ化します。 sourceによるソートを回避するには、これをインデックスの次のフィールドにする必要があります。 analytics.idも参照するため、このフィールドをインデックスの一部として使用し、最後に置くことをお勧めします。 MySQLはインデックスのみを読み取ることができ、テーブルを操作することはできませんか?わかりませんが、テストはかなり簡単です。

transactionsで使用されるため、analyticsのインデックスはJOINで始まる必要があります。 statusも必要です。

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
FROM analytics
LEFT JOIN transactions ON analytics.id = transactions.analytics
WHERE analytics.user_id = 52094 
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10 
11

最初にいくつかの分析...

_SELECT  a.source AS referrer,
        COUNT(*) AS frequency,  -- See question below
        SUM(t.status = 'COMPLETED') AS sales
    FROM  analytics AS a
    LEFT JOIN  transactions AS t  ON a.id = t.analytics AS a
    WHERE  a.user_id = 52094
    GROUP BY  a.source
    ORDER BY  frequency DESC
    LIMIT  10 
_

aからtへのマッピングが「1対多」である場合、COUNTSUMが正しい値であるかどうかを検討する必要がありますまたは膨らんだ値。クエリの現状では、これらは「膨張」しています。 JOINは集計の前に発生するため、トランザクションの数と完了したトランザクションの数を数えています。それが望ましいと思います。

注:通常のパターンはCOUNT(*);です。 COUNT(x)と言うことは、xNULLであることを確認することを意味します。チェックは必要ないと思いますか?

このインデックスはWHEREを処理し、「カバー」しています。

_ analytics:  INDEX(user_id, source, id)   -- user_id first

 transactions:  INDEX(analytics, status)  -- in this order
_

_GROUP BY_は「ソート」を必要とする場合と必要としない場合があります。 _ORDER BY_は_GROUP BY_とは異なり、間違いなくソートが必要になります。そして、グループ化された行のセット全体をソートする必要があります。 LIMITへのショートカットはありません。

通常、サマリー表は日付指向です。つまり、_PRIMARY KEY_には「日付」とその他のディメンションが含まれます。おそらく、日付とuser_idによるキー入力は理にかなっていますか?平均的なユーザーの1日あたりのトランザクション数はいくつですか? 10以上の場合は、サマリーテーブルを検討してみましょう。また、UPDATEingまたはDELETEingの古いレコードにならないことが重要です。 もっと

たぶん

_user_id ...,
source ...,
dy DATE ...,
status ...,
freq      MEDIUMINT UNSIGNED NOT NULL,
status_ct MEDIUMINT UNSIGNED NOT NULL,
PRIMARY KEY(user_id, status, source, dy)
_

次に、クエリは

_SELECT  source AS referrer,
        SUM(freq) AS frequency,
        SUM(status_ct) AS completed_sales
    FROM  Summary
    WHERE  user_id = 52094
      AND  status = 'COMPLETED'
    GROUP BY source
    ORDER BY  frequency DESC
    LIMIT  10 
_

速度は多くの要因からきています

  • 小さい表(見る行が少ない)
  • いいえJOIN
  • より有用なインデックス

(まだ追加のソートが必要です。)

要約表がなくても、速度が向上する可能性があります...

  • テーブルの大きさは? `innodb_buffer_pool_sizeはどのくらいの大きさですか?
  • Normalizing一部の文字列がかさばって反復的であると、そのテーブルがI/Oバインドされなくなる可能性があります。
  • これはひどいです:KEY (transaction_id(191));それを修正する5つの方法については、 ここ を参照してください。
  • IPアドレスには255バイトも、_utf8mb4_unicode_ci_も必要ありません。 (39)とasciiで十分です。
7
Rick James

このクエリの場合:

_SELECT a.source AS referrer, 
       COUNT(*) AS frequency, 
       SUM( t.status = 'COMPLETED' ) AS sales
FROM analytics a LEFT JOIN
     transactions t
     ON a.id = t.analytics
WHERE a.user_id = 52094 
GROUP BY a.source 
ORDER BY frequency DESC 
LIMIT 10 ;
_

analytics(user_id, id, source)およびtransactions(analytics, status)のインデックスが必要です。

6
Gordon Linoff

以下をお試しいただき、問題が解決するかどうかをお知らせください。

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
FROM (SELECT * FROM analytics where user_id = 52094) analytics
LEFT JOIN (SELECT analytics, status from transactions where analytics = 52094) transactions ON analytics.id = transactions.analytics
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10
4
Vincent Rye

このクエリは、数百万のanalyticsレコードをtransactionsレコードと結合する可能性があり、数百万のレコードの合計(ステータスチェックを含む)を計算します。最初にLIMIT 10を適用してから結合を行い、合計を計算できれば、クエリを高速化できます。残念ながら、結合にはanalytics.idが必要です。これはGROUP BYを適用すると失われます。しかし、多分analytics.sourceはとにかくクエリをブーストするのに十分な選択性があります。

したがって、私の考えは、頻度を計算し、それらによって制限し、サブクエリでanalytics.sourcefrequencyを返し、この結果を使用してメインクエリでanalyticsをフィルタリングすることです。これにより、残りの結合と計算がレコードの数が減ると期待されます。

最小限のサブクエリ(注:結合なし、合計なし、10レコードを返します):

SELECT
    source,
    COUNT(id) AS frequency
FROM analytics
WHERE user_id = 52094
GROUP BY source
ORDER BY frequency DESC 
LIMIT 10

上記のクエリをサブクエリxとして使用した完全なクエリ:

SELECT
    x.source AS referrer,
    x.frequency,
    SUM(IF(t.status = 'COMPLETED', 1, 0)) AS sales
FROM
    (<subquery here>) x
    INNER JOIN analytics a
       ON x.source = a.source  -- This reduces the number of records
    LEFT JOIN transactions t
       ON a.id = t.analytics
WHERE a.user_id = 52094      -- We could have several users per source
GROUP BY x.source, x.frequency
ORDER BY x.frequency DESC

これによって期待されるパフォーマンスが向上しない場合は、MySQLが予期しない順序で結合を適用していることが原因である可能性があります。ここで説明したように "MySQLの実行順序を強制する方法はありますか?" この場合、結合をSTRAIGHT_JOINで置き換えることができます。

以下のアプローチを試していただけませんか:

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(sales) AS sales
FROM analytics
LEFT JOIN(
        SELECT transactions.Analytics, (CASE WHEN transactions.status = 'COMPLETED' THEN 1 ELSE 0 END) AS sales
        FROM analytics INNER JOIN transactions ON analytics.id = transactions.analytics
) Tra
ON analytics.id = Tra.analytics
WHERE analytics.user_id = 52094 
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10 
3
Singh Kailash

これを試して

SELECT 
    a.source AS referrer, 
    COUNT(a.id) AS frequency, 
    SUM(t.sales) AS sales
FROM (Select id, source From analytics Where user_id = 52094) a
LEFT JOIN (Select analytics, case when status = 'COMPLETED' Then 1 else 0 end as sales 
           From transactions) t ON a.id = t.analytics
GROUP BY a.source 
ORDER BY frequency DESC 
LIMIT 10 

「大規模なテーブルです」とおっしゃっていたので、これを提案しますが、このSQLはごく少数の列のみを使用します。この場合、必要な列のみを含むインラインビューを使用すると、

注:ここでもメモリが重要な役割を果たします。インラインビューを決定する前に、メモリを確認してください

2
Gaj

2つのテーブルからクエリを分離しようと思います。上位10個のsourcesだけが必要なので、最初にそれらを取得してから、transactionssales列からクエリを実行します。

SELECT  source as referrer
        ,frequency
        ,(select count(*) 
          from   transactions t  
          where  t.analytics in (select distinct id 
                                 from   analytics 
                                 where  user_id = 52094
                                        and source = by_frequency.source) 
                 and status = 'completed'
         ) as sales
from    (SELECT analytics.source
                ,count(*) as frequency
        from    analytics 
        where   analytics.user_id = 52094
        group by analytics.source
        order by frequency desc
        limit 10
        ) by_frequency

distinctがない場合も高速になる可能性があります

2

述語user_id = 52094は説明のためであり、アプリケーションでは、選択されたuser_idは変数であると想定しています。

ここでは、ACIDプロパティはそれほど重要ではないと思います。

(1)したがって、ユーティリティテーブルを使用して、必要なフィールドのみを持つ2つのレプリカテーブル(Vladimirが上で提案したインデックスと同様)を維持します。

CREATE TABLE mv_anal (
  `id` int(11) NOT NULL,
  `user_id` int(11) DEFAULT NULL,
  `source` varchar(45),
  PRIMARY KEY (`id`)
);

CREATE TABLE mv_trans (
  `id` int(11) NOT NULL,
  `status` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `analytics` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

CREATE TABLE util (
  last_updated_anal int (11) NOT NULL,
  last_updated_trans int (11) NOT NULL
);

INSERT INTO util (0, 0);

ここでの利点は、元のテーブルの比較的小さな予測を読み取ることです。うまくいけば、OSレベルとDBレベルのキャッシュは機能し、低速のセカンダリストレージからではなく高速のRAMから読み取られます。 これは非常に大きな利益になる可能性があります。

これが2つのテーブルを更新する方法です(以下はcronによって実行されるトランザクションです)。

-- TRANSACTION STARTS -- 

INSERT INTO mv_trans 
SELECT id, IF (status = 'COMPLETE', 1, 0) AS status, analysis 
FROM transactions JOIN util
ON util.last_updated_trans <= transactions.id

UPDATE util
SET last_updated_trans = sub.m
FROM (SELECT MAX (id) AS m FROM mv_trans) sub;

-- TRANSACTION COMMITS -- 

-- similar transaction for mv_anal.

(2)次に、選択性に取り組み、順次スキャン時間を短縮します。 mv_analのuser_id、source、id(このシーケンス)にbツリーインデックスを作成する必要があります。

注:上記は、アナリティクステーブルにインデックスを作成するだけで実現できますが、そのようなインデックスを作成するには、60M行の大きなテーブルを読み取る必要があります。私の方法では、非常に薄いテーブルのみを読み取るためにインデックスの構築が必要です。したがって、btreeをより頻繁に再構築できます(テーブルが追加専用であるため、スキューの問題に対処します)。

これは、私が確認する方法ですクエリ時に高い選択性が達成されますで、btreeの問題を歪めます。

(3)PostgreSQLでは、WITHサブクエリは常にマテリアライズされます。 MySQLについても同様に願っています。したがって、最適化の最後のマイルとして:

WITH sub_anal AS (
  SELECT user_id, source AS referrer, COUNT (id) AS frequency
  FROM mv_anal
  WHERE user_id = 52094
  GROUP BY user_id, source
  ORDER BY COUNT (id) DESC
  LIMIT 10
)
SELECT sa.referrer, sa.frequency, SUM (status) AS sales
FROM sub_anal AS sa 
JOIN mv_anal anal 
ON sa.referrer = anal.source AND sa.user_id = anal.user_id
JOIN mv_trans AS trans
ON anal.id = trans.analytics
2
Edward Aung

私はサブクエリを試してみます:

SELECT a.source AS referrer, 
       COUNT(*) AS frequency,
       SUM((SELECT COUNT(*) FROM transactions t 
        WHERE a.id = t.analytics AND t.status = 'COMPLETED')) AS sales
FROM analytics a
WHERE a.user_id = 52094 
GROUP BY a.source
ORDER BY frequency DESC 
LIMIT 10; 

さらに、@ Gordonの回答とまったく同じようにインデックスを作成します:analytics(user_id、id、source)およびトランザクション(analytics、status)。

2
Lukasz Szozda

あなたのクエリで私が見つける唯一の問題は

GROUP BY analytics.source 
ORDER BY frequency DESC 

このクエリのため、一時テーブルを使用してファイルソートを行っています。

これを回避する1つの方法は、次のような別のテーブルを作成することです。

CREATE TABLE `analytics_aggr` (
  `source` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `frequency` int(10) DEFAULT NULL,
  `sales` int(10) DEFAULT NULL,
  KEY `sales` (`sales`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`

以下のクエリを使用してanalytics_aggrにデータを挿入します

insert into analytics_aggr SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
    FROM analytics
    LEFT JOIN transactions ON analytics.id = transactions.analytics
    WHERE analytics.user_id = 52094 
    GROUP BY analytics.source 
    ORDER BY null 

これで、簡単にデータを取得できます

select * from analytics_aggr order by sales desc
2
surya singh

パーティーに遅れました。 MySQLのキャッシュに1つのインデックスをロードする必要があると思います。 NLJはおそらくパフォーマンスを低下させています。ここに私がそれを見る方法があります:

パス

クエリは簡単です。 2つのテーブルがあり、「パス」は非常に明確です。

  • オプティマイザは、最初にanalyticsテーブルを読み取ることを計画する必要があります。
  • オプティマイザーはtransactionsテーブルを2番目に読み取ることを計画する必要があります。これは、LEFT OUTER JOINを使用しているためです。これについてはあまり議論しません。
  • さらに、analyticsテーブルは6,000万行であり、最適なパスはこの行で行をできるだけ早くフィルタリングする必要があります。

アクセス

パスが明確になったら、インデックスアクセスとテーブルアクセスのどちらを使用するかを決定する必要があります。どちらにも長所と短所があります。ただし、SELECTのパフォーマンスを向上させる必要があります。

  • Index Accessを選択する必要があります。
  • ハイブリッドアクセスを避けます。したがって、テーブルアクセス(フェッチ)は絶対に避けてください。翻訳:関係するすべての列をインデックスに配置します。

フィルタリング

繰り返しますが、SELECTの高いパフォーマンスが必要です。したがって:

  • フィルタリングは、テーブルレベルではなく、インデックスレベルで実行する必要があります。

行の集計

フィルタリング後、次のステップはGROUP BY analytics.sourceで行を集計することです。これは、source列をインデックスの最初の列として配置することで改善できます。

パス、アクセス、フィルタリング、および集約に最適なインデックス

上記のすべてを考慮して、言及されたすべての列をインデックスに含める必要があります。次のインデックスは、応答時間を改善するはずです。

create index ix1_analytics on analytics (user_id, source, id);

create index ix2_transactions on transactions (analytics, status);

これらのインデックスは、上記の「パス」、「アクセス」、および「フィルタリング」戦略を満たします。

インデックスキャッシュ

最後に-これは重要です-セカンダリインデックスをMySQLのメモリキャッシュにロードします。 MySQLはNLJ(Nested Loop Join)(MySQL用語では「ref」)を実行しており、2番目のアクセスにはランダムに約200k回アクセスする必要があります。

残念ながら、MySQLのキャッシュにインデックスをロードする方法がわかりません。 FORCEの使用は、次のように機能します。

SELECT 
    analytics.source AS referrer, 
    COUNT(analytics.id) AS frequency, 
    SUM(IF(transactions.status = 'COMPLETED', 1, 0)) AS sales
FROM analytics
LEFT JOIN transactions FORCE index (ix2_transactions)
  ON analytics.id = transactions.analytics
WHERE analytics.user_id = 52094 
GROUP BY analytics.source 
ORDER BY frequency DESC 
LIMIT 10

十分なキャッシュ領域があることを確認してください。これを理解するための短い質問/答えは次のとおりです: mysqlインデックスが完全にメモリに収まるかどうかを理解する方法

幸運を!ああ、結果を投稿してください。

1
The Impaler

この質問は間違いなく多くの注目を集めているので、すべての明白な解決策が試されたと私は確信しています。ただし、クエリでLEFT JOINに対応するものは見つかりませんでした。

LEFT JOINステートメントは通常、クエリプランナーにハッシュ結合を強制します。ハッシュ結合は、少数の結果では高速ですが、多数の結果では非常に遅くなります。 @Rick Jamesの回答で述べたように、元のクエリの結合はIDフィールドanalytics.idにあるため、これは多数の結果を生成します。ハッシュ結合はひどいパフォーマンス結果をもたらします。以下の提案は、スキーマや処理を変更することなく、これに対処します。

集計はanalytics.sourceによって行われるため、ソース別の頻度とソース別の売上に個別の集計を作成し、集計が完了するまで左結合を延期するクエリを試します。これにより、インデックスを最適に使用できます(通常、これは大きなデータセットのマージ結合です)。

これが私の提案です:

SELECT t1.source AS referrer, t1.frequency, t2.sales
FROM (
  -- Frequency by source
  SELECT a.source, COUNT(a.id) AS frequency
  FROM analytics a
  WHERE a.user_id=52094
  GROUP BY a.source
) t1
LEFT JOIN (
  -- Sales by source
  SELECT a.source,
    SUM(IF(t.status = 'COMPLETED', 1, 0)) AS sales
  FROM analytics a
  JOIN transactions t
  WHERE a.id = t.analytics
    AND t.status = 'COMPLETED'
    AND a.user_id=52094
  GROUP by a.source
) t2
  ON t1.source = t2.source
ORDER BY frequency DESC 
LIMIT 10 

お役に立てれば。

1
saarp