web-dev-qa-db-fra.com

MySQL comment remplir les dates manquantes dans la plage?

J'ai un tableau avec 2 colonnes, date et score. Il contient au maximum 30 entrées, pour chacun des 30 derniers jours.

date      score
-----------------
1.8.2010  19
2.8.2010  21
4.8.2010  14
7.8.2010  10
10.8.2010 14

Mon problème est que certaines dates manquent - je veux voir:

date      score
-----------------
1.8.2010  19
2.8.2010  21
3.8.2010  0
4.8.2010  14
5.8.2010  0
6.8.2010  0
7.8.2010  10
...

Ce que j'ai besoin de la seule requête est d'obtenir: 19,21,9,14,0,0,10,0,0,14 ... Cela signifie que les dates manquantes sont remplies de 0.

Je sais comment obtenir toutes les valeurs et en langage côté serveur en itérant les dates et en manquant les blancs. Mais est-ce possible de le faire dans mysql, pour que je trie le résultat par date et récupère les pièces manquantes.

EDIT: Dans ce tableau, il y a une autre colonne nommée UserID, donc j'ai 30 000 utilisateurs et certains d'entre eux ont le score dans ce tableau. Je supprime les dates tous les jours si la date est inférieure à 30 jours car j'ai besoin du score des 30 derniers jours pour chaque utilisateur. La raison en est que je fais un graphique de l'activité de l'utilisateur au cours des 30 derniers jours et pour tracer un graphique, j'ai besoin des 30 valeurs séparées par une virgule. Je peux donc dire dans la requête, obtenez-moi l'activité USERID = 10203 et la requête me donnerait les 30 scores, un pour chacun des 30 derniers jours. J'espère que je suis plus clair maintenant.

59
Jerry2

MySQL n'a pas de fonctionnalité récursive, vous avez donc la possibilité d'utiliser l'astuce de table NUMBERS -

  1. Créez une table qui ne contient que des nombres incrémentiels - facile à faire en utilisant un auto_increment:

    DROP TABLE IF EXISTS `example`.`numbers`;
    CREATE TABLE  `example`.`numbers` (
      `id` int(10) unsigned NOT NULL auto_increment,
       PRIMARY KEY  (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
  2. Remplissez le tableau en utilisant:

    INSERT INTO `example`.`numbers`
      ( `id` )
    VALUES
      ( NULL )
    

    ... pour autant de valeurs que vous le souhaitez.

  3. Utilisez DATE_ADD pour construire une liste de dates, en augmentant les jours en fonction de la valeur NUMBERS.id. Remplacez "2010-06-06" et "2010-06-14" par vos dates de début et de fin respectives (mais utilisez le même format, AAAA-MM-JJ) -

    SELECT `x`.*
      FROM (SELECT DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY)
              FROM `numbers` `n`
             WHERE DATE_ADD('2010-06-06', INTERVAL `n`.`id` -1 DAY) <= '2010-06-14' ) x
    
  4. LEFT JOIN sur votre table de données en fonction de la portion de temps:

       SELECT `x`.`ts` AS `timestamp`,
              COALESCE(`y`.`score`, 0) AS `cnt`
         FROM (SELECT DATE_FORMAT(DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY), '%m/%d/%Y') AS `ts`
                 FROM `numbers` `n`
                WHERE DATE_ADD('2010-06-06', INTERVAL `n`.`id` - 1 DAY) <= '2010-06-14') x
    LEFT JOIN TABLE `y` ON STR_TO_DATE(`y`.`date`, '%d.%m.%Y') = `x`.`ts`
    

Si vous souhaitez conserver le format de date, utilisez la fonction DATE_FORMAT :

DATE_FORMAT(`x`.`ts`, '%d.%m.%Y') AS `timestamp`
55
OMG Ponies

Vous pouvez accomplir cela en utilisant un Calendrier Table. C'est un tableau que vous créez une fois et remplissez avec une plage de dates (par exemple, un ensemble de données pour chaque jour 2000-2050; cela dépend de vos données). Ensuite, vous pouvez faire une jointure externe de votre table contre la table de calendrier. Si une date manque dans votre tableau, vous retournez 0 pour le score.

14
Soundlink

Je ne suis pas fan des autres réponses, nécessitant la création de tableaux et autres. Cette requête le fait efficacement sans tables auxiliaires.

SELECT 
    IF(score IS NULL, 0, score) AS score,
    b.Days AS date
FROM 
    (SELECT a.Days 
    FROM (
        SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS Days
        FROM       (SELECT 0 AS a 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) AS a
        CROSS JOIN (SELECT 0 AS a 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) AS b
        CROSS JOIN (SELECT 0 AS a 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) AS c
    ) a
    WHERE a.Days >= curdate() - INTERVAL 30 DAY) b
LEFT JOIN your_table
    ON date = b.Days
ORDER BY b.Days;

Permet donc de disséquer cela.

SELECT 
    IF(score IS NULL, 0, score) AS score,
    b.Days AS date

Le if détectera les jours sans score et les définira sur 0. b.Days est le nombre de jours configuré que vous avez choisi d'obtenir à partir de la date actuelle, jusqu'à 1000.

    (SELECT a.Days 
    FROM (
        SELECT curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS Days
        FROM       (SELECT 0 AS a 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) AS a
        CROSS JOIN (SELECT 0 AS a 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) AS b
        CROSS JOIN (SELECT 0 AS a 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) AS c
    ) a
    WHERE a.Days >= curdate() - INTERVAL 30 DAY) b

Cette sous-requête est quelque chose que j'ai vu sur stackoverflow. Il génère efficacement une liste des 1000 derniers jours à partir de la date actuelle. L'intervalle (actuellement 30) dans la clause WHERE à la fin détermine quels jours sont retournés; le maximum est de 1000. Cette requête pourrait être facilement modifiée pour renvoyer des centaines d'années de dates, mais 1000 devrait être bon pour la plupart des choses.

LEFT JOIN your_table
    ON date = b.Days
ORDER BY b.Days;

C'est la partie qui apporte votre table qui contient la partition. Vous vous comparez à la plage de dates sélectionnée à partir de la requête du générateur de dates pour pouvoir remplir les 0 si nécessaire (le score sera initialement défini sur NULL, car il s'agit d'un LEFT JOIN; cela est corrigé dans l'instruction select). Je le commande aussi par les dates, juste parce que. C'est la préférence, vous pouvez également commander par score.

Avant le ORDER BY vous pouvez facilement rejoindre votre table sur les informations utilisateur que vous avez mentionnées lors de votre modification, pour ajouter cette dernière exigence.

J'espère que cette version de la requête aide quelqu'un. Merci d'avoir lu.

9
Michael Conard

La réponse de Michael Conard est excellente mais j'avais besoin d'intervalles de 15 minutes où le temps doit toujours commencer en haut de chaque 15e minute:

SELECT a.Days 
FROM (
    SELECT FROM_UNIXTIME( FLOOR( UNIX_TIMESTAMP() / (15 * 60) ) * (15 * 60)) - INTERVAL 15 * (a.a + (10 * b.a) + (100 * c.a)) MINUTE AS Days
    FROM       (SELECT 0 AS a 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) AS a
    CROSS JOIN (SELECT 0 AS a 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) AS b
    CROSS JOIN (SELECT 0 AS a 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) AS c
) a
WHERE a.Days >= curdate() - INTERVAL 30 DAY

Cela mettra l'heure actuelle à la ronde précédente à la 15e minute:

FROM_UNIXTIME( FLOOR( UNIX_TIMESTAMP() / (15 * 60) ) * (15 * 60))

Et cela réduira le temps avec une étape de 15 minutes:

- INTERVAL 15 * (a.a + (10 * b.a) + (100 * c.a)) MINUTE

S'il existe un moyen plus simple de le faire, faites-le moi savoir.

1
phoenix