はじめに
本稿では、MySQL 8.0の新機能であるWindow関数について解説します。前半は、学習効率が良いと思われる順序で、Window関数の各機能をイメージを交えて説明します。後半は、PostgreSQLとOracleのWindow関数の機能をMySQL 8.0で模倣する方法を解説します。
対象読者
- MySQLのWindow関数の理解を深めたい方
必要な環境
本稿で扱うSQLは、MySQL 8.0.15で動作確認しました。
Window関数とは
Window関数は、select句とorder by句で使うことができます。とはいえ、order by句で使うことは、ほとんどありません。
Window関数のメリット
自己結合や相関サブクエリを使ったり、親言語(PHPやRubyなど)やストアドプロシージャで求めていた結果を、SQLで容易に求めることができるようになります。帳票作成やデータ分析において、特に使います。
MySQL 8.0のWindow関数の使用頻度
使用頻度の高いものからおさえていくと学習効率が良いです。
最頻出 | count max min sum Row_Number |
頻出 | Lag Lead Rank dense_rank |
たまに | avg First_Value Last_Value nth_Value JSON_ArrayAgg |
レア | NTile |
本稿では、以下の機能を順に、サンプルとともに紹介していきます。
- Select文の件数取得
- 最大値の行の取得
- 順位を付ける
- 最大値の行の取得(ソートキーが複数)
- 前後の行の値
- 累計
- 移動累計
- First_ValueとLast_Valueとnth_Value
- 全称肯定、全称否定、存在肯定、存在否定
- 最頻値(モード)
- Range指定で2分前の値を求める
- PostgreSQLのstring_aggを模倣
- PostgreSQLのgroups指定を模倣
- OracleのCountでのdistinct指定を模倣
- Oracleのnth_Valueでのfrom Lastを模倣
- OracleのLast_Valueでのignore nullsを模倣(entire)
- OracleのLast_Valueでのignore nullsを模倣(until)
- OracleのLag,Leadでのignore nullsを模倣
1. Select文の件数取得
Select文の結果の件数がほしいことは、よくあります。そんな時に使うのが、Window関数のcount関数です。以下のように使います。
create table CountT(ID int,Val int); insert into CountT values(1,10), (1,20), (2,10), (2,30), (2,50);
select ID,Val, count(*) over() as recordCount from CountT; +------+------+-------------+ | ID | Val | recordCount | +------+------+-------------+ | 1 | 10 | 5 | | 1 | 20 | 5 | | 2 | 10 | 5 | | 2 | 30 | 5 | | 2 | 50 | 5 | +------+------+-------------+
select ID,Val, (select count(*) from CountT) as recordCount from CountT;
脳内のイメージは、このようになります
where句やgroup by句、having句があっても、select文の結果の件数が求まります。
select ID,Val, count(*) over() as recordCount from CountT where Val in(10,20);
select ID,max(Val) as maxVal, count(*) over() as recordCount from CountT group by ID having max(Val) = 20;
distinct指定、もしくは、Limit句があるselect文の結果の件数を求めるには、インラインビューを使えば良いです。MySQLのselect文は、下記の順序で動作するからです。
- from句
- where句
- group by句
- having句
- select句
- order by句
- distinct
- Limit句
create table disT(ColA int,ColB int); insert into disT values(1,null), (1, 3), (1, 3), (2,null), (2,null);
select ColA,ColB,count(*) over() as recordCount from (select distinct ColA,ColB from disT order by ColA,ColB Limit 2) tmp order by ColA,ColB;
2. 最大値の行の取得
IDごとに、Valが最大値の行を取得します。サンプルは以下の通りです。
create table MaxT(ID int,Val int,extraCol char(1)); insert into MaxT values(1,10,'A'), (1,20,'B'), (2,10,'C'), (2,30,'D'), (2,50,'E'), (3,70,'F'), (3,70,'G');
select ID,max(Val) from MaxT group by ID;
select ID,Val,extraCol from (select ID,Val,extraCol, max(Val) over(partition by ID) as maxVal from MaxT) tmp where Val = maxVal; +------+------+----------+ | ID | Val | extraCol | +------+------+----------+ | 1 | 20 | B | | 2 | 50 | E | | 3 | 70 | F | | 3 | 70 | G | +------+------+----------+
select ID,Val,extraCol from MaxT a where Val = (select max(b.Val) from MaxT b where b.ID = a.ID);
Window関数が使えるのは、select句かorder by句です。なので、Window関数の結果をwhere句で使うには、インラインビューを使う必要があります。
脳内のイメージは、このようになります。partition byで、脳内で赤線を引くと分かりやすいです。
3. 順位を付ける
順位や連番を付けたい時に使うのが、Window関数のRow_Number関数、rank関数、dense_rank関数です。以下のようになります。
create table rnT(ID int,score int); insert into rnT values(1,100), (1,100), (1, 90), (1, 80), (2,100), (2, 70), (2, 70);
select ID,score, Row_Number() over(partition by ID order by score desc) as "Row_Number", rank() over(partition by ID order by score desc) as "rank", dense_rank() over(partition by ID order by score desc) as "dense_rank" from rnT order by ID,score desc; +------+-------+------------+------+------------+ | ID | score | Row_Number | rank | dense_rank | +------+-------+------------+------+------------+ | 1 | 100 | 1 | 1 | 1 | | 1 | 100 | 2 | 1 | 1 | | 1 | 90 | 3 | 3 | 2 | | 1 | 80 | 4 | 4 | 3 | | 2 | 100 | 1 | 1 | 1 | | 2 | 70 | 2 | 2 | 2 | | 2 | 70 | 3 | 2 | 2 | +------+-------+------------+------+------------+
Row_Number関数は、1から始まって、必ず連番になります。rank関数は、同点があると順位が飛びます。dense_rank関数は、同点があっても順位が飛びません。denseは、「密集した」という意味です。
select ID,score, (select count(*)+1 from rnT b where b.ID = a.ID and b.score > a.score) as "rank", (select count(distinct b.score)+1 from rnT b where b.ID = a.ID and b.score > a.score) as "dense_rank" from rnT a order by ID,"rank";
脳内のイメージは、このようになります。partition byで、脳内で赤線を引くと分かりやすいです。
4. 最大値の行の取得(ソートキーが複数)
「2. 最大値の行の取得」では、ソートキーが1つだったので、Window関数のmax関数を使いましたが、ソートキーが複数になると、順位を付ける関数を使う必要があります。
IDごとで、SortKey1が最大の行の中で、SortKey2が最大の行を取得するには、以下のようになります。
create table multiSortKeyT(ID int,SortKey1 int,SortKey2 int,extraCol char(3)); insert into multiSortKeyT values(1,10, 2,'AAA'), (1,10, 3,'BBB'), (1,30, 1,'CCC'), (2,20, 1,'DDD'), (2,50, 2,'EEE'), (2,50, 2,'FFF'), (3,60, 1,'GGG'), (3,60, 2,'HHH'), (3,60, 3,'III'), (4,10,20,'JJJ');
select ID,SortKey1,SortKey2,extraCol from (select ID,SortKey1,SortKey2,extraCol, rank() over(partition by ID order by SortKey1 desc,SortKey2 desc) as rn from multiSortKeyT) a where rn = 1 order by ID,extraCol; +------+----------+----------+----------+ | ID | SortKey1 | SortKey2 | extraCol | +------+----------+----------+----------+ | 1 | 30 | 1 | CCC | | 2 | 50 | 2 | EEE | | 2 | 50 | 2 | FFF | | 3 | 60 | 3 | III | | 4 | 10 | 20 | JJJ | +------+----------+----------+----------+
select ID,SortKey1,SortKey2,extraCol from multiSortKeyT a where (SortKey1,SortKey2) = (select b.SortKey1,b.SortKey2 from multiSortKeyT b where b.ID = a.ID order by b.SortKey1 desc,b.SortKey2 desc Limit 1) order by ID,extraCol;
select ID,SortKey1,SortKey2,extraCol from multiSortKeyT a where not exists(select 1 from multiSortKeyT b where b.ID = a.ID and (b.SortKey1 > a.SortKey1 or b.SortKey1 = a.SortKey1 and b.SortKey2 > a.SortKey2)) order by ID,extraCol;
脳内のイメージは、このようになります。partition byで、脳内で赤線を引くと分かりやすいです。