J'ai des difficultés avec GROUP BY
dans MySQL.
Configuration de ma base de données:
client_visit
- id
- member_id
- status_type_id (type_of_visit table)
- visit_starts_at
- visit_ends_at
member
- id
schedule_event
- id
- member_id
- starts_at
- ends_at
type_of_visit
- id
- type (TYPE_BOOKED, TYPE_PRESENT etc)
Aux fins de cette question: un member
enseigne une classe ou dirige une activité (un schedule_event
) à un moment donné. Un client
s'inscrit à cette classe ou activité.
Par exemple:
Réservez les visites des clients A, B et C et celles-ci vont à client_visit
table composée de schedule_event_id
et member_id
, donc nous savons quelle classe et quel membre enseigne/ou a une activité.
Maintenant, nous voulons connaître le temps total passé par un membre donné à enseigner/diriger des événements auxquels les clients se sont inscrits (sur la base du client_visit
type_of_visit
colonne équivalente à "Réservé" ou "Présent"). Nous prendrons l'ID membre 82 comme cas de test.
L'ID membre 82 comptait 4 clients dans deux classes différentes, donc si chaque classe prenait 2h 15 minutes (8100 secondes), cela signifie que le temps total devrait être de 16200 secondes.
Voici ma requête d'abord:
SELECT cv.member_id AS `member_id`,
sch.id AS `scheduleId`,
cv.visit_starts_at AS `visitStartsAt`,
TIMESTAMPDIFF(SECOND, sch.starts_at, sch.ends_at) AS `totalTime`
FROM `schedule_event` AS `sch`
LEFT JOIN `client_visit` AS `cv` ON cv.schedule_event_id = sch.id
INNER JOIN `type_of_visit` AS `tov` ON tov.id = cv.status_type_id
WHERE (tov.type = 'TYPE_BOOKED' OR tov.type = 'TYPE_PRESENT') and cv.member_id = 82
Cela me montre les clients de la première classe et celui de la seconde. Je veux juste deux rangées, une pour chaque classe. J'ajoute donc ceci:
GROUP BY sch.id
Maintenant, le résultat est le suivant:
Jusqu'ici tout va bien,
Je sais qu'il existe deux identifiants de planification pour ce membre, j'ai donc modifié le groupe en les regroupant en un seul:
GROUP BY sch.id AND cv.member_id
Je m'attends à ce qu'il fusionne d'abord sur la base de sch.id
(le résultat est déjà affiché dans l'image ci-dessus) et cv.member_id
(nous avons eu deux lignes, donc après la fusion, nous devrions en avoir une)
et le résultat est (j'ai modifié scheduleId en ajoutant GROUP_CONCAT, afin que nous puissions voir les deux ID de planification sont là):
Maintenant, tout comme j'ai rassemblé les deux ID de programme, je veux additionner le temps pour les deux classes programmées.
Je modifie maintenant la requête:
SUM(TIMESTAMPDIFF(SECOND, sch.starts_at, sch.ends_at)) AS `totalTime`
J'ai 32400! Pour une raison quelconque, le SUM voit toujours les 4 lignes au lieu du seul 2 unique.
Je m'attendais à ce que le résultat final soit
+-----------+------------+
| member_id | total_time |
+-----------+------------+
| 82 | 16200 |
+-----------+------------+
Toutes les autres colonnes ne sont pas nécessaires, je les ai juste faites pour voir ce qui se passe
Qu'est-ce qui ne va pas?
Comme l'a dit Willem Renzema, vous avez mal compris comment GROUP BY
travaux. Comme il ne semble pas que vous ayez compris ce qu'il a dit, permettez-moi d'essayer de le dire un peu différemment.
GROUP BY
, assez logiquement, est utilisé pour regrouper les lignes de votre jeu de résultats. Normalement, vous fournissez une liste des colonnes à utiliser pour regrouper vos lignes. GROUP BY sch.id, cv.member_id
indique à SQL d'identifier les ensembles de valeurs uniques pour ces deux colonnes et de regrouper les lignes dans l'ensemble de résultats par ces valeurs. Dans votre cas, il existe deux paires de valeurs uniques pour ces deux valeurs:
cv.member_id
= 82, sch.id
= 17101cv.member_id
= 82, sch.id
= 17153Ainsi, vous obtiendrez deux groupes de lignes - trois qui ont la première paire de valeurs et un qui a la deuxième paire.
Ajout de colonnes supplémentaires à un GROUP BY
la clause ne donnera jamais moins de groupes - soit les nouvelles colonnes sont les mêmes dans toutes les lignes (auquel cas vous avez le même nombre de groupes), ou les nouvelles colonnes ont des valeurs différentes à partir de certaines lignes dans un ou plusieurs groupes originaux d'heures (dans ce cas, vous aurez maintenant plus de groupes).
De plus (comme l'a souligné Willem), vous avez une erreur de syntaxe. Les colonnes dans un GROUP BY
la liste est séparée par des virgules. Dans ton GROUP BY sch.id AND cv.member_id
, vous regroupez par calcul: sch.id AND cv.member_id
, ou le résultat du traitement des deux sch.id
et cv.member_id
comme s'il s'agissait de valeurs booléennes. Comme ni l'un ni l'autre n'est égal à 0, lorsqu'ils sont convertis en booléens, les deux sont évalués à 1 (vrai), et la combinaison (true AND true)
est vrai. Ainsi, vous vous retrouvez avec un seul groupe, de 4 rangées.
Revenons en arrière et examinons ce que vous essayez de faire (à quoi il ressemble). Pour un member_id
, vous voulez le temps total pendant lequel ils sont impliqués dans des activités de type "Réservé" ou "Présent".
Notez que le temps total est calculé à partir de schedule_event
table. Notez également qu'un member_id
peut être associé au même schedule_event
plus d'une fois. Donc, pour obtenir le temps total, nous devons identifier le schedule_event
lignes que notre member_id
est lié à, et additionne le temps pour ces valeurs uniques.
Cela étant, la façon la plus simple de procéder consiste à utiliser une sous-requête pour obtenir la liste des _ schedule_events
notre member_id
est lié à, puis additionne le temps total pour ces événements distincts.
Voici une requête qui fera exactement cela:
SELECT `member_id`
,SUM(`totalTime`) as `totalTime`
FROM (
SELECT DISTINCT
cv.member_id AS `member_id`,
sch.id AS `scheduleId`,
TIMESTAMPDIFF(SECOND, sch.starts_at, sch.ends_at) AS `totalTime`
FROM
`schedule_event` AS `sch`
INNER JOIN `client_visit` AS `cv` ON cv.schedule_event_id = sch.id
INNER JOIN `type_of_visit` AS `tov` ON tov.id = cv.status_type_id
WHERE
(tov.type = 'TYPE_BOOKED' OR tov.type = 'TYPE_PRESENT')
AND cv.member_id = 82
) sq
GROUP BY `member_id`;
La sous-requête (nommée avec imagination sq
) est fondamentalement votre requête d'origine. J'ai changé ton LEFT JOIN
à un INNER JOIN
, car nous devons avoir un client_visit
enregistrement pour identifier à la fois le member_id
, et le type de visite. Cependant, j'ai supprimé le SUM
sur totalTime
; à ce stade, nous voulons juste connaître le temps chaque schedule_event
prendra. J'ai également ajouté DISTINCT
- peu nous importe combien de fois cette schedule_event
apparaît avec ce member_id
; la durée totale sera la même, qu'elle s'affiche une, trois fois ou 207 fois.
Une fois que nous avons identifié le schedule_event
données que notre member_id
est connecté à, alors nous voulons le temps total pour tous ceux schedule_event
Lignes. Donc, nous prenons les résultats de la sous-requête, les regroupons par member_id
(au cas où il serait nécessaire de le retirer pour plusieurs member_id
valeurs) et résumer les temps calculés pour chaque schedule_event
rangée.
Puisque joanolo s'était donné la peine de mettre en place un dbfiddle pour votre problème, j'ai pris son travail et ajouté cette requête à la fin, afin que vous puissiez voir les résultats étaient ce que vous vouliez; le lien dbfiddle mis à jour est ici .
J'espère que cela aide à clarifier comment GROUP BY
fonctionne réellement pour vous.
Je crois que vous avez une mauvaise compréhension de ce que fait GROUP BY. Pas étonnant, j'ai eu des problèmes moi-même lors de mon premier apprentissage, en grande partie parce que le manuel MySQL ne dit pas explicitement ce que fait GROUP BY, du moins pas que je puisse trouver (et j'ai beaucoup cherché, tout à l'heure; beaucoup de mises en garde et comportement spécial, pas tellement une définition réelle).
Ma définition (à la volée):
GROUP BY condense vos résultats SELECT afin qu'une seule ligne soit renvoyée pour chaque combinaison distincte de valeurs pour les colonnes spécifiées dans la clause GROUP BY. En ce sens, il est similaire à DISTINCT, mais fonctionne sur les colonnes du GROUP BY au lieu de l'instruction SELECT.
En terrain non-MySQL, vous ne pouvez sélectionner que les colonnes que vous spécifiez dans votre clause GROUP BY, PLUS les fonctions d'agrégation que vous voulez. Ces fonctions d'agrégation, y compris SUM, fonctionnent sur une base par ligne, rapportant un résultat UNIQUEMENT pour les lignes supplémentaires désormais "masquées".
Comme vous pouvez le voir, c'est ce que fait réellement votre requête (ou le serait, mais je pense que vous avez donné un exemple inexact, comme ypercube le fait remarquer dans les commentaires). Il résume toutes les lignes supplémentaires désormais masquées et indique leur total pour le sch.id
.
Si vous voulez que le total des valeurs distinctes de chaque sch.id
, vous devrez faire les choses différemment pour obtenir les informations souhaitées.
L'une des raisons pour lesquelles ce n'est pas simple, c'est que MySQL n'a aucune idée de la ligne que vous souhaitez inclure dans la somme. Ils peuvent être tous les mêmes dans votre exemple (8100), mais il n'y a aucune garantie de cela.
Puisque MySQL vous permet de sélectionner des colonnes qui ne sont ni spécifiées dans la clause GROUP BY ni des fonctions d'agrégation, il en choisit essentiellement une au hasard et vous l'affiche. Bien qu'il ne soit pas réellement aléatoire, il n'est pas déterministe et peut changer à tout moment pour la même requête et les mêmes données, même s'il vous semble toujours donner le même résultat.
Donc, avant de pouvoir continuer, vous devez décider comment vous voulez déterminer quelle ligne pour chaque sch.id
contient la valeur que vous souhaitez additionner.
Si vous savez que les valeurs sont toujours les mêmes, une solution simple (mais pas nécessairement optimisée) consiste à encapsuler votre requête GROUP BY d'origine dans une autre requête (faisant de l'original une sous-requête), puis à utiliser la fonction SUM dans la requête externe, sans clause GROUP BY. La sous-requête supprimera vos doublons et la requête externe résumera le total des lignes dédoublonnées.