web-dev-qa-db-ja.com

SUM()はGROUP BYを無視し、2行ではなく4行を合計します

MySQLのGROUP BYに問題があります。

私のデータベース設定:

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)

この質問の目的:memberは、特定の時間にクラスを教えるか、アクティビティ(schedule_event)を主導します。 clientがこのクラスまたはアクティビティにサインアップします。

例えば:

クライアントA、B、Cの本の訪問とそれらの訪問はclient_visitschedule_event_idで構成されるmember_idテーブルに移動するため、どのクラスとどのメンバーが教えているか、アクティビティを行っているかがわかります。

ここで、特定のメンバーがクライアントがサインアップしたイベントの指導/指導に費やした合計時間を知りたい(client_visittype_of_visit列は「Booked」または「Present」と同等)。メンバーID 82をテストケースとして使用します。

メンバーID 82には2つの異なるクラスに4つのクライアントがあり、各クラスに2時間15分(8100秒)かかった場合、合計時間は16200秒になるはずです。

これが最初のクエリです。

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

結果は次のとおりです。 enter image description here

これは、最初のクラスのクライアントと2番目のクラスのクライアントを示しています。各クラスに1つずつ、2つの行が必要です。だから、私はこれを追加します:

 GROUP BY sch.id

これで、結果は次のようになります。 enter image description here

ここまでは順調ですね、

このメンバーには2つのスケジュールIDがあることを知っているので、これらを1つにまとめるようにグループを変更しました。

 GROUP BY sch.id AND cv.member_id

私は最初にsch.id(上の画像に既に表示されている結果)とcv.member_id(2つの行を取得したので、マージ後は1つになるはずです)に基づいてマージされると思います。

結果は(GROUP_CONCATを追加してscheduleIdを変更したので、両方のスケジュールIDがそこにあることがわかります): enter image description here

ここで、2つのスケジュールIDをまとめたように、2つのスケジュールされたクラスの時間を合計します。

ここでクエリを変更します。

SUM(TIMESTAMPDIFF(SECOND, sch.starts_at, sch.ends_at)) AS `totalTime`

そして結果は: enter image description hereenter image description here

私は32400を手に入れました!何らかの理由で、SUMは一意の2だけではなく、4行すべてを表示しています。

最終結果は

+-----------+------------+
| member_id | total_time |
+-----------+------------+
| 82        | 16200      |
+-----------+------------+

他のすべての列は必要ありません。何が起こっているのかを確認するためにそれらを作成しました

どうしましたか?

5
undefinedman

ウィレム・レンゼマが言ったように、GROUP BYの仕組みを誤解しました。彼の言ったことを理解していないようですので、少し違った言い方をしてみましょう。

論理的に十分なGROUP BYは、結果セットの行をグループ化するために使用されます。通常、行のグループ化に使用する列のリストを提供します。 GROUP BY sch.id, cv.member_idは、これらの2つの列の一意の値のセットを識別し、それらの値によって結果セットの行をグループ化するようにSQLに指示します。あなたのケースでは、これらの2つの値には2つの一意の値のペアがあります:

  • cv.member_id = 82、sch.id = 17101
  • cv.member_id = 82、sch.id = 17153

したがって、2つの行グループが得られます。3つは最初の値のペアで、もう1つは2番目のペアです。

GROUP BY句に列を追加すると、never結果としてグループが少なくなります-新しい列がすべての行で同じである(同じ数のグループがある場合)、または新しい列が異なる値を持ち、1つ以上の時間の元のグループのいくつかの行からいくつかの行を形成します(この場合、より多くのグループがあります)。

また、(Willemによって指摘されたように)構文エラーがあります。 GROUP BYリストの列はカンマで区切られます。 GROUP BY sch.id AND cv.member_idでは、sch.id AND cv.member_id、またはsch.idcv.member_idの両方をブール値であるかのように処理した結果によって、グループ化しています。どちらも0ではないため、ブール値に変換すると、両方が1(真)に評価され、(true AND true)の組み合わせは真になります。したがって、4行の1つのグループで終了します。

一歩下がって、あなたが実際に何をしようとしているのか(それがどのように見えるか)を考えてみましょう。特定のmember_idについて、「予約済み」または「現在」タイプのアクティビティに関与した合計時間を求めます。

合計時間はschedule_eventテーブルから計算されることに注意してください。また、特定のmember_idを同じschedule_eventに複数回関連付けることができることに注意してください。したがって、合計時間を取得するには、schedule_eventが関連付けられている個別のmember_id行を特定し、それらの一意の値の時間を合計する必要があります。

その場合、次に進む最も簡単な方法は、サブクエリを使用してschedule_eventsのリストを取得し、member_idが関連付けられているリストを取得して、それらの異なるイベントの合計時間を合計することです。

これを行うクエリは次のとおりです。

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

サブクエリ(想像的にsqとラベル付けされている)は、基本的には元のクエリです。 LEFT JOINと訪問のタイプの両方を識別するためにINNER JOINレコードが必要なため、client_visitmember_idに変更しました。ただし、SUMtotalTimeを削除しました。この時点では、各schedule_eventにかかる時間を知りたいだけです。私はDISTINCTも追加しました-このschedule_eventがこのmember_idとともに何回現れるかは関係ありません。合計時間は、1回、3回、または207回表示されても同じです。

schedule_eventが接続されているmember_idデータを特定したら、これらすべてのschedule_event行の合計時間を求めます。したがって、サブクエリの結果を取得し、それらをmember_idでグループ化し(複数のmember_id値でこれを引き戻す必要がある場合に備えて)、それぞれの計算時間を合計しますschedule_event行。

Joanoloが問題のdbfiddleを設定するのに苦労したので、私は彼の作業を取り、最後にこのクエリを追加したので、結果が希望どおりだったことがわかります。更新されたdbfiddleリンクは here です。

これがGROUP BYが実際にどのように機能するかを明確にするのに役立つことを願っています。

2
RDFozz

GROUP BYの機能について誤解していると思います。当然のことですが、MySQLのマニュアルではGROUP BYの機能が実際に明示されていないため、最初に学習したときに自分で問題が発生しました。特別な動作であり、実際の定義ではありません)。

私(オンザフライ)の定義:

GROUP BYはSELECT結果を圧縮して、GROUP BY句で指定された列の値の組み合わせごとに1行だけが返されるようにします。その意味では、DISTINCTに似ていますが、SELECTステートメントの代わりにGROUP BYの列で機能します。

非MySQLランドでは、GROUP BY句で指定した列と、必要な任意の 集計関数 のみをSELECTできます。 SUMを含むこれらの集計関数は、行ごとに動作し、現在「非表示」になっている追加行についてのみ結果を報告します。

ご覧のとおり、これがクエリが実際に実行していることです(または実行する予定ですが、ypercubeがコメントで指摘しているように、不正確な例を示したと思います)。指定されたsch.idについて、現在非表示になっているすべての追加行を合計し、それらの合計を報告しています。

sch.idの個別の値のみの合計が必要な場合は、必要な情報を取得するために別の方法で行う必要があります。

単純ではない理由の1つは、MySQLが合計に含めたい行がわからないことです。例(8100)ではすべて同じである可能性がありますが、その保証はありません。

MySQLではGROUP BY句で指定されていない列も集計関数でもない列を選択できるため、基本的に「ランダム」に1つを選択して表示します。実際にはランダムではありませんが、非決定的であり、常に同じ結果を与えるように見えても、同じクエリとデータに対していつでも変更できます。

したがって、先に進む前に、各sch.idのどの行に合計する値が含まれるかをどのように決定するかを決定する必要があります。

値が常に同じであることがわかっている場合、単純な(必ずしも最適化されているわけではありませんが)ソリューションとして、元のGROUP BYクエリを別のクエリでラップし(元のクエリをサブクエリにする)、外部クエリでSUM関数を使用します。 GROUP BY句なし。サブクエリは重複を削除し、外部クエリは重複排除された行の合計を合計します。

6
Willem Renzema