この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
DI部の大矢です。こんにちは。
現在私は、Amazon Redshiftを使ったデータ分析システムの開発プロジェクトに参画しておりますが、
今回は、データ分析システムではよく出てくる「履歴テーブル」を扱うSQLについて書いてみます。
記載したSQLは、Amazon Redshift(1.0.1086)、PostgreSQL(9.5.3)で動作確認しました。
履歴テーブルとは
履歴テーブルとは、データを更新する際、元のレコードはそのまま残して、新たなレコードをinsertしていくようにしているテーブルです。 こうしておくと、過去の任意の時点でデータがどのような状態であったのか、調べることができます。
今回は以下のような「顧客テーブル」を例に使用します。
A社は社名を頻繁に変え、同時に引っ越しもしているようですね。
create table customer (
customer_id smallint
, customer_name varchar(256) --会社名
, address varchar(256) --住所
, updated_at timestamp --更新日時
);
customer_id | customer_name | address | updated_at
-------------+---------------+-------------+---------------------
1 | 有限会社A | 世田谷区... | 2012-04-01 12:34:56
1 | A株式会社 | 新宿区... | 2015-03-01 12:34:56
1 | A.com | 千代田区... | 2016-07-01 12:34:56
2 | B社 | 千葉市... | 2012-07-01 12:34:56
2 | B社 | 市川市... | 2016-02-01 12:34:56
3 | C社 | 名古屋市... | 2015-02-01 12:34:56
履歴テーブルから最新レコードを取得する
顧客テーブルから、customer_idごとの最新のレコードのみを抽出するクエリーは以下のようになります。
select customer_id, customer_name, address, updated_at from (
select
customer_id
, customer_name
, address
, updated_at
, row_number() over (
partition by customer_id
order by updated_at desc
) as row_num
from customer
) as r
where row_num = 1
customer_id | customer_name | address | updated_at
-------------+---------------+-------------+---------------------
1 | A.com | 千代田区... | 2016-07-01 12:34:56
2 | B社 | 市川市... | 2016-02-01 12:34:56
3 | C社 | 名古屋市... | 2015-02-01 12:34:56
ウィンドウ関数「row_number」を使って、customer_idごとに更新日時の降順でレコードに順位をつけ、1位のレコードのみに絞ります。
履歴テーブルから、ある時点のレコードを取得
最新ではなく、過去のある時点のレコードを取得する場合は、先ほどのクエリーにwhere句で更新日時の条件を加えることでできます。 例えば2015年初時点を条件とすると、以下のようになります。
select customer_id, customer_name, address, updated_at from (
select
customer_id
, customer_name
, address
, updated_at
, row_number() over (
partition by customer_id
order by updated_at desc
) as row_num
from customer
where updated_at < timestamp '2015-01-01 00:00:00'
) as r
where row_num = 1
customer_id | customer_name | address | updated_at
-------------+---------------+-------------+---------------------
1 | 有限会社A | 世田谷区... | 2012-04-01 12:34:56
2 | B社 | 千葉市... | 2012-07-01 12:34:56
別のテーブルとJOINする際、適切な履歴レコードとJOINさせる
以下のような「売上テーブル」があるとします。
create table sales (
sales_id smallint
, customer_id smallint
, sales_date date --売上日
, sales_amount bigint --売上額
);
sales_id | customer_id | sales_date | sales_amount
----------+-------------+------------+--------------
1 | 1 | 2014-03-15 | 5000000
2 | 1 | 2015-03-15 | 10000000
3 | 1 | 2016-03-15 | 20000000
4 | 2 | 2016-01-15 | 1000000
5 | 2 | 2016-01-15 | 2000000
6 | 3 | 2015-03-15 | 30000000
売上テーブルと顧客テーブルをJOINすることを考えます。
せっかく顧客テーブルが「履歴テーブル」なので、売上日の時点の会社名、住所を表示したいですね。
※前提として、売り上げを登録する前に顧客テーブルに顧客を登録している、顧客の会社名や住所が変更されたら即座に顧客テーブルに反映している、とします。
ということで、以下のようなクエリーを考えてみました。
select
sales_id
, sales_date
, sales_amount
, customer_name
, address
, updated_at
from (
select
*
, row_number() over (
partition by sales_id
order by updated_at desc
) as row_num
from (
select
s.sales_id
, s.sales_date
, s.sales_amount
, c.customer_id
, c.customer_name
, c.address
, c.updated_at
from sales as s
inner join customer as c
on s.customer_id = c.customer_id
and s.sales_date >= c.updated_at
) as s_c
) as r
where row_num = 1
sales_id | sales_date | sales_amount | customer_name | address | updated_at
----------+------------+--------------+---------------+-------------+---------------------
1 | 2014-03-15 | 5000000 | 有限会社A | 世田谷区... | 2012-04-01 12:34:56
2 | 2015-03-15 | 10000000 | A株式会社 | 新宿区... | 2015-03-01 12:34:56
3 | 2016-03-15 | 20000000 | A株式会社 | 新宿区... | 2015-03-01 12:34:56
4 | 2016-01-15 | 1000000 | B社 | 千葉市... | 2012-07-01 12:34:56
5 | 2016-01-15 | 2000000 | B社 | 千葉市... | 2012-07-01 12:34:56
6 | 2015-03-15 | 30000000 | C社 | 名古屋市... | 2015-02-01 12:34:56
一応できました。
解説しますと、一番下のサブクエリーで、売り上げテーブルと顧客テーブルをJOINしています。JOINの条件として、顧客レコードは更新日時が売上日時より前であることとしています。 こうすることで、売り上げ後の顧客レコードは排除できましたが、売上日よりも古いレコードが複数ある場合、まだ絞り切れてません。 一番下のサブクエリーだけを実行した結果は以下のようになります。
sales_id | sales_date | sales_amount | customer_id | customer_name | address | updated_at
----------+------------+--------------+-------------+---------------+-------------+---------------------
1 | 2014-03-15 | 5000000 | 1 | 有限会社A | 世田谷区... | 2012-04-01 12:34:56
2 | 2015-03-15 | 10000000 | 1 | A株式会社 | 新宿区... | 2015-03-01 12:34:56
2 | 2015-03-15 | 10000000 | 1 | 有限会社A | 世田谷区... | 2012-04-01 12:34:56
3 | 2016-03-15 | 20000000 | 1 | A株式会社 | 新宿区... | 2015-03-01 12:34:56
3 | 2016-03-15 | 20000000 | 1 | 有限会社A | 世田谷区... | 2012-04-01 12:34:56
4 | 2016-01-15 | 1000000 | 2 | B社 | 千葉市... | 2012-07-01 12:34:56
5 | 2016-01-15 | 2000000 | 2 | B社 | 千葉市... | 2012-07-01 12:34:56
6 | 2015-03-15 | 30000000 | 3 | C社 | 名古屋市... | 2015-02-01 12:34:56
このサブクエリーの結果を、row_number関数を使ってsales_idごとに更新日時の降順でレコードに順位をつけ、1位のみに絞ることで、ようやく望んだ結果が得られました。
まとめ
今回は、分析システムのDBでよく登場する「履歴テーブル」を扱うSQLを紹介しました。