ソリューションを段階的に開発し、各変換をビューに分解します。これは、何が行われているのかを説明するのに役立ち、デバッグとテストにも役立ちます。これは基本的に、機能分解の原則をデータベースクエリに適用しています。
また、Oracle拡張機能を使用せずに、最新のRBDMSで実行する必要のあるSQLを使用してこれを実行します。したがって、キープ、オーバー、パーティションはなく、サブクエリとグループバイだけです。 (RDBMSで機能しない場合は、コメントでお知らせください。)
まず、テーブル。私は創造性がないので、month_valueと呼びます。 IDは実際には一意のIDではないため、「eid」と呼びます。他の列は「m」onth、「y」ear、および「v」alueです:
create table month_value(
eid int not null, m int, y int, v int );
データを挿入した後、2つのeidについて、次のようになります。
> select * from month_value;
+-----+------+------+------+
| eid | m | y | v |
+-----+------+------+------+
| 100 | 1 | 2008 | 80 |
| 100 | 2 | 2008 | 80 |
| 100 | 3 | 2008 | 90 |
| 100 | 4 | 2008 | 80 |
| 200 | 1 | 2008 | 80 |
| 200 | 2 | 2008 | 80 |
| 200 | 3 | 2008 | 90 |
| 200 | 4 | 2008 | 80 |
+-----+------+------+------+
8 rows in set (0.00 sec)
次に、2つの変数として表される1つのエンティティ、月があります。これは実際には1つの列(日付または日時、あるいは日付のテーブルへの外部キー)である必要があるため、1つの列にします。これを線形変換として実行し、(y、m)と同じようにソートし、(y、m)タプルには唯一の値があり、すべての値が連続するようにします。
>> create view cm_abs_month as
select *, y * 12 + m as am from month_value;
それは私たちに与えます:
> select * from cm_abs_month;
+-----+------+------+------+-------+
| eid | m | y | v | am |
+-----+------+------+------+-------+
| 100 | 1 | 2008 | 80 | 24097 |
| 100 | 2 | 2008 | 80 | 24098 |
| 100 | 3 | 2008 | 90 | 24099 |
| 100 | 4 | 2008 | 80 | 24100 |
| 200 | 1 | 2008 | 80 | 24097 |
| 200 | 2 | 2008 | 80 | 24098 |
| 200 | 3 | 2008 | 90 | 24099 |
| 200 | 4 | 2008 | 80 | 24100 |
+-----+------+------+------+-------+
8 rows in set (0.00 sec)
次に、相関サブクエリで自己結合を使用して、各行について、値が変更される最も早い後続月を検索します。このビューは、作成した前のビューに基づいています:
> create view cm_last_am as
select a.*,
( select min(b.am) from cm_abs_month b
where b.eid = a.eid and b.am > a.am and b.v <> a.v)
as last_am
from cm_abs_month a;
> select * from cm_last_am;
+-----+------+------+------+-------+---------+
| eid | m | y | v | am | last_am |
+-----+------+------+------+-------+---------+
| 100 | 1 | 2008 | 80 | 24097 | 24099 |
| 100 | 2 | 2008 | 80 | 24098 | 24099 |
| 100 | 3 | 2008 | 90 | 24099 | 24100 |
| 100 | 4 | 2008 | 80 | 24100 | NULL |
| 200 | 1 | 2008 | 80 | 24097 | 24099 |
| 200 | 2 | 2008 | 80 | 24098 | 24099 |
| 200 | 3 | 2008 | 90 | 24099 | 24100 |
| 200 | 4 | 2008 | 80 | 24100 | NULL |
+-----+------+------+------+-------+---------+
8 rows in set (0.01 sec)
last_amは、値vが変更される最初の(最も早い)月(現在の行の月の後)の「絶対月」になります。テーブルにそのeidのそれ以降の月がない場合はnullです。
last_amは、vの変更(last_amで発生)に至るまでのすべての月で同じであるため、last_amとv(およびもちろんeid)でグループ化できます。どのグループでも、min(am)は絶対値です。 最初のの月 その値を持っていた連続した月:
> create view cm_result_data as
select eid, min(am) as am , last_am, v
from cm_last_am group by eid, last_am, v;
> select * from cm_result_data;
+-----+-------+---------+------+
| eid | am | last_am | v |
+-----+-------+---------+------+
| 100 | 24100 | NULL | 80 |
| 100 | 24097 | 24099 | 80 |
| 100 | 24099 | 24100 | 90 |
| 200 | 24100 | NULL | 80 |
| 200 | 24097 | 24099 | 80 |
| 200 | 24099 | 24100 | 90 |
+-----+-------+---------+------+
6 rows in set (0.00 sec)
これが必要な結果セットであるため、このビューはcm_result_dataと呼ばれます。欠けているのは、絶対月を(y、m)タプルに戻すものだけです。
そのためには、month_valueテーブルに参加します。
問題は2つだけです:1)前の月が必要 出力にlast_amがあり、2)データに来月がない場合はnullがあります。 OPの仕様を満たすには、1か月の範囲である必要があります。
編集:これらは実際には1か月よりも長い範囲である可能性がありますが、いずれの場合も、eidの最新の月を見つける必要があることを意味します。つまり:
(select max(am) from cm_abs_month d where d.eid = a.eid )
ビューは問題を分解するため、別のビューを追加することで、この「エンドキャップ」を1か月前に追加できますが、これを合体に挿入します。どちらが最も効率的かは、RDBMSがクエリを最適化する方法によって異なります。
1か月前に取得するには、(cm_result_data.last_am-1 =cm_abs_month.am)
に参加します。nullがある場合は常に、OPは「to」月を「from」月と同じにすることを望んでいるため、その上でcoalesceを使用します:coalesce(last_am、am)。 lastはnullを排除するため、結合は外部結合である必要はありません。
> select a.eid, b.m, b.y, c.m, c.y, a.v
from cm_result_data a
join cm_abs_month b
on ( a.eid = b.eid and a.am = b.am)
join cm_abs_month c
on ( a.eid = c.eid and
coalesce( a.last_am - 1,
(select max(am) from cm_abs_month d where d.eid = a.eid )
) = c.am)
order by 1, 3, 2, 5, 4;
+-----+------+------+------+------+------+
| eid | m | y | m | y | v |
+-----+------+------+------+------+------+
| 100 | 1 | 2008 | 2 | 2008 | 80 |
| 100 | 3 | 2008 | 3 | 2008 | 90 |
| 100 | 4 | 2008 | 4 | 2008 | 80 |
| 200 | 1 | 2008 | 2 | 2008 | 80 |
| 200 | 3 | 2008 | 3 | 2008 | 90 |
| 200 | 4 | 2008 | 4 | 2008 | 80 |
+-----+------+------+------+------+------+
参加し直すことで、OPが必要とする出力を取得します。
参加し直さなければならないというわけではありません。たまたま、absolute_month関数は双方向であるため、年を再計算して月をオフセットするだけです。
まず、「エンドキャップ」月の追加に注意しましょう:
> create or replace view cm_capped_result as
select eid, am,
coalesce(
last_am - 1,
(select max(b.am) from cm_abs_month b where b.eid = a.eid)
) as last_am, v
from cm_result_data a;
そして今、OPごとにフォーマットされたデータを取得します:
select eid,
( (am - 1) % 12 ) + 1 as sm,
floor( ( am - 1 ) / 12 ) as sy,
( (last_am - 1) % 12 ) + 1 as em,
floor( ( last_am - 1 ) / 12 ) as ey, v
from cm_capped_result
order by 1, 3, 2, 5, 4;
+-----+------+------+------+------+------+
| eid | sm | sy | em | ey | v |
+-----+------+------+------+------+------+
| 100 | 1 | 2008 | 2 | 2008 | 80 |
| 100 | 3 | 2008 | 3 | 2008 | 90 |
| 100 | 4 | 2008 | 4 | 2008 | 80 |
| 200 | 1 | 2008 | 2 | 2008 | 80 |
| 200 | 3 | 2008 | 3 | 2008 | 90 |
| 200 | 4 | 2008 | 4 | 2008 | 80 |
+-----+------+------+------+------+------+
そして、OPが必要とするデータがあります。すべてのSQLで実行され、任意のRDBMSで実行され、シンプルで理解しやすく、テストしやすいビューに分解されます。
再参加するのが良いですか、それとも再計算するのが良いですか?それは読者にお任せします(これはトリックの質問です)。
(RDBMSでビューのグループ化が許可されていない場合は、最初に参加してからグループ化するか、グループ化してから、相関するサブクエリを使用して月と年を取得する必要があります。これは読者の演習として残されています。)
ジョナサン・レフラーはコメントで尋ねます、
データにギャップがある場合(たとえば、値が80の2007-12のエントリがあり、2007-10のエントリはあるが、2007-11のエントリはない場合)、クエリはどうなりますか?質問はそこで何が起こるか明確ではありません。
ええと、あなたは正確に正しいです、OPは指定しません。おそらく、ギャップがないという(言及されていない)前提条件があります。要件がない場合は、そこにない可能性のあるものをコード化しようとすべきではありません。しかし、実際には、ギャップがあると「結合」戦略が失敗します。 「再計算」戦略は、これらの条件下では失敗しません。もっと言いたいのですが、それは私が上でほのめかしたトリックの質問のトリックを明らかにするでしょう。