SQLを使用してデータをグループ分けする方法

このQ&Aのポイント
  • SQLを使用してデータをグループ分けする方法について紹介します。メンバーの点数からグループ分けするためには、条件として50点以上離れていれば別のグループとすることができます。最近知られてきたLAG/LEAD分析関数を使えば、SQLを使用して簡単にグループ分けができます。具体的な処理内容として、テーブルのデータをグループ分けし、結果を新しいテーブルに格納する方法を紹介します。
  • 例えば、テーブルのデータをグループ分けしたい場合、各レコードの最小値と最大値を使って差を計算します。この差が50点以内であれば同じグループとみなし、それ以外のレコードは別のグループとなります。テーブルのデータが更新された場合でも、SQLを使用して簡単に再計算することができます。
  • 以上のように、SQLを使用してデータをグループ分けする方法を紹介しました。LAG/LEAD分析関数を使えば、簡単にグループ分けができるため、管理が楽になります。テーブルのデータが更新された場合でも、SQLを使って再計算することができるので、柔軟性があります。ぜひこの方法を活用してみてください。
回答を見る
  • ベストアンサー

データをグループ分けするSQLを組みたいのですが

メンバーを、各人の点数からグループ分けしたいと 考えています。 グループ分けの基準は、たとえば50点という尺度があり、 その尺度以上離れていたら別のグループとするというものです。 また、各メンバーの点数は最大と最小が登録されています。 現在は、VBでプログラムを作って対応しましたが、 最近、LAG/LEAD分析関数の存在を知ったので、それをうまく使えば、 SQLだけで対応できるのではないかと思えます。 SQLだけで対応できると、そのVIEWを作ってしまえば、ユーザーに 簡単に引き渡せるので管理が楽になります。 うまいSQLがあれば教えてください。 具体的な処理内容は以下の通りです。 例えば、TABLE1のようなデータで、グループ分けをすると ID=1と3の差が50点以内、3と2の差も50点以内なので、 その3名で1グループできます。 ID=4と5は、他と50点以上離れているので、それぞれ一人で 1グループになります。 ID= 6,7 は、差が50点以内なので、2名で1グループとなります。 結果として、TABLE2のような結果になります。 TABLE 1 ID MIN_SCORE MAX_SCORE 1 100 110 2 190 200 3 120 150 4 300 330 5 400 420 6 670 700 7 600 630 TABLE 2 CLASS MIN_SCORE MAX_SCORE N 1 100 200 3 2 300 330 1 3 400 420 1 4 600 700 2 そこに、TABLE1'のようなデータが加わると ID=4と8は差が50点以内で、7と8も差が50点以内なので、 そこらが全て同じグループになり TABLE 2' の結果となります。 TABLE 1' 8 380 550 TABLE 2' CLASS MIN_SCORE MAX_SCORE N 1 100 200 3 2 300 700 5 以上、よろしくお願いします

  • Oracle
  • 回答数3
  • ありがとう数3

質問者が選んだベストアンサー

  • ベストアンサー
  • jamshid6
  • ベストアンサー率88% (591/669)
回答No.3

#1,#2です。 >MIN_SCOREが同じものが複数ある場合 なるほど、そういうケースの値も入りうるわけですか。 それは想定していませんでしたが、以下のように条件を追加します。 SELECT ROW_NUMBER() OVER (ORDER BY MIN(MIN_SCORE)) CLASS, MIN(MIN_SCORE) MIN_SCORE, MAX(MAX_SCORE) MAX_SCORE, COUNT(*) N FROM (SELECT DISTINCT CONNECT_BY_ROOT ID AS ROOTID,ID,MIN_SCORE,MAX_SCORE FROM TABLE1 CONNECT BY ( (PRIOR MIN_SCORE<MIN_SCORE AND PRIOR MAX_SCORE+50>=MIN_SCORE) OR (PRIOR MIN_SCORE=MIN_SCORE AND PRIOR ID<ID)) ) WHERE ROOTID IN (SELECT MIN(ID) FROM TABLE1 t WHERE NOT EXISTS (SELECT ID FROM TABLE1 WHERE MIN_SCORE<t.MIN_SCORE AND MAX_SCORE>=t.MIN_SCORE-50) GROUP BY MIN_SCORE) GROUP BY ROOTID 自分のMIN_SCORE~MIN_SCORE-50点のレンジにレコードが存在しないものを起点として、 自分のMIN_SCORE~MAX_SCORE+50点以上の間にあるレコードを辿っていくわけなので、 MIN_SCOREが同じならばIDが最小の1つをとり、他のものはCONNECT先に含めるようにします。 ところで、本件を考えているうちに、CONNECT BYを使わなくても実現できることも気づきました。 構造だけなら、こちらの方がわかりやすいかもしれませんので、ご参考までに。 SELECT ROW_NUMBER() OVER (ORDER BY x1.MIN_SCORE) SEQ, x1.MIN_SCORE, x2.MAX_SCORE, (SELECT COUNT(*) FROM TABLE1 WHERE MIN_SCORE BETWEEN x1.MIN_SCORE AND x2.MAX_SCORE) N FROM (SELECT ROW_NUMBER() OVER (ORDER BY MIN_SCORE) SEQ, MIN_SCORE FROM TABLE1 t1 WHERE NOT EXISTS (SELECT * FROM TABLE1 WHERE MIN_SCORE<t1.MIN_SCORE AND (t1.MIN_SCORE-50)<=MAX_SCORE) GROUP BY MIN_SCORE) x1 INNER JOIN (SELECT ROW_NUMBER() OVER (ORDER BY MAX_SCORE) SEQ, MAX_SCORE FROM TABLE1 t2 WHERE NOT EXISTS (SELECT * FROM TABLE1 WHERE MIN_SCORE<=(t2.MAX_SCORE+50) AND t2.MAX_SCORE<MAX_SCORE) GROUP BY MAX_SCORE) x2 ON x2.SEQ=x1.SEQ

anahiro
質問者

お礼

いろいろ教えていただきありがとうございます。 CONNECT BY は初めて見たのですが、他を調べても等号条件の例しか見つからず、不等号条件は実は非常に高度な技なんですね。まだ、CONNECT 自体の機能については完全には理解しきれていませんが、非常に参考になりました。 あとで上げられたCONNECT BY を使わない方法は、アルゴリズムは先のと同じかと思ってしまったのでSQLを読み解くのに時間がかかってしまいました。実際は、各グループの親を求めるのではなく、各グループの最小値と最大値を別々に求めるように、アルゴリズム自体も変更されているのですね。感服しました。 最初は、なぜ ROW_NUMBER() OVER (ORDER BY MIN_SCORE) SEQ と ROW_NUMBER() OVER (ORDER BY MAX_SCORE) SEQ とでJOINできるのかわからなかくて、きっとここらがCONNECT BY の代わりになるコツなんだろうと曲解してえらく悩んでしまいました。 今回の回答のおかげで、またひとつSQLの守備範囲が広がりました。本当にどうもありがとうございます。

その他の回答 (2)

  • jamshid6
  • ベストアンサー率88% (591/669)
回答No.2

#1です。 CONNECT BYが必要と思った理由は複数のレコード間で50点以内の条件を満たすものを辿っていく必要があったからです。 (LAGやLEADでそういう使い方ができるのであれば、私自身の勉強不足ということになるかもしれませんが) B_CLASSのクラス分けは単純にint((B_SCORE-4)/20で行えばよく、サブクエリのを1つ深くするだけの話だと思います。 >実際はもっともっともっと複雑な問題なのですが ということなので、B_CLASSのところだけクリアするクエリを示してもあまり意味はなさそうです。 結局は質問者さんに応用力を発揮していただかないといけないと思います。 (それでも、CONNECT BYでこなせないとは思いませんが)

anahiro
質問者

補足

言われてみるとLAG/LEAD でグループ分けするのは難しい気がしてきました。 jamshid6さんの案は、CONNECT BY の CONNECT_BY_ROOT関数? を使い、各グループ毎の大元の親レコードを求めるPRIOR条件式を作りだすのがコツというわけですね。その方針なら、int((B_SCORE-4)/20 もPRIOR条件式に組み込んでグループ分けできるようにすれば、私が当初固執していたPARTITIONを使わなくても一気にグループ分けができる気がしてきました。 ところで、まだ、完全にCONNECT BYの使い方が理解できていませんので 大元の親を決める条件式で教えてください。 MIN_SCOREが同じものが複数ある場合、jamshid6 さんの書かれた CONNECT BY PRIOR MIN_SCORE<MIN_SCORE では、別のグループになってしまう気がします。 といって、 CONNECT BY PRIOR MIN_SCORE<=MIN_SCORE では、大元の親が複数できてしまい、これもグループ分けが失敗する気がします。 ここらは、CONNECT BY がどういう処理をしていて、この課題にはどう考えるべきなのでしょうか?

  • jamshid6
  • ベストアンサー率88% (591/669)
回答No.1

これって、LAGやLEADを使っても別に楽にはならないような気がするのですが。 むしろCONNECT BYを使った方がいいのでは?(バージョン書いてませんが10g以上なら) SELECT ROW_NUMBER() OVER (ORDER BY MIN(MIN_SCORE)) CLASS, MIN(MIN_SCORE) MIN_SCORE, MAX(MAX_SCORE) MAX_SCORE, COUNT(*) N FROM (SELECT CONNECT_BY_ROOT ID AS ROOTID,MIN_SCORE,MAX_SCORE FROM TABLE1 CONNECT BY PRIOR MIN_SCORE<MIN_SCORE AND PRIOR MAX_SCORE+50>=MIN_SCORE) WHERE ROOTID IN (SELECT ID FROM TABLE1 t WHERE NOT EXISTS (SELECT ID FROM TABLE1 WHERE MIN_SCORE<t.MIN_SCORE AND MAX_SCORE>=t.MIN_SCORE-50)) GROUP BY ROOTID

anahiro
質問者

補足

CONNECT BY は初めて知りました。 貴重な指摘ありがとうございます。 ただ、実際のデータ構造は先に出したテーブル構造より複雑で、もうひとつ別の分類尺度をもっており、例えば TABLE 1B のB_SOCREの値に範囲によりグループ分けしてから先の題意のグループ分けする必要があるのです。 今回の例の場合は、4 <= B_SOCRE <24 と 24<=B_SCORE<44 , 44<=B_SCORE <64・・毎にグループ分けするので、単純にCONNECT BY で分類するだけでは、うまくいきません。(今回は答えが同じようになるようにわざと4<= <24 にしましたが、実際は0から450ぐらいの範囲でばらばらです。) 私のイメージとして、・・・()OVER (PARTITION BY int((B_SCORE-4)/20 ORDER BY MIN_SCORE) といった形になるのかと思っており、LAG/LEADを使うとよいかと思っていました。 実は、実際はもっともっともっと複雑な問題なのですが、それだと説明が難しくなるため、質問時に課題を省略して説明しており、jamshid6さんにはせっかく回答していただきましたが、申し訳ありませんでした。 さらなるお知恵を拝借お願いします。 TABLE 1B ID MIN_SCORE MAX_SCORE B_SCORE 1 100 110 10 2 190 200 4 3 120 150 23 4 300 330 16 5 400 420 10 6 670 700 20 7 600 630 19

関連するQ&A

  • SQLのcount()とgroup by

    以下のようなテーブルがあったとき、 tbl1 id | score 1 | 100 1 | 200 1 | 300 2 | 200 2 | 100 3 | 300 3 | 400 3 | 500 3 | 600 idでまとめて個数を出すとすると、 SELECT count(*) FROM tbl1 group by id とすれば count(*) 3 2 4 と結果が返ってきますが、この結果にidを足して id | count(*) 1 | 3 2 | 2 3 | 4 というようなSQLはどうやって書けばいいのでしょうか? MySQLでは、 SELECT id, count(*) FROM tbl1 group by id と書けるのですが、これは標準的なSQLではNGなそうなので、気になって質問しました。 よろしくお願いします。

  • SQL GROUPで件数の一番多いレコードのみ取る

    SQL GROUPで件数の一番多いレコードのみ取る ORACLE10です。 次のようなテーブルがあります。 USER -----------テーブル名 ID,NAME,DATA----列名 1,A,any 1,A,any 1,B,any 2,C,any 3,D,any このテーブルから、IDをGROUPにして、さらに、一番使用頻度の多いNAMEを取得したいのです。 表示したいのは、IDとNAMEです。 つまり、取得結果例は次のようになります。 取得結果例 ID,NAME----列名 1,A 2,C 3,D SQL文をどのように作れば可能でしょうか? ちなみに、IDやNAMEのマスタテーブルはありません。 自分では、「(select ID, NAME, count(*) from USER group by ID, NAME) COUNT」で、COUNTを内部テーブルにして、グループ内で一番多いcount(*)をmaxで取得してみようかと思ったんですが、そこからアイデアが出ませんでした。

  • グループ化したいのですが

    テーブルAに以下のような項目があります。 [ID][SEI][MEI] やりたい事は、 ・[ID]ごとにグループ化して1行にまとめたい。 ・[SEI]と[MEI]をつなげて1つの項目として表示させたい。 これを実現させるには、どういうSQLを書けば良いでしょうか? 私が試したのは、以下のSQL文です。 SELECT ID,MIN(SEI) + MIN(MEI) AS NAME FROM TABLEA GROUP BY ID でも、これだと同じIDが複数存在すると[SEI][MEI]が別人のものがくっついてしまいます。当然ですが。 グループ化する前に[SEI][MEI]を連結させる処理が必要なのだろうと思うのですが、それをどうすれば良いのかが分かりません...。 どうかよろしくお願いします。m(_ _)m

  • 複数のテーブルを参照して値を変更する方法

    お世話になっております。 以下のようなテーブルがあったとして、table1,table2の値を参照して加算し、master_tableの値を変更するようなSQLは一つの文で書く方法はありますでしょうか? 例えば、あるSQLを実行すると、master_tableのid1のscoreが500になるようなSQLです。 どなたかご教授ください。 master_table id | score 1 | 0 2 | 0 table1 id | score 1 | 100 2 | 300 table2 id | score 1 | 400 2 | 100

    • ベストアンサー
    • MySQL
  • GROUP BYで集約されるときのソートを変えたい

    GROUP BYで集約されるときのソートを変えたい MySQLの5.1を使用しています。 テーブル"tbl"には、"no","id","score"の3つのフィールドがあり、 "no"は主キーです。 +---+----+-------+ | no | id | score | +---+----+-------+ | 1 | 10 | 10002 | | 2 | 10 | 10000 | | 3 | 10 | 10008 | | 4 | 11 | 10004 | | 5 | 12 | 10006 | +---+----+-------+ 上記の表から、 scoreの値が高い順にグループ化してソートしたいのですが、 以下のSQL文ではscoreが最初に登録されたものに集約されてからグループ化されてソートしてしまうため、 思い通りの結果が得られずに困っています。 ↓具体例 SELECT no, id, score FROM tbl ORDER BY score DESC GROUP BY id; +---+----+-------+ | no | id | score | +---+----+-------+ | 5 | 12 | 10006 | | 4 | 11 | 10004 | | 1 | 10 | 10002 | +---+----+-------+ ↓欲しいソート順 +---+----+-------+ | no | id | score | +---+----+-------+ | 3 | 10 | 10008 | | 5 | 12 | 10006 | | 4 | 11 | 10004 | +---+----+-------+ テンポラリテーブルはなるべく使用せずソートしたいのですが、 このような事は可能なのでしょうか? ご存知の方がいらっしゃいましたら教えていただけると助かります。 よろしくお願いします。

    • ベストアンサー
    • MySQL
  • グループ化したいのですが

    テーブルAに以下のような項目があります。 [MAIL_ID][SEND_DATE][USER_ID] テーブルBに以下のような項目があります。 [USER_ID][SEI][MEI] やりたい事は、 ・テーブルAとBを[USER_ID]をキーにして結合させたい。 ・テーブルAの[MAIL_ID]ごとにグループ化して1行にまとめたい。 ・テーブルBの[SEI]と[MEI]をつなげて1つの項目として表示させたい。 これを実現させるには、どういうSQLを書けば良いでしょうか? 私が試したのは、以下のSQL文です。 SELECT A.MAIL_ID, MIN(A.SEND_DATE) AS SEND_DATE,MIN( A.USER_ID) AS USER_ID,MIN(B.SEI) + MIN(B.MEI) AS ATESAKI FROM TABLEA A INNER JOIN TABLEB B ON A.USER_ID = B.USER_ID WHERE A.SEND_ID = '900001' GROUP BY A.MAIL_ID でも、これだと同じMAIL_IDが複数存在すると[SEI][MEI]が別人のものがくっついてしまいます。当然ですが。 グループ化する前に[SEI][MEI]を連結させる処理が必要なのだろうと思うのですが、それをどうすれば良いのかが分かりません...。 どうかよろしくお願いします。m(_ _)m

  • SQLを教えて下さい

    SQL文を教えて下さい 以下の2つのテーブルがあります。 TABLE1 --------------- ID,NAME,PRICE --------------- 1,部品A,100 2,部品B,200 3,部品C,300 TABLE2 --------------- ID,NAME,PRICE --------------- 1,部品Aのサブ1,1 1,部品Aのサブ2,2 2,部品Bのサブ1,5 2,部品Bのサブ2,5 --------------- SELECT TABLE1.NAME TABLE2.NAME TABLE1.PRICE TABLE2.PRICE WHERE TABLE1.ID = TABLE2.ID(+) TABLE2.PRICE = (SELECT MAX(?) ?) 期待する結果 ・TABLE1.NAME = 部品Aの場合 --------------- 部品A,部品Aのサブ2,100,2 ・TABLE1.NAME = 部品Bの場合 --------------- 部品B,部品Bのサブ1,200,5 TABLE1.NAME = 部品Cの場合 --------------- 部品C,,300, ・TABLE1のNAMEに、TABLE2のNAMEが関連付く(付かないものもある) ・外部結合とMAX関数を用いたい。 ・TABLE2のPRICEが大きい方を取得(同一の場合は、どちらかを取得)

  • SQLについて質問します

    VB2010 とSQL Server2008 R2 で開発をしています。 担当者テーブル(table_tanto)と売上明細テーブル(table_uriage_meisai)があります。 各テーブルのカラムは以下です。 table_tanto ・tanto_id ・tanto_name table_uriage_meisai ・meisai_id ・tanto_id ・kingaku ・date ある期間の担当者別の売上実績を一覧で出力したいと考えています。 「table_uriage_meisai」の「date」で範囲指定したいと思います。 結果はこんな形を望んでいます。 担当者ID 担当者名 売上合計 0001   ○○○   ****** 0002   □□□      0 0003   △△△   ****** 0004   ●●●   ****** 2点質問があります。 売上の無かった担当者も含めてすべての担当者の一覧を出したいと考えています。 そのSQlがわからないのと、売上がなかった担当者はその列がNullになってしまうと思うんですが、後の計算で困ります。 Nullのときゼロを返すようにするにはどうしたらよいでしょうか。 以上2点について教えて下さい。 よろしくお願いします。

  • 差集合について

    MYSQL5.5について質問です。 Table1.idからTable2.idを引いて差集合を得たいとと思います。 例えば、 Table1.id 001 002 003 004 Tbale2.id 002 003 004 とあれば、最終的に欲しい結果は 001 のみです。 SQLを書くと下記のようなSQLになると思います。 mysql> SELECT table1.* FROM table1 -> LEFT JOIN table2 ON table1.id=table2.id -> WHERE table2.id IS NULL; しかし、これで差集合が得られるのかわかりません。 left joinして、table2 is nullにすれば、差集合に なるか教えて頂けますでしょうか? また、not existsで代用できるみたいですが、その場合、 どのようなSQLを書けばよいでしょうか?

  • SQL文を教えてください。

    特定の日付Xを指定した場合、 以下のような結果になるSQL文を教えてください。 よろしくお願いします。 1) 日付X = 2003-12-17 の場合 ID table_A_DATE table_B_DATE table_C_DATE ------------------------------------------------ 001 2003-12-10 2003-12-12 2003-12-14 002 2003-12-15 ---------- 2003-12-16 003 2003-12-06 2003-12-10 ---------- 004 2003-12-08 ---------- ---------- 2) 日付X = 2003-12-11 の場合 ID table_A_DATE table_B_DATE table_C_DATE ------------------------------------------------ 003 2003-12-06 2003-12-10 ---------- 004 2003-12-08 ---------- ---------- table A(テーブルAには全ユーザのデータがあります) ID DATE ------------------ 001 2003-12-10 002 2003-12-15 003 2003-12-06 004 2003-12-08 table B ID DATE ------------------ 001 2003-12-12 003 2003-12-10 table C ID DATE ------------------ 001 2003-12-14 002 2003-12-16