web-dev-qa-db-fra.com

Comment optimiser cette requête MySQL? Des millions de lignes

J'ai la requête suivante:

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 

La table d'analyse a 60 millions de lignes et la table des transactions a 3 lignes.

Lorsque j'exécute un EXPLAIN sur cette requête, j'obtiens:

+------+--------------+-----------------+--------+---------------------+-------------------+----------------------+---------------------------+----------+-----------+-------------------------------------------------+
| # 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     |                                                 |
+------+--------------+-----------------+--------+---------------------+-------------------+----------------------+---------------------------+----------+-----------+-------------------------------------------------+

Je ne peux pas comprendre comment optimiser cette requête car elle est déjà très basique. Il faut environ 70 secondes pour exécuter cette requête.

Voici les index qui existent:

+-------------+-------------+----------------------------+---------------+------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+
|   # 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'    |  ''      |  ''            |
+----------------+-------------+-------------------+---------------+-------------------+------------+--------------+-----------+---------+--------+-------------+----------+----------------+

Schéma simplifié pour les deux tables avant d'ajouter des index supplémentaires comme suggéré car cela n'a pas amélioré la situation.

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;

Si ce qui précède ne peut plus être optimisé. Tout conseil de mise en œuvre sur les tableaux récapitulatifs sera excellent. Nous utilisons une pile LAMP sur AWS. La requête ci-dessus s'exécute sur RDS (m1.large).

30
Abs

Je créerais les index suivants (index b-tree):

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

Ceci est différent de la suggestion de Gordon.

L'ordre des colonnes dans l'index est important.

Vous filtrez par analytics.user_id Spécifique, ce champ doit donc être le premier de l'index. Ensuite, vous regroupez par analytics.source. Pour éviter le tri par source, ce doit être le champ suivant de l'index. Vous faites également référence à analytics.id, Il est donc préférable que ce champ fasse partie de l'index, mettez-le en dernier. MySQL est-il capable de lire uniquement l'index et de ne pas toucher la table? Je ne sais pas, mais c'est assez facile à tester.

L'index sur transactions doit commencer par analytics, car il serait utilisé dans le JOIN. Nous avons également besoin de 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
Vladimir Baranov

D'abord une analyse ...

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 

Si le mappage de a à t est "un-à-plusieurs", alors vous devez déterminer si les COUNT et SUM ont les valeurs correctes ou des valeurs gonflées. En l'état, la requête est "gonflée". JOIN se produit avant l'agrégation, vous comptez donc le nombre de transactions et le nombre de transactions terminées. Je suppose que cela est souhaité.

Remarque: Le modèle habituel est COUNT(*); dire COUNT(x) implique de vérifier x pour être NULL. Je soupçonne que le contrôle n'est pas nécessaire?

Cet index gère le WHERE et est "couvrant":

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

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

Le GROUP BY Peut ou non nécessiter un "tri". Le ORDER BY, Étant différent du GROUP BY, Aura certainement besoin d'un tri. Et l'ensemble des lignes groupées devra être trié; il n'y a pas de raccourci pour le LIMIT.

Normalement, les tableaux récapitulatifs sont orientés vers la date. Autrement dit, le PRIMARY KEY Comprend une "date" et d'autres dimensions. Peut-être que la saisie par date et user_id aurait un sens? Combien de transactions par jour l’utilisateur moyen a-t-il? Si au moins 10, considérons un tableau récapitulatif. De plus, il est important de ne pas être UPDATEing ou DELETEing anciens enregistrements. Plus

J'aurais probablement

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

Ensuite, la requête devient

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 

La vitesse vient de nombreux facteurs

  • Tableau plus petit (moins de lignes à regarder)
  • Non JOIN
  • Index plus utile

(Il a encore besoin du tri supplémentaire.)

Même sans le tableau récapitulatif, il peut y avoir des accélérations ...

  • Quelle est la taille des tables? Quelle est la taille de `innodb_buffer_pool_size?
  • Normalizing certaines des chaînes qui sont à la fois volumineuses et répétitives pourraient rendre cette table non liée aux E/S.
  • C'est affreux: KEY (transaction_id(191)); Voir ici pour 5 façons de le réparer.
  • Les adresses IP n'ont pas besoin de 255 octets, ni utf8mb4_unicode_ci. (39) et ascii suffisent.
7
Rick James

Pour cette requête:

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 ;

Vous voulez un index sur analytics(user_id, id, source) et transactions(analytics, status).

6
Gordon Linoff

Essayez ci-dessous et faites-moi savoir si cela aide.

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

Cette requête joint potentiellement des millions d'enregistrements analytics avec des enregistrements transactions et calcule la somme (y compris la vérification d'état) de millions d'enregistrements. Si nous pouvions d'abord appliquer le LIMIT 10 puis faire la jointure et calculer la somme, nous pourrions accélérer la requête. Malheureusement, nous avons besoin du analytics.id pour la jointure, qui est perdue après l'application du GROUP BY. Mais peut-être analytics.source est suffisamment sélectif pour augmenter la requête de toute façon.

Mon idée est donc de calculer les fréquences, limiter par elles, pour renvoyer le analytics.source et frequency dans une sous-requête et pour utiliser ce résultat pour filtrer le analytics dans la requête principale, qui effectue ensuite le reste des jointures et des calculs sur un nombre, espérons-le, très réduit d'enregistrements.

Sous-requête minimale (remarque: pas de jointure, pas de somme, retourne 10 enregistrements):

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

La requête complète utilisant la requête ci-dessus comme sous-requête 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

Si cela ne donne pas le gain de performances attendu, cela peut être dû à l'application par MySQL des jointures dans un ordre inattendu. Comme expliqué ici "Existe-t-il un moyen de forcer l'ordre d'exécution de MySQL?" , vous pouvez remplacer la jointure par STRAIGHT_JOIN dans ce cas.

Pourriez-vous essayer ci-dessous Approche:

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

Essaye ça

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 

Je propose ceci parce que vous avez dit "ils sont une table massive" mais ce sql n'utilise que très peu de colonnes. Dans ce cas, si nous utilisons la vue en ligne avec des colonnes requises uniquement, ce sera bien

Remarque: la mémoire jouera également un rôle important ici. Confirmez donc la mémoire avant de décider de la vue en ligne

2
Gaj

J'essaierais de séparer l'interrogation des deux tables. Puisque vous n'avez besoin que des 10 premiers sources, je les obtiendrais d'abord, puis interrogerais depuis transactions la colonne sales:

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

Il peut également être plus rapide sans distinct

2
Alexandr Kapshuk

Je suppose que le prédicat, user_id = 52094, est à des fins d'illustration et dans l'application, le user_id sélectionné est une variable.

Je suppose également que la propriété ACID n'est pas très importante ici.

(1) Par conséquent, je maintiendrai deux tables de réplique avec uniquement les champs nécessaires (c'est similaire aux indices que Vladimir avait suggéré ci-dessus) en utilisant une table d'utilité.

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);

Le gain ici est que nous lirons des projections relativement plus petites des tables originales - espérons que les caches de niveau OS et DB fonctionnent et qu'ils ne sont pas lus à partir d'un stockage secondaire plus lent mais à partir de RAM plus rapide. Cela peut être un très grand gain.

Voici comment j'ai mis à jour les deux tables (ci-dessous est une transaction exécutée par un 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) Maintenant, je vais aborder la sélectivité pour réduire le temps de balayage séquentiel. Je vais devoir construire un index b-tree sur user_id, source et id (dans cette séquence) sur mv_anal.

Remarque: ce qui précède peut être atteint en créant simplement un index sur la table d'analyse, mais la construction d'un tel index nécessite la lecture d'un grand tableau avec 60 millions de lignes. Ma méthode nécessite que la construction d'index ne lise que les tables très fines. Ainsi, nous pouvons reconstruire le btree plus fréquemment (pour contrer le problème d'asymétrie car la table est en ajout uniquement).

C'est ainsi que je m'assure la haute sélectivité est atteinte lors de l'interrogation et pour contrer le problème de biais de btree.

(3) Dans PostgreSQL, les sous-requêtes WITH sont toujours matérialisées. J'espère de même pour MySQL. Par conséquent, comme le dernier kilomètre d'optimisation:

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

J'essaierais la sous-requête:

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; 

De plus, les index correspondent exactement à la réponse de @ Gordon: analyses (id_utilisateur, id, source) et transactions (analyses, statut).

2
Lukasz Szozda

Le seul problème que je trouve dans votre requête est

GROUP BY analytics.source 
ORDER BY frequency DESC 

à cause de cette requête fait le tri de fichiers en utilisant une table temporaire.

Une façon d'éviter cela est de créer une autre table comme

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;`

insérer des données dans analytics_aggr en utilisant la requête ci-dessous

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 

Maintenant, vous pouvez facilement obtenir vos données en utilisant

select * from analytics_aggr order by sales desc
2
surya singh

Tard à la fête. Je pense que vous devrez charger un index dans le cache de MySQL. La NLJ est probablement en train de tuer les performances. Voici comment je le vois:

Le chemin

Votre requête est simple. Il a deux tables et le "chemin" est très clair:

  • L'optimiseur doit d'abord planifier la lecture de la table analytics.
  • L'optimiseur doit prévoir de lire la table transactions en second. En effet, vous utilisez un LEFT OUTER JOIN. Pas beaucoup de discussion sur celui-ci.
  • De plus, la table analytics compte 60 millions de lignes et le meilleur chemin devrait filtrer les lignes dès que possible sur celle-ci.

L'accès

Une fois le chemin libre, vous devez décider si vous souhaitez utiliser un accès à l'index ou un accès à la table. Les deux ont des avantages et des inconvénients. Cependant, vous souhaitez améliorer les performances de SELECT:

  • Vous devez choisir Index Access.
  • Évitez l'accès hybride. Par conséquent, vous devez éviter à tout prix tout accès à la table (récupération). Traduction: placez toutes les colonnes participantes dans les index.

Le filtrage

Encore une fois, vous voulez des performances élevées pour le SELECT. Donc:

  • Vous devez effectuer le filtrage au niveau de l'index, pas au niveau de la table.

Agrégation de lignes

Après le filtrage, l'étape suivante consiste à agréger les lignes par GROUP BY analytics.source. Cela peut être amélioré en plaçant la colonne source comme première colonne de l'index.

Index optimaux pour le chemin, l'accès, le filtrage et l'agrégation

Compte tenu de tout ce qui précède, vous devez inclure toutes les colonnes mentionnées dans les index. Les index suivants devraient améliorer le temps de réponse:

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

create index ix2_transactions on transactions (analytics, status);

Ces index remplissent les stratégies de "chemin", "d'accès" et de "filtrage" décrites ci-dessus.

Le cache d'index

Enfin - et c'est essentiel - chargez l'index secondaire dans le cache mémoire de MySQL. MySQL exécute un NLJ (Nested Loop Join) - un "ref" dans le jargon MySQL - et doit accéder au second au hasard près de 200 000 fois.

Malheureusement, je ne sais pas comment charger l'index dans le cache de MySQL. L'utilisation de FORCE peut fonctionner, comme dans:

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

Assurez-vous que vous disposez de suffisamment d'espace cache. Voici une courte question/réponse pour comprendre: Comment savoir si l'index mysql tient entièrement en mémoire

Bonne chance! Oh, et affichez les résultats.

1
The Impaler

Cette question a certainement reçu beaucoup d'attention, donc je suis sûr que toutes les solutions évidentes ont été essayées. Je n'ai pas vu quelque chose qui aborde le LEFT JOIN dans la requête, cependant.

Je me suis rendu compte que LEFT JOIN les instructions forcent généralement les planificateurs de requêtes à se joindre à un hachage, ce qui est rapide pour un petit nombre de résultats, mais terriblement lent pour un grand nombre de résultats. Comme indiqué dans la réponse de @Rick James, puisque la jointure dans la requête d'origine se trouve sur le champ d'identité analytics.id, cela générera un grand nombre de résultats. Une jointure de hachage donnera des résultats de performances terribles. La suggestion ci-dessous aborde ceci ci-dessous sans aucun changement de schéma ou de traitement.

Puisque l'agrégation est de analytics.source, J'essaierais une requête qui crée des agrégations distinctes pour la fréquence par source et les ventes par source et reporte la jointure gauche jusqu'à ce que l'agrégation soit terminée. Cela devrait permettre aux index d'être mieux utilisés (il s'agit généralement d'une jointure de fusion pour les grands ensembles de données).

Voici ma suggestion:

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 

J'espère que cela t'aides.

1
saarp