0
votes

MYSQL SUM jusqu'au dernier jour de Chaque mois pour les 12 derniers mois

J'ai une table comme cette deux

Tableau A P>

2019-05-31  33488.69        109.127800
2019-06-30  263.690          1248932.227800
2019-07-31  274.690         131.827800
2019-08-31  627.690         13.687800
2019-09-30  1533.370000     08.347800
2019-10-31  1444.370000     01.327800
2019-11-30  5448.370000     247.227800
2019-12-31  61971.370000    016.990450
2020-01-31  19550.370000    2535.185450
2020-02-29  986.370000      405.123300
2020-03-31  1152.370000     26.793300
2020-04-30  9404.370000     11894.683300
2020-05-31  3404.370000     17894.683300




6 commentaires

Comment votre table est-elle indexée? Veuillez nous montrer le schéma. À tout le moins, vous devriez avoir un index sur toutes ces colonnes.


l'index de A a la date, l'id, B_id et l'index de B inclut l'id et le type les deux.


Quelle version de MySQL utilisez-vous? Ces totaux en cours d'exécution sont beaucoup plus faciles à obtenir avec les fonctions de fenêtre, disponibles à partir de MySQL 8.


Vos lignes de résultats semblent un peu surprenantes. Je m'attendais à ce que les valeurs augmentent chaque mois, mais il semble y avoir des mois avec des soldes négatifs réduisant les totaux en cours. Alors, est-ce que peut-il manquer des mois dans votre tableau? (Sinon, la requête de ma réponse deviendrait beaucoup plus simple sans qu'il soit nécessaire de créer des lignes de mois.)


pouvez-vous faire dans une procédure stockée? L'utilisation d'une variable utilisateur pour accumuler un total cumulé résoudrait ce problème.


il est très peu probable qu'il y ait des mois manquants dans les données .. mais oui, il a des valeurs négatives.


3 Réponses :


2
votes

J'utiliserais l'agrégation conditionnelle et pré-agrégerais les totaux mensuels en une seule passe, au lieu de faire vingt-six passages individuels à plusieurs reprises sur les mêmes données.

Je commencerais par quelque chose comme ceci: p >

SELECT d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY  AS last_date
     , SUM(IFNULL(t.tot_type_5,0))                   AS rt_type_5
     , SUM(IFNULL(t.tot_type_7,0))                   AS rt_type_7
  FROM ( -- first day of next month
         SELECT DATE(NOW()) + INTERVAL -DAY(DATE(NOW()))+1 DAY + INTERVAL 1 MONTH AS dt
       ) d
 CROSS
  JOIN ( -- thirteen integers, integers 0 thru 12
         SELECT 0 AS n
         UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
         UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
         UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
       ) i

 LEFT
 JOIN ( -- totals by month
        SELECT CASE WHEN A.date < DATE(NOW()) + INTERVAL -14 MONTH
               THEN LAST_DAY(     DATE(NOW()) + INTERVAL -14 MONTH )
               ELSE LAST_DAY( A.date )
               END                                    AS _month_end
             , SUM(IF( B.type = 5 , B.amount , NULL)) AS tot_type_5
             , SUM(IF( B.type = 7 , B.amount , NULL)) AS tot_type_7
          FROM A
          JOIN B
            ON B.id = A.B_id
         WHERE B.type IN (5,7)
         GROUP
            BY _month_end
      ) t
   ON t._month_end < d.dt

GROUP BY d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY
ORDER BY d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY DESC

(le montant de la colonne n'est pas qualifié dans la requête d'origine, il suffit donc de deviner ici de quelle table provient. ajustez si nécessaire. La meilleure pratique consiste à qualifier tout références de colonnes.

Cela nous donne les sous-totaux pour chaque mois, en un seul passage par A et B.

Nous pouvons tester et ajuster cette requête.

Ensuite, nous pouvons incorporer cela comme une vue en ligne dans une requête externe qui additionne ces totaux mensuels (je ferais une jointure externe, juste au cas où des lignes manquent, semez nous ne finirons pas par omettre des lignes.) p>

Quelque chose comme ceci:

SELECT CASE WHEN A.date < DATE(NOW()) + INTERVAL -14 MONTH
       THEN LAST_DAY(     DATE(NOW()) + INTERVAL -14 MONTH )
       ELSE LAST_DAY( A.date )
       END                                    AS _month_end
     , SUM(IF( B.type = 5 , B.amount , NULL)) AS tot_type_5
     , SUM(IF( B.type = 7 , B.amount , NULL)) AS tot_type_7
  FROM A
  JOIN B
    ON B.id = A.B_id
 WHERE B.type IN (5,7)
 GROUP
    BY _month_end

La conception est destinée à faire un coup à travers l'ensemble A JOIN B . Nous nous nous attendons à récupérer environ 14 lignes. Et nous effectuons une semi-jointure, en dupliquant plusieurs fois les mois les plus anciens, soit environ 14 x 13/2 = 91 lignes, qui se réduisent en 13 lignes.

Le gros rocher en termes de performances va matérialiser cette requête de vue en ligne.


2 commentaires

Hé! Merci de m'avoir si bien expliqué. Je pense que Group By _month_end ne donne pas le résultat que je recherchais .. je voulais SUM jusqu'au mois dernier des 12 derniers mois .. La réponse de Thorsten Kettner a fonctionné le mieux pour moi.


Yah, la requête de vue en ligne renvoie les totaux par mois. Ce n'est qu'une partie de celui-ci. C'est la condition dans la semi-jointure de la requête externe ( t._month_end ) qui provoque la combinaison des totaux "par mois" pour les mois précédents . Et c'est la clé pour gagner en performances, la requête externe additionne des ensembles beaucoup plus petits de totaux mensuels pré-calculés. Le modèle de requête présenté dans cette réponse satisfait à la spécification et fonctionnera dans les versions MySQL antérieures à 8



0
votes

Voici comment j'aborderais probablement cela dans MySQL 8 avec SUM OVER :

  1. Obtenez les 12 derniers mois.
  2. Utilisez ces mois pour ajouter des lignes de mois vides aux données d'origine, car MySQL ne prend pas en charge les jointures externes complètes.
  3. Obtenez les totaux cumulés pour tous les mois.
  4. N'afficher que les douze derniers mois.

La requête:

with months (date) as
(
  select last_day(current_date - interval  1 month) union all
  select last_day(current_date - interval  2 month) union all
  select last_day(current_date - interval  3 month) union all
  select last_day(current_date - interval  4 month) union all
  select last_day(current_date - interval  5 month) union all
  select last_day(current_date - interval  6 month) union all
  select last_day(current_date - interval  7 month) union all
  select last_day(current_date - interval  8 month) union all
  select last_day(current_date - interval  9 month) union all
  select last_day(current_date - interval 10 month) union all
  select last_day(current_date - interval 11 month) union all
  select last_day(current_date - interval 12 month)
)
, data (date, amount, type) as
(
  select last_day(a.date), a.amount, b.type
  from a
  join b on b.id = a.b_id
  where b.type in (5, 7)
  union all
  select date, null, null from months
)
select
  date,
  sum(sum(case when type = 5 then amount end)) over (order by date) as t5,
  sum(sum(case when type = 7 then amount end)) over (order by date) as t7
from data
group by date
order by date
limit 12;

Démo: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=ddeb3ab3e086bfc182f0503615fba74b

Je ne sais pas si c'est plus rapide que votre propre requête ou non. Essayez-le. (Vous obtiendrez ma requête beaucoup plus rapidement en ajoutant une colonne générée pour last_day (date) à votre table et en l'utilisant. Si vous en avez besoin souvent, cela peut être une option.)


1 commentaires

Merci pour votre réponse. Cela a parfaitement fonctionné ... n'était pas familier avec les fonctions de fenêtre. C'est aussi beaucoup plus rapide. Merci.



0
votes

Vous obtenez des réponses compliquées. Je pense que c'est plus facile. Commencez par savoir que nous pouvons facilement additionner pour chaque mois:

SET @running5 := 0;
SET @running7 := 0;

SELECT 
    LAST_DAY(CONCAT(year,'-',month,'-',1)),
    (@running5 := @running5 + (CASE WHEN type=5 THEN monthtotal ELSE 0 END)) as running5, 
    (@running7 := @running7 + (CASE WHEN type=7 THEN monthtotal ELSE 0 END)) as running7
FROM 
(SELECT SUM(amount) as monthtotal,type,MONTH(date) as month,YEAR(date) as year FROM A LEFT JOIN B on A.B_id=B.id GROUP BY type,month,year) AS totals
ORDER BY year,month

À partir de ces données, nous pouvons utiliser une variable pour obtenir le total cumulé. Il est préférable de le faire en initialisant la variable, mais ce n'est pas nécessaire. Nous pouvons obtenir les données nécessaires comme ceci

SET @running := 0;

SELECT (@running := @running + monthtotal) as running, type, LAST_DAY(CONCAT(year,'-',month,'-',1))
FROM 
(SELECT SUM(amount) as monthtotal,type,MONTH(date) as month,YEAR(date) as year FROM A LEFT JOIN B on A.B_id=B.id GROUP BY type,month,year) AS totals
ORDER BY year,month

Vous devez vraiment avoir un connecteur qui prend en charge plusieurs instructions, ou faire plusieurs appels pour initialiser la variable. Bien que vous puissiez annuler la vérification de la variable et la valeur par défaut à 0, vous rencontrez toujours un problème si vous exécutez la requête une deuxième fois.

Dernière chose, si vous voulez vraiment que les types soient additionnés séparément: p >

SELECT SUM(amount) as monthtotal,
        type,
        MONTH(date) as month,
        YEAR(date) as year 
FROM A LEFT JOIN B on A.B_id=B.id 
GROUP BY type,month,year

Nous n'affichons toujours pas les mois sans données. Je ne suis pas sûr que ce soit une exigence. Mais cela ne devrait nécessiter qu'un seul passage de la table A.

Assurez-vous également que l'identifiant de la table B est indexé.


1 commentaires

Il est bon que cette réponse note que le modèle de requête ne garantit pas qu'une ligne pour chaque mois sera renvoyée. Et oui, c'est une noix plus facile à casser. Nous devons également noter que le Manuel de référence de MySQL donne un avertissement spécifique sur l'affectation et l'évaluation des variables définies par l'utilisateur dans la même instruction; La documentation MySQL indique que le comportement n'est pas défini, ce qui signifie que le comportement n'est pas garanti, mais nous observons un comportement cohérent avec des requêtes soigneusement conçues.