SQL: SELECT文におけるWHERE句とHAVING句

September 3, 2010 – 6:33 pm

 随分まえになるが、「Webアクセス解析ツール、ひとまず完成」などアクセス解析ツール作成にかかわり一連のエントリを書き、このブログへのアクセス情報をデータベースとする方法について検討した。

加えて、本データベースを活用し、本ブログアクセスについて、Webページごとのアクセス数を集計する簡単なツールを作成していたところであったが、最近になって、このツールに不具合があり、その原因が、SELECT文のWEHERE句とHAVING句の使い方について正しく理解していないことに気づいた。

気づいた点について、備忘録としてメモしておいた。

ブログアクセス情報DBの構成: まず、本題に入る前に、このブログへのアクセス情報を記録しているデータベースの構造について触れておく。

このアクセス情報DBは二つのテーブル、accessとmypageの二つのテーブルから構成される。それぞれのテーブルの内容は以下のようなもの:

テーブルaccess:

mysql> describe access;
+---------------+--------------+------+-----+-------------------+-----------------------------+
| Field         | Type         | Null | Key | Default           | Extra                       |
+---------------+--------------+------+-----+-------------------+-----------------------------+
| id            | int(8)       | NO   | PRI | NULL              | auto_increment              | 
| time          | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | 
| visitor       | varchar(128) | NO   |     | NULL              |                             | 
| ip_address    | varchar(16)  | NO   |     | NULL              |                             | 
| uniqueness    | varchar(16)  | NO   |     | NULL              |                             | 
| reff_uri      | varchar(512) | NO   |     | NULL              |                             | 
| refferer      | varchar(128) | NO   |     | NULL              |                             | 
| search_engine | varchar(32)  | NO   |     | NULL              |                             | 
| keywords      | varchar(128) | NO   |     | NULL              |                             | 
| page_id       | int(8)       | YES  | MUL | NULL              |                             | 
| user_agent    | varchar(128) | NO   |     | NULL              |                             | 
+---------------+--------------+------+-----+-------------------+-----------------------------+
11 rows in set (0.00 sec)

テーブル mypage:

mysql> describe mypage;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int(8)       | NO   | PRI | NULL    | auto_increment | 
| page_visited | varchar(128) | NO   |     | NULL    |                | 
| title        | varchar(256) | NO   |     | NULL    |                | 
+--------------+--------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)

テーブルmypage のカラムpage_visited はテーブルaccessのカラムpage_idとリンクしている(Table作成時にしようしたSQL文access.sqlは「PHPによる自前のWebアクセス解析ツール作成を検討」に書いている)。

Webページごとのアクセス数を集計することがツールの目的。これを行うため、当初、次のようなSQL文を作成。これにより、テーブルaccessから1日間に各々のWebページへのアクセス数を集計しようとしている。

mysql> SELECT time, page_id, COUNT(*) AS num_access
    -> FROM access
    -> GROUP BY page_id
    -> HAVING time > SUBDATE( NOW(), INTERVAL 1 DAY )
    -> ORDER BY num_access DESC;

このSQL文の結果は、以下:

+---------------------+---------+------------+
| time                | page_id | num_access |
+---------------------+---------+------------+
| 2010-09-03 11:10:01 |     937 |          1 | 
+---------------------+---------+------------+
1 row in set (0.25 sec)

page_idが937のグループひとつのみが抽出されており、意図した結果ではないことは明瞭。ここで、

HAVING time > SUBDATE( NOW(),  INTERVAL 1DAY )

の部分はpage_id をキーとして抽出されたグループに対する抽出条件になっていることに気づいた。

本来、access のレコードそれぞれについて、アクセスタイムについて(1日間にアクセスされたレコードという適合条件を)適用し、そののちWeb ページの種類毎にグループ化する必要がある。この点を考慮して修正し、次のようなSQL文を作成した:

mysql> SELECT time, page_id, COUNT(*) AS num_access
    -> FROM access
    -> WHERE time > SUBDATE( NOW(), INTERVAL 1 DAY )
    -> GROUP BY page_id
    -> ORDER BY num_access DESC;

この結果は、

+---------------------+---------+------------+
| time                | page_id | num_access |
+---------------------+---------+------------+
| 2010-09-02 11:39:05 |      17 |         57 | 
| 2010-09-02 11:40:25 |      34 |         22 | 
| 2010-09-02 13:00:02 |     325 |         22 | 
| 2010-09-02 11:41:11 |       7 |         19 | 
| 2010-09-02 11:57:48 |      29 |         17 | 
| 2010-09-02 11:37:13 |     619 |         16 | 

 (中 略)

| 2010-09-02 18:54:23 |     715 |          1 | 
| 2010-09-02 11:35:11 |      26 |          1 | 
| 2010-09-02 16:41:14 |     298 |          1 | 
| 2010-09-03 10:25:19 |     843 |          1 | 
+---------------------+---------+------------+
159 rows in set (0.24 sec)

ここに見られるように、Web Pageごとのアクセス数(num_access)が求められており、ここで目的としたいるデータが抽出されている。当初のSQL文との違いはHAVING句に代え、WHERE句をもちいたところ、そしてその位置(順序)だ。

WHERE句とHAVING句の違い: SELECT文でフィルタとして用いているWHERE句とHAVING句の違いを明確にしておかないと、うえに示した最初のSELECT文のような間違いをすることになる。SQLの入門書「一晩で学ぶSQL」(おすすめの入門書、私は重宝している)の記述について、重要と思われる部分を以下に抜粋しておく:

 ・・・WHERE句は特定の行についての検索をすることができますが、特定のグループについての検索はできないからです。WHERE句はグループを認識できないのです。
 では、WHERE句の代わりに何を使用すればよいのでしょうか。SQLはこの目的のために別の句として、HAVING句を用意しています。・・・WHEREは行に対して働き、HAVINGはグループに対して働く・・。(p.74)

また、より一般的な話として、SELECT文の句について、

(1) SELECT
(2) FROM
(3) WHERE
(4) GROUP BY
(5) HAVING
(6) ORDER BY

の順番で使う必要があることも知っておくことも大切。

なんとも、まとまりのないメモになってしまったが、ま、備忘録としては役立つだろう。


  1. 1 Trackback(s)

  2. Sep 5, 2011: Yama's Memorandum » Blog Archive » 自宅サーバー構築・覚書きリスト

Post a Comment