はじめに
データベースを利用する目的の1つとして,過去の情報を時系列順に,文字通り「データの集積」として保持する用途があります。そうした情報は,たとえばシステムのパフォーマンスログや会社の財務状況,あるいは集団の人口推移だったりするでしょう。多くの業務では,こうした過去のデータへ遡って現在と比較する,あるいは過去の2点や期間同士を比較して将来の指針を策定する手がかりとすることが重要になります。いわゆる「データウェアハウス」と総称される用途です。
リレーショナルデータベースとSQLにおいては,そうした時系列的なデータの多くは,「時間」を表す列(時刻,日付,年度などなど)をキーとして,異なる時刻のデータは異なる行として保持するのが一般的なテーブル設計です。これを「列持ち」の形で保持することは,例外的なケースに属します(注1)。
そうすると当然,ある2点の時刻におけるデータを比較するためには行間比較が必要となります。そのために必要な記述方法の基礎を学ぶことが本章の目的です。手続き型言語でファイルを扱う場合には,こういう行間比較のためには「ループ」による処理が基本となりますが,SQLには文単位のループは一切現れません。代わりにSQLは,独自の原理に基づいた方法を用います。
具体的には,標準SQLの新しい機能であるOLAP関数を利用する方法と,従来の自己結合(相関サブクエリ)を使う方法の2通りを紹介します。この両者を学ぶことには,SQLに存在する手続き型的な側面と集合指向的な側面を一挙に学ぶことができるというメリットがあります。
まあ,そうしたことは,本章を読み進む過程で追々わかってくるでしょう。今は,頭の片隅にメモ程度に覚えておいていただければ結構です。それでは,早速始めましょう。
- 注1)
- もし列持ちの形式を取るとすれば,固定的な結果を出力するためのテーブルとして使用する場合でしょう。しかし,列数を拡張することはコストがかさむため,これはあくまで例外的なケースですし,また本章で見るように,行持ちから列持ちの形式へ変換することが可能なため,基本的には行持ちの形式をとるべきです。
まずは基本
直近を求める
まずは基本的な時系列分析から始めましょう。時系列にデータを比較する場合,基本となるのは,時系列に従って,1行ずつ過去へ遡る,または未来へ進むSQLです。サンプルに,表1のようなサーバの時間ごとの負荷量を記録したテーブルを使います。サンプリングは思いついたときに不定期に行われるため,不連続で間隔もランダムな日付が格納されています。
表1 LoadSampleテーブル
| sample_date (計測日) | load(負荷量) |
|---|---|
| 2008-02-01 | 1024 |
| 2008-02-02 | 2366 |
| 2008-02-05 | 2366 |
| 2008-02-07 | 985 |
| 2008-02-08 | 780 |
| 2008-02-12 | 1000 |
まずは,各行について過去の直近の行を求めてみましょう。OLAP関数を使える実装ならば,リスト1のような簡潔な書き方で実現できます(実行結果は図1:注2)。
リスト1 過去の直近を求める(OLAP関数:現在のところ実装依存)
SELECT sample_date AS cur_date,
MIN(sample_date)
OVER (ORDER BY sample_date ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest
FROM LoadSample;
図1 リスト1の実行結果
cur_date latest -------- -------- 08-02-01 08-02-02 08-02-01 08-02-05 08-02-02 08-02-07 08-02-05 08-02-08 08-02-07 08-02-12 08-02-08
2月1日より前のデータはこのテーブルには登録されていないので,2月1日の行については直前の日付はNULLです。2月2日以降についてはそれぞれ直前の日付が存在するので,これがlatest列に入ることになります。このクエリのポイントは,「BETWEEN 1 PRECEDING AND 1 PRECEDING」によって,OLAP関数が動く範囲をあくまでsample_dateでソートした場合の直前の1行に限定していることです。普通,「BETWEEN」というのは,複数行の範囲を指定するために使う場合が多いのですが,ここはあえて範囲を1行に限定するために利用しています。
これを実装非依存のクエリにするには,OLAP関数の部分を相関サブクエリに書き換えます(リスト2)。
リスト2 直近(相関サブクエリ:実装非依存)
SELECT LS0.sample_date AS cur_date,
(SELECT MAX(sample_date)
FROM LoadSample LS1
WHERE LS1.sample_date < LS0.sample_date) AS latest
FROM LoadSample LS0;
これも結果はリスト1と同じです。ポイントはWHERE句の「LS1.sample_date < LS0.sample_date」という非等値結合です。この意味は,カレント行(LS0)のsample_dateより小さい日付の集合(LS1)から,最大の日付を選択する,ということです。このクエリの意味がわかりづらいなら,相関サブクエリを自己結合に変えたものを見るとわかりやすくなるでしょう(リスト3)。
リスト3 直近(自己結合:実装非依存)
SELECT LS0.sample_date AS cur_date,
MAX(LS1.sample_date) AS latest
FROM LoadSample LS0
LEFT OUTER JOIN LoadSample LS1
ON LS1.sample_date < LS0.sample_date
GROUP BY LS0.sample_date;
- 注2)
- Oracleには,これをもっと簡略化したLAGという関数があります(未来へ進むならLEAD関数)。独自拡張の関数のためここでは紹介しませんが,Oracleユーザの方は使い方を試してみてください。

