WHAT'S NEW?
Loading...

SQLでちゃんとしたランキングを作る【2/3】

実に7ヶ月ぶりにこのシリーズを再開することにした。
前回なにをやったのか、というと、率直にリンク先を見ていただければ良いと思う。
SQLでちゃんとしたランキングを作る【1/3】

つまりseletしてorderしてlimitするだけでは正しいランキングがでない、ということだ。

幸いにも識者の方から色々アドバイスをもらい、俺なりに試してみた結果を書いてみようと思う。

まず、識者、つまりDupont氏から引用してもらったSQLがこれだ。
select (select count(distinct b.sal) from emp b where b.sal <= a.sal) as rnk, a.sal from emp a
個人的にSQLは改行を入れて見やすくする主義なので、以下のように適宜変更してみる。
select
  (
    select
      count(distinct b.sal)
    from
      emp b
    where
      b.sal <= a.sal
  ) as rnk, a.sal
from
  emp a
コードというものは、改行やインデントで適切にフォーマットしておくべきだと思っている。

しかしSQLに関してはまだ拘りはないので、だいたいいつもフォーマットは違う。統一しておいたほうが良い場合と、統一しないでも良い場合があるのだが、SQLは俺にとってまだ後者の方だ。



で、実はこのSQLはサンプルコードそのままなので、当然データベース名もテーブル名もフィールド名も違う。

既存データベースは以下のような内容になっている。

このデータを生成するには以下のSQLでインサートしてほしい。
CREATE TABLE IF NOT EXISTS `ranking_history` (
  `id` int(8) NOT NULL AUTO_INCREMENT COMMENT '主キー',
  `user_id` int(5) NOT NULL COMMENT 'ユーザID',
  `point` int(8) NOT NULL COMMENT 'ポイント',
  `created` date NOT NULL COMMENT '作成日',
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`,`point`,`created`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

INSERT INTO `ranking_history` (`id`, `user_id`, `point`, `created`) VALUES
(1, 1, 42, '2010-06-02'),
(2, 2, 26, '2010-06-02'),
(3, 3, 31, '2010-06-02'),
(4, 4, 26, '2010-06-02'),
(5, 5, 49, '2010-06-02'),
(6, 6, 12, '2010-06-02'),
(7, 7, 66, '2010-06-02'),
(8, 8, 31, '2010-06-02'),
(9, 9, 41, '2010-06-02'),
(10, 10, 49, '2010-06-02'),
(11, 11, 55, '2010-06-02'),
(12, 12, 19, '2010-06-02'),
(13, 13, 8, '2010-06-02'),
(14, 14, 17, '2010-06-02'),
(15, 15, 21, '2010-06-02'),
(16, 15, 51, '2010-06-02'),
(17, 16, 17, '2010-06-02'),
(18, 17, 21, '2010-06-02'),
(19, 18, 9, '2010-06-02'),
(20, 19, 26, '2010-06-02'),
(21, 20, 11, '2010-06-02'),
(22, 21, 21, '2010-06-02'),
(23, 21, 48, '2010-06-02'),
(24, 23, 30, '2010-06-02'),
(25, 24, 21, '2010-06-02'),
(26, 25, 20, '2010-06-02');
SQL自体あまり詳しい方ではないのだが、俺なりに解釈して、既存データベースに沿った形に変更してみた。
select
  (
    select
      count(distinct B.point)
    from
      ranking_history as B
    where
      B.point >= A.point
  ) as RANK,  A.point
from
  ranking_history as A
そして上記SQLを実行してみた結果が以下だ。
ん?なんだこれ?これは本当に俺が求めているデータなのか?一体全体どうなんだ?もしかして俺は大いなる勘違いをしてしまっているのではないか?

しかしそれはこの世の終わりではない。
とりあえずpointを昇順にしてみよう。
select
  (
    select
      count(distinct B.point)
    from
      ranking_history as B
    where
      B.point >= A.point
  ) as RANK,  A.point
from
  ranking_history as A
order by
 A.point desc
このSQLの結果は以下のようになる。
うむ、どうにかまともになりつつある事がわかるとおもう。
RANKは昇順になっていて、pointもちゃんと降順に鳴っている。

しかしまだいろいろ足りない部分があるではないか。これではただ昇順データと降順データが建に並んでいるだけに過ぎず、何をもって正しいランキングなのかなぞ、誰も証明できないではないか。

しかしそれはこの世の終わりではない。
まずは重複データと思われる部分を細かく分けるために、user_Idフィールドも表示させてみよう。
select
  (
    select
      count(distinct B.point)
    from
      ranking_history as B
    where
      B.point >= A.point
  ) as RANK, A.user_id, A.point
from
  ranking_history as A
order by
 A.point desc
内側のselectでA.user_idを追加しただけだ。
このSQLの結果は以下のようになる。
これで、今まで完全に重複していた8位の31ポイントなど、user_idが入ることで、しっかりと意味を持ったデータになった。
この時点で少々安心感が増した。

しかしここで1点考える。

現状、存在するすべてのデータを使っているため、ランキングが18位まで存在することになっている。
これを10位までにしたい場合どうするのか。

このSQLで単純にlimit句を使ってしまうと、単純に上から10個目、つまり8位で終わってしまう。
select
  (
    select
      count(distinct B.point)
    from
      ranking_history as B
    where
      B.point >= A.point
  ) as RANK, A.user_id, A.point
from
  ranking_history as A
order by
  A.point desc
limit
  10
これの結果が、以下のようになってしまう。
たしかにlimit 10がしっかり働いていて大安心!
って、そうじゃない。そうではないんだ。
  • 上から10個
ではなく
  • RANKが10以下
にしたいわけだ。

多くの人がそうなのかも知れないし、俺だけなのかも知れないが、安易にlimitで出来るじゃん!とか思っていると、とたんにこう言った次のステージへ進むドアが勝手に開いてしまい、吸い込まれるように先に行かざるをえないケースというのが発生する。
このタイミングでレガーディな抵抗をするか、イノベーティブに前進するか、だ。

俺は素直に従おうと思う。

まずはwhere句でRANKの範囲を指定してみる。

select
  (
    select
      count(distinct B.point)
    from
      ranking_history as B
    where
      B.point >= A.point
  ) as RANK, A.user_id, A.point
from
  ranking_history as A
where
  A.RANK <= 10
order by
  A.point desc
これを実行するとエラーが出る。
#1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'LIMIT 0, 30' at line 17
'LIMIT 0, 30'というのはphpMyAdminが勝手に付けるので無視するが、意味はこうだ。

貴様のSQLには文法上の誤りがある。
使っているMySQLのバージョンにあったマニュアルを一読されたし!

だ。相変わらず無粋且つ無礼なエラーメッセージである。正直、辟易である。

すでにしてもう意味を訳してみようなどと思わなくなるレベルまで達してしまっている。
もう少し、なんていうかこう、CakePHPのエラーまでじゃなくても、せめてphp位のエラーはだして欲しいと切に思う。

多分だけど、A.RANKがまずいような気がする。そして次回はそれをテストする。
まだまだ先は遠そうだが、出来るまでやるのが主義なので、頑張ろうと思う。
もし失敗してもまたやり直せば良い。それがこの世の終わりではないのだから。

続く。