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.
MySQL n'a pas de fonctionnalité récursive, vous avez donc la possibilité d'utiliser l'astuce de table NUMBERS -
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;
Remplissez le tableau en utilisant:
INSERT INTO `example`.`numbers`
( `id` )
VALUES
( NULL )
... pour autant de valeurs que vous le souhaitez.
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
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`
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.
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.
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.