【SQL】SQLで変化(増加/変化なし/減少)について考える

■ はじめに

SQLで変化(増加/変化なし/減少)について考える

目次

【1】ポイント
 1)過去データと比較するために自己結合する
 2)データ仕様上、歯抜けがあるかどうかを確認
【2】Oracle 11g
 例1:年間売上の変化(増加/変化なし/減少)
 例2:年間売上の変化(増加/変化なし/減少。歯抜けデータ)
【3】PostgreSQL
 例1:最新5年間の変化を表示
【4】おまけ:変化のHTMLでの表記

【1】ポイント

1)過去データと比較するために自己結合する

変化を調べるためには、過去データと比較する必要がある。
それをSQL文で実現するためには、過去データを同じ行で
扱う必要があるので、LEFT OUTER JOINで自己結合する

2)データ仕様上、歯抜けがあるかどうかを確認

2021->2020->2019っとデータが連続していれば
1)の結合条件は単純に「year - 1」とすればいいが
2021->2019(2020データなし)っとなっていた場合
結合条件が複雑になる。
 => データ仕様が不明であれば、歯抜けデータありとして
  進めるのも一つの手かと、、、

JOIN部分抜粋

...
FROM
 SALES S1 -- 現在のデータ用(S1)
LEFT OUTER JOIN
 SALES S2 -- 直近のデータ用(S2)
ON
 -- 【条件1】現在のデータ(S1)より前のデータ(S2)を取得
 S2.YEAR < S1.YEAR
 -- かつ
 AND
 -- 【条件2】以下の条件のデータが存在しない(取得条件として省く)
 NOT EXISTS(SELECT 1 FROM SALES S3
 -- 前のデータ(S2)より大きい かつ 現在のデータ(S1)より小さい
  WHERE S3.YEAR > S2.YEAR AND S3.YEAR < S1.YEAR)
...

/*
【条件2】において、
例えば S1=2003の場合、2002(直近), 2003, ... まで取得する。
【条件1】で「現在のデータ(S1)より前のデータ(S2)を取得」ので
2003~以降は弾かれて、結果的に、「2002(直近)」が残る。
*/

 【2】Oracle 11g

 使用テーブル

-- 年間売上
CREATE TABLE "SALES" 
(
  "YEAR" INT,
  "SALE" INT,
  PRIMARY KEY ("YEAR")
);

# DROP TABLE SALES;

データ

INSERT INTO SALES VALUES(2000, 1280);
INSERT INTO SALES VALUES(2001, 1220);
INSERT INTO SALES VALUES(2002, 1470);
INSERT INTO SALES VALUES(2003, 1310);
INSERT INTO SALES VALUES(2004, 1310);
INSERT INTO SALES VALUES(2005, 1920);
INSERT INTO SALES VALUES(2006, 1680);
INSERT INTO SALES VALUES(2007, 1790);
INSERT INTO SALES VALUES(2008, 2170);
INSERT INTO SALES VALUES(2009, 1810);
INSERT INTO SALES VALUES(2010, 1810);

例2用の追加データ(歯抜けデータ)

INSERT INTO SALES VALUES(2011, 1420);
INSERT INTO SALES VALUES(2013, 1720);
INSERT INTO SALES VALUES(2014, 1640);
INSERT INTO SALES VALUES(2017, 2310);
INSERT INTO SALES VALUES(2018, 2810);

確認用 select文

SELECT * FROM SALES;

YEAR    SALE
2000    1280
2001    1220
2002    1470
2003    1310
2004    1310
2005    1920
2006    1680
2007    1790
2008    2170
2009    1810
2010    1810

例2用の追加データ(歯抜けデータ)

SELECT * FROM SALES;

YEAR    SALE
2000    1280
2001    1220
2002    1470
2003    1310
2004    1310
2005    1920
2006    1680
2007    1790
2008    2170
2009    1810
2010    1810
2011    1420
2013    1720
2014    1640
2017    2310
2018    2810

例1:年間売上の変化(増加/変化なし/減少)

SELECT
 S1.YEAR AS YEAR,
 CASE WHEN S1.SALE = S2.SALE THEN ''
      WHEN S1.SALE > S2.SALE THEN ''
      WHEN S1.SALE < S2.SALE THEN ''
 ELSE '-' END AS UP_OR_DOWN,
 S1.SALE AS SALE,
 S2.SALE AS PREVIOUS_SALE,
 S1.SALE - S2.SALE AS DIFF
FROM
 SALES S1
LEFT OUTER JOIN
 SALES S2
ON
 S1.YEAR = S2.YEAR + 1
ORDER BY
 YEAR;

出力結果

YEAR UP_OR_DOWN SALE PREVIOUS_SALE DIFF
2000 -      1280 « NULL »      « NULL »
2001 ↓      1220 1280          -60
2002 ↑     1470 1220          250
2003 ↓     1310 1470          -160
2004 →     1310 1310          0
2005 ↑     1920 1310          610
2006 ↓     1680 1920          -240
2007 ↑     1790 1680          110
2008 ↑     2170 1790          380
2009 ↓     1810 2170          -360
2010 →     1810 1810          0

例2:年間売上の変化(増加/変化なし/減少。歯抜けデータ)

歯抜けデータが存在する場合、
例1のSQLでは対応できないので、代わりに以下のようになる

SQL

SELECT
 S1.YEAR AS YEAR,
 CASE WHEN S1.SALE = S2.SALE THEN ''
      WHEN S1.SALE > S2.SALE THEN ''
      WHEN S1.SALE < S2.SALE THEN ''
 ELSE '-' END AS UP_OR_DOWN,
 S1.SALE AS SALE,
 S2.SALE AS PREVIOUS_SALE,
 S1.SALE - S2.SALE AS DIFF
FROM
 SALES S1
LEFT OUTER JOIN
 SALES S2
ON
 S2.YEAR < S1.YEAR
 AND NOT EXISTS(SELECT 1 FROM SALES S3
  WHERE S3.YEAR > S2.YEAR AND S3.YEAR < S1.YEAR)
ORDER BY YEAR;

出力結果

YEAR UP_OR_DOWN SALE PREVIOUS_SALE DIFF
2000 -         1280 « NULL »       « NULL »
2001 ↓     1220 1280           -60
2002 ↑     1470 1220           250
2003 ↓     1310 1470           -160
2004 →     1310 1310           0
2005 ↑     1920 1310           610
2006 ↓     1680 1920           -240
2007 ↑     1790 1680           110
2008 ↑     2170 1790           380
2009 ↓     1810 2170           -360
2010 →     1810 1810           0
2011 ↓     1420 1810           -390
2013 ↑     1720 1420           300
2014 ↓     1640 1720           -80
2017 ↑     2310 1640           670
2018 ↑     2810 2310           500

【3】PostgreSQL

* 「【1】Oracle 11g」と全く変わらないSQL文で取得できた
* もう少し複雑なテーブルでの年間売上の変化を
 以下の関連記事の「例2:最新データによる年間売上の増減」に記載。

https://dk521123.hatenablog.com/entry/2012/07/26/013620

 使用テーブル

-- 年間売上
CREATE TABLE sales 
(
  year INT,
  sales INT,
  PRIMARY KEY ("year")
);

データ

INSERT INTO sales VALUES(2000, 1280);
INSERT INTO sales VALUES(2001, 1220);
INSERT INTO sales VALUES(2002, 1470);
INSERT INTO sales VALUES(2003, 1310);
INSERT INTO sales VALUES(2004, 1310);
INSERT INTO sales VALUES(2005, 1920);
INSERT INTO sales VALUES(2006, 1680);
INSERT INTO sales VALUES(2007, 1790);
INSERT INTO sales VALUES(2008, 2170);
INSERT INTO sales VALUES(2009, 1810);
INSERT INTO sales VALUES(2010, 1810);
INSERT INTO sales VALUES(2011, 1420);
INSERT INTO sales VALUES(2013, 1720);
INSERT INTO sales VALUES(2014, 1640);
INSERT INTO sales VALUES(2017, 2310);
INSERT INTO sales VALUES(2018, 2810);
INSERT INTO sales VALUES(2020, 2710);
INSERT INTO sales VALUES(2021, 2710);

例1:最新5年間の売上増減を表示

書き方1

SELECT
 s1.year AS year,
 CASE
   WHEN s1.sales = s2.sales THEN ''
   WHEN s1.sales > s2.sales THEN ''
   WHEN s1.sales < s2.sales THEN ''
   ELSE '-'
 END AS up_or_down,
 s1.sales AS sales,
 s2.sales AS previous_sales,
 s1.sales - s2.sales AS diff
FROM sales AS s1, sales AS s2
WHERE
 s2.year=(SELECT MAX(year) FROM sales AS s3
  WHERE s1.year > s3.year)
ORDER BY
 s1.year DESC
LIMIT 5;

書き方2

SELECT
 s1.year AS year,
 CASE WHEN s1.sales = s2.sales THEN ''
      WHEN s1.sales > s2.sales THEN ''
      WHEN s1.sales < s2.sales THEN ''
 ELSE '-' END AS up_or_down,
 s1.sales AS sales,
 s2.sales AS previous_sales,
 s1.sales - s2.sales AS diff
FROM
 sales AS s1
LEFT OUTER JOIN
 sales AS s2
ON
 s2.year = (SELECT MAX(year) FROM sales AS s3
  WHERE s1.year > s3.year)
ORDER BY
 s1.year DESC
LIMIT 5;

書き方3

SELECT
 S1.year AS year,
 CASE WHEN S1.sales = S2.sales THEN ''
      WHEN S1.sales > S2.sales THEN ''
      WHEN S1.sales < S2.sales THEN ''
 ELSE '-' END AS up_or_down,
 S1.sales AS sales,
 S2.sales AS previous_sales,
 S1.sales - S2.sales AS diff
FROM
 sales S1
LEFT OUTER JOIN
 sales S2
ON
 S2.year < S1.year
 AND NOT EXISTS(SELECT 1 FROM sales S3
  WHERE S3.year > S2.year AND S3.year < S1.year)
ORDER BY year DESC
LIMIT 5;

出力結果

year,up_or_down,sales,previous_sales,diff
2021,→,2710,2710,0
2020,↓,2710,2810,-100
2018,↑,2810,2310,500
2017,↑,2310,1640,670
2014,↓,1640,1720,-80

【4】おまけ:変化のHTMLでの表記

変化(増加/変化なし/減少)のHTMLでの表記は
以下の表の特殊文字を使うといいかも。

https://gray-code.com/html_css/list-of-symbols-and-special-characters/

記号 数値参照(10進数) 数値参照(16進数) 説明
&#8679; &#x21E7; 上向き二重矢印
&#8680; &#x21E8; 右向き二重矢印
&#8681; &#x21E9; 下向き二重矢印

参考文献

https://codezine.jp/article/detail/907
https://codezine.jp/article/detail/907?p=2

関連記事

サブクエリ / 副問合せ
https://dk521123.hatenablog.com/entry/2015/12/21/002727
相関サブクエリ / 自己相関サブクエリ
https://dk521123.hatenablog.com/entry/2016/01/23/230608
EXISTS / NOT EXISTS文 ~ 存在有無を確認 ~
https://dk521123.hatenablog.com/entry/2010/07/09/214044
過去直近データを取得するには
https://dk521123.hatenablog.com/entry/2016/01/05/234938
最新日付データを取得するには
https://dk521123.hatenablog.com/entry/2014/10/09/004105
期間が重なるデータを取得するには
https://dk521123.hatenablog.com/entry/2011/12/17/025502
順位付け・ランキング ~ RANK / DENSE_RANK ~
https://dk521123.hatenablog.com/entry/2012/08/15/225233
初めの数文字が一致してたら同一データと見なすSQLを考える
https://dk521123.hatenablog.com/entry/2018/04/11/225229
複数の集計を1回のSQL文で行う方法を考える
https://dk521123.hatenablog.com/entry/2018/04/12/210108
SQLで階層構造データを扱うことを考える
https://dk521123.hatenablog.com/entry/2013/02/21/235841
グループ内のMAXデータを取得する
https://dk521123.hatenablog.com/entry/2018/10/06/230841
SQLで変化(増加/変化なし/減少)について考える
https://dk521123.hatenablog.com/entry/2021/08/26/222043
促音・拗音を区別せず検索する
https://dk521123.hatenablog.com/entry/2012/11/09/010214
WITH句 ~ 共通テーブル式
https://dk521123.hatenablog.com/entry/2012/07/26/013620