group byの注意点

今日、こんな感じのテーブルを作って、毎晩日次売上を月次売上に足しこむバッチを走らせる、という仕事をした。

●店舗別日次売上
create table daily (
	store_id integer not null primary key,	-- 店舗ID
	store_group_id integer not null,	-- 店舗が所属する店舗グループID
	sales bigint not null			-- 今日の売上
);

●店舗別月次売上
create table monthly (
	store_id integer not null primary key,
	store_group_id integer not null,
	sales bigint not null
);

やり方を2つ思いついた。

  • full outer join でヨコに結合して足し算
  • union でタテ積みして集計

まず結合を使ってみる。

(1)ヨコに結合
select 
	coalesce(d.store_id, m.store_id),
	coalesce(d.store_group_id, m.store_group_id),
	coalesce(d.sales, 0) + coalesce(m.sales, 0) as sales
from daily d 
full outer join monthly m
on d.store_id=m.store_id

前提として、月半ばで開店または閉店する店舗がある(つまり「日次にあって月次にない店舗」「月次にあって日次にない店舗」がある)ので、full outer join以外の結合方法では欠落する店舗が発生する。なのでfull outer joinが必須。
outer joinしたので、うっかりnullを返さないよう全ての列にcoalesceを付加する必要がある。


coalesceの処置が面倒くさいので、group byを使ってみる。
こっちの方が見通しがいい。

(2)タテ積みして集計
select 
	store_id, 
	store_group_id,
	sum(sales) as sales
from (
	select * from daily
	union all
	select * from monthly
) dm
group by 
	store_id, 
	store_group_id

ところが。

タテ積みの方は仕様を満たしていない

ここで、店舗が所属するグループが月半ばで変更されることもある、と判明した。
また、月次サマリは最新のグループIDをその店舗の所属グループIDとして集計せよ、という仕様になった。
この時、(2)のクエリは仕様を満たしていない。
例えばstore_id=2の店舗の所属グループが、1から999に変わったとする。
正解はこうなのに

store_id store_group_id sales
1 1 500
2 999 600
3 2 600
4 2 100

結果はstore_id=2のレコードが2行になる。

store_id store_group_id sales
1 1 500
2 1 500
2 999 100
3 2 600
4 2 100

group byするなら、スカラサブクエリを使うしかないのかな。これはまた見通しが悪い...

select 
	store_id, 
	coalesce(
		(select store_group_id
		 from daily where store_id=dm.store_id),
		(select store_group_id
		 from monthly where store_id=dm.store_id)	
	),
	sum(sales) as sales
from (
	select * from daily
	union all
	select * from monthly
) dm
group by 
	store_id

わかったこと

結局何がわかったのだろう。

  • group byに並べる条件には、集計単位になる項目と、ただの見出しの項目がある。
  • ただの見出しの項目は、集計単位になる項目に従属して値が一意に決まる(「店舗IDが決まれば店舗グループIDが決まる」等)。
  • ただの見出しのつもりで並べたのに、実は集計単位に従属していない項目があったら、結果が狂ってしまう。

group byに指定してもしなくても結果表の行数が変わらない項目は、「見出しのつもりで並べた項目」だ。
それが常に結果表の行数を変えないのか=テストデータが都合のいい感じになってるだけじゃないのか、必ず確認しなくてはならない。