プログラミング言語SML#解説 4.1.0版
22 SQL式とコマンド

22.5 SELECTクエリ

SML#では, SML#では,SELECTクエリの各句を独立に部分的に定義し, それらを合成することで,SELECTクエリ全体を構築することができる. SELECTクエリを構成する句 sqlclauseには以下のものがある.

sqlclause ::= sqlSelectClause SELECT句
 | sqlFromClause FROM句
 | sqlWhereClause WHERE句
 | sqlOrderClause ORDER BY句
 | sqlOffsetClause OFFSET句
 | sqlLimitClause LIMIT句

各句の構文の詳細は本節で副節に分けて定義する.

SELECTクエリ全体はこれらの句およびGROUP BY句sqlGroupClauseを 組み合わせて構成される. その構文は以下の通りである.

sqlselect ::= sqlSelectClause SELECT句を持つSELECTクエリ
sqlFromClause
(sqlWhereClause)?
(sqlGroupClause)?
(sqlOrderClause)?
(sqlLimitClause)?
 | select ... ( exp ) SELECT句が埋め込まれるSELECTクエリ
sqlFromClause
(sqlWhereClause)?
(sqlGroupClause)?
(sqlOrderClause)?
(sqlLimitClause)?
 | select ... ( exp ) SELECTクエリ全体の埋め込み

最初の2つの構文がSELECTクエリを構成する. SELECTクエリは,SELECT句とFROM句を必ず含む. その他の句は任意である. 一部のRDBMSの実装(例えばPostgreSQLなど)では FROM句を持たないSELECTクエリが許されているが, SML#ではSELECTクエリは必ずFROM句を持たなければならない. 2つ目の構文冒頭の select ... (exp)については後述する.

SELECTクエリを構成する各句が以下の型を持つならば, SELECTクエリ全体は(τ, w) SQL.query型を持つ.

  • sqlFromClause(τ1, w) SQL.from型を持つ.

  • sqlWhereClauseは存在するならば (τ1 -> τ1, w) SQL.whr型を持つ.

  • GROUP BY句が存在するとき, τ1をグループ化した型τ2が計算される (22.5.4節参照). GROUP BY句がなければ,τ2=τ1である.

  • sqlSelectClauseは, (τ2, τ, w) SQL.select型を持つ.

  • sqlOrderClauseは存在するならば (τ -> τ, w) SQL.orderby型を持つ.

  • sqlLimitClauseは存在するならば (τ -> τ, w) SQL.limit型を持つ.

例えば,以下は完全なSELECTクエリの例である.

val q = fn db => _sql select #t.name as name, #t.age as age
                      from #db.employee as t
                      where #t.age >= 20

3つ目の構文 select...(exp)は, SELECTクエリを直接書く代わりに, SML#の式expを評価した結果得られた SELECTクエリを埋め込む. expSQL.query型でなければならない. 例えば,以下は上述の例のクエリqをサブクエリとして 他のクエリq2に埋め込む例である.

val q2 = fn db => _sql select #t.name as name
                       from (select...(q db)) as t
                       where #t.name like "%Taro%"

2つ目の構文および以下の副節に示す各句の構文の定義のとおり, 一部の例外を除き, 句名に続けて...(exp)と書くことで, SML#の式を評価した結果をその句とすることができる. 例えば, 上述の例と同等のSELECTクエリを, 以下のように, SELECT句とFROM句を独立に定義したのち, ...(exp)記法を用いて組み合わせることで 構築することができる.

val s = _sql select #t.name as name, #t.age as age
val f = fn db => _sql from #db.employee as t
val q = fn db => _sql select...(s) from...(f db)

これら ...(exp)記法に含まれる expは,SQL評価式ではなく, SML#の式である. このexpの中では, 通常のSML#の式と同様に, SQL予約語は予約語ではなく識別子と解釈される.

22.5.1 SELECT句

SELECT句sqlSelectClauseの構文は以下の通りである.

sqlSelectClause ::= select  (distinct_all)?  sqlSelectField, ,  sqlSelectField
distinct_all ::= distinct  | all
sqlSelectField ::= sqlexp (as lab)?

SELECT句は1つ以上のフィールド sqlSelectFieldからなる. 各フィールドはラベルlabを持つ. i番目のフィールド(最初のフィールドを1とする)の as labiが 省略されている場合, as iが指定されているとみなす. どのフィールドのラベルも他のフィールドのラベルと異なって いなければならない.

n個のフィールドを持つsqlSelectClauseの型は, そのi番目(1in)のフィールド sqlexpi as labiの SQL評価式 sqlexpiの型が (τ,w)τi のとき,

(τ, {lab1 : τ1, , labn : τn} list, w) SQL.query

である.

22.5.2 FROM句

FROM句sqlFromClauseの構文は以下の通りである.

sqlFromClause ::= from sqlTable , , sqlTable
 | from ... (exp)

最初の構文がFROM句を構築する. 2つ目の構文は式expの評価結果をFROM句として埋め込む. expSQL.from型でなければならない.

最初の構文が含むコンマは,SML#の組式のコンマと曖昧に なることがある. 例えば,

(1, _sql from #db.t1, #db.t2, #db.t3)

は,1とFROM句のペアなのか, あるいは2番目の要素を_sql from #db.t1とする4つ組なのか, 曖昧である. 前者を意図しているならば

(1, _sql (from #db.t1, #db.t2, #db.t3))

後者を意図しているならば

(1, _sql (from #db.t1), #db.t2, #db.t3)

のように,括弧を付けて書かなければならない. 22.2節で示した_sql式が書ける 位置の制限は, この曖昧さを回避するために導入されている. なお,

(_sql from #db.t1, #db.t2, #db.t3)

は,開き括弧の直後に_sqlが来ているため,全体がひとつのSQL構文で あると認識される.

sqlTableはテーブルを表す式であり,その構文は 以下の通りである.

sqlTable ::= #vid.lab テーブル参照
 | sqlTable as lab テーブルのラベル付け
 | ( (_sql)? sqlselect ) テーブルサブクエリ
 | sqlTable (inner)? join sqlTable on sqlexp テーブルの内部結合
 | sqlTable cross join sqlTable テーブルの直積
 | sqlTable natural join sqlTable テーブルの自然結合
 | (sqlTable)

ラベル付け構文について以下の規則がある.

  • ラベル付け構文は他のどのsqlTable構文よりも結合力が強い.

  • テーブル参照 #vid.labに 直接かかるラベル付け構文が無い場合, as labが補われ, テーブルと同名のラベルが付けられているものとみなす.

また,sqlTableの構文には以下の制限がある.

  • sqlFromClauseに現れる全ての as labは互いに異なっていなければならない. 従って,同じ名前のテーブルを2度以上参照するときは, それぞれの参照に別のラベルをasで付けなければならない.

  • sqlTable1 natural join sqlTable2sqlTable1およびsqlTable2は, 内部結合または直積であってはならない.

  • sqlTable as labsqlTableは内部結合および直積であってはならない.

n個の sqlTablei as labi1in)を持つ FROM句の型は, sqlTableiの型をτiとするとき,

({lab1 : τ1, , labn : τn} list, w) SQL.from

である.

sqlTableiの型は以下の通りである.

  • #vid.labの型は, SML#の変数vidの型が (τ, w) SQL.dbでかつ τがフィールドlab:τを持つレコード型のとき, τである.

  • sqlTable as labの型は sqlTableの型に等しい.

  • (sqlselect)の型は, sqlselectの型が(τ,w) SQL.queryのときτである.

  • sqlTable1 natural join sqlTable2の 型は,2つのテーブルを自然結合したレコード型である.

SML#は内部結合式および直積式の型を計算しない. そのため,内部結合および直積の結果に asで直接にラベル付けをすることはできない (構文上制限されている). 内部結合および直積の結果の型は FROM句全体の型であるレコード型として現れる.

FROM句は,概念上,テーブルを結合したひとつのテーブルを 計算する. FROM句の型に現れるレコード型 {lab1 : τ1, , labn : τn}は, このテーブルの各行における, ラベル付けされた成分を表している. 他の句に現れるSQLカラム参照式 #lab1.lab2lab1は, 各成分に付けられたこれらのラベルを指す.

22.5.3 WHERE句

WHERE句sqlWhereClauseの構文は以下の通りである.

sqlWhereClause ::= where sqlexp
 | where ... (exp)

最初の構文がWHERE句を構成する. sqlexpの型が (τ,w)SQL.bool3のとき, WHERE句の型は (τ -> τ, w) SQL.whrである.

2つ目の構文は式expの評価結果をWHERE句として埋め込む. expSQL.whr型でなければならない.

22.5.4 GROUP BY句

GROUP BY句sqlGroupClauseの構文は以下の通りである.

sqlGroupClause ::= group by sqlexp, ,  sqlexp  (having sqlexp)?
 | group by ()

他の句とは異なり, GROUP BY句には ...(exp)記法が存在せず,従って SELECTクエリから分離して構築することができない. GROUP BY句は,構文上,ひとつのSELECTクエリの一部として SELECT句およびFROM句と共に現れる.

FROM句の型を (τ, w) SQL.from型とすると, GROUP BY句にコンマ区切りで並べられている sqlexpiはそれぞれ, (τ,w)τi型でなければならない. GROUP BY句はこれらの式の評価結果の組をキーとして, FROM句が計算したテーブルを複数の行のグループに分割する. 行のグループのテーブルの型τは後述する方法で計算される.

GROUP BY句は高々ひとつのHAVING句を持ってもよい. HAVING句のSQL評価式は GROUP BY句の評価後に行のグループをフィルタするための条件式であり, 従ってその型は(τ,w)SQL.bool3である.

2つ目の構文 group by ()は, FROM句が計算したテーブル全体をひとつのグループとする ことを表す標準SQLの構文である. 伝統的な標準SQLでは, GROUP BY句を書かずにSELECT句で集約関数を用いると, そのクエリはテーブル全体を集約するものとみなされる. 例えば,

SELECT avg(e.age) FROM employee AS e

は,テーブル全体のe.ageの平均を求める正しいSQLクエリである. 一方,SML#では, テーブル全体を集約するクエリには group by ()を書かなければならない. 上述のSQLクエリは,SML#では以下のように書く.

fn db => _sql select avg(#e.age) from #db.employee as e group by ()

データベースサーバーには,SML#プログラムに書かれている通り,

SELECT avg(e.age) FROM employee AS e GROUP BY ()

が送信される.

GROUP BY句が計算する行のグループの型τは, おおよそ以下のようにして計算される.

  1. 1.

    グループ化する前の行の型を

    τ={k1:τ1kn:τn}

    とする. FROM句が出力するテーブルの型はτ listである.

  2. 2.

    行をグループ化すると, その型はτ list listとなる.

  3. 3.

    行のグループ {k1:τ1, , kn:τn} listを転置し, τt={k1:τ1 list, kn:τn list}とする.

手順3の転置を行うためには,カラム名の集合 k1,,knが静的に確定している必要がある. SML#は, カラム名の集合を, 同じクエリに構文上含まれるカラム参照式の集合から取得する. GROUP BY句にキーとして指定されているか, SELECT句などに含まれるグループを指すカラム参照式 #lab1.lab2それぞれについて, もしそのカラム参照式が GROUP BY句に書かれたキーのいずれかひとつに一致するならば, そのカラムを単一の値のカラムとしてGROUP BY句の結果の型に含める. そうでないならば, そのカラムは値のリストを持つカラムとなる. 参照されないカラムの型は, GROUP BY句の出力の型として計算されない.

行の集合の型の計算は,あくまで構文上の文脈に基づいて行われ, 変数の参照関係などを考慮しない. 例えば,

val q = fn db => _sql select #e.department, avg(#e.salary)
                      from #db.employee as e
                      group by #e.department

はGROUP BY句を持つ正しいクエリである一方, SELECT句を分離した以下の例では型エラーが発生する.

val s = _sql select #e.department, avg(#e.salary)
val q = fn db => _sql select...(s)
                      from #db.employee as e
                      group by #e.department

なぜなら,SELECT句がGROUP BY句と構文上同じSELECTクエリに現れる 最初の例では, GROUP BY句の出力の型に #e.department#e.salaryの両方が適切に含まれる一方, 2つ目の例では SELECT句がGROUP BY句を持つSELECTクエリの中に無いため, GROUP BY句の結果は全く参照されないものとして GROUP BY句の型が計算されるからである. 良い習慣として,group byを含むクエリを書くときは, select...(exp)記法を使わない ことが望ましい.

以下は,注意深い読者のための補足説明である. SELECT句を分離しても,もしそのSELECT句が GROUP BY句で指定したキーしか参照しないならば, 型エラーは発生しない. 例えば上述の例のsの定義を書き換えて avg(#e.salary)を削除し,

val s = _sql select #e.department
val q = fn db => _sql select...(s)
                      from #db.employee as e
                      group by #e.department

としたならば,型エラーは発生しない. なぜなら,#e.departmentはGROUP BY句で指定された キーであり,従ってSELECT句での参照の有無にかかわらず GROUP BY句の型に含まれるからである.

22.5.5 ORDER BY句

ORDER BY句sqlOrderClauseの構文は以下の通りである.

sqlOrderClause ::= order by sqlOrderKey, sqlOrderKey
 | order by ... ( exp )
sqlOrderKey ::= sqlexp (asc_desc)?
asc_desc ::= asc  | desc

最初の構文がORDER BY句を構築する. 2つ目の構文は式expの評価結果をORDER BY句として埋め込む. expSQL.orderby型でなければならない.

ORDER BY句は SELECT句が計算した結果のテーブルに対して 行の並び替えを行う. SELECT句の結果の型をτとすると, ORDER BY句に現れる各sqlexpi(τ,w)τi型であり, ORDER BY句全体の型は (τ -> τ, w) SQL.orderbyである.

ORDER BY句には, 後方互換性を持たない仕様変更や, ベンダー独自の拡張が存在する. SML#でORDER BY句にキーとして書けるのは, SELECT句が出力する結果のカラムを参照する任意の式である. ORDER BY句から SELECT句の結果のカラムを参照するには, テーブル名を持たないカラム参照式 #.labを用いる. 例えば以下のように書く.

fn db => _sql select #e.name as name, #.age as age
              from #db.employee as e
              order by #.age

多くのデータベースエンジンは, SELECT句の結果に含まれないカラムをキーとしてソートすることを 許している一方,そのようなカラム参照はSML#では型エラーとなる. 例えば,以下のクエリは型エラーである.

fn db => _sql select #e.name as name, #.age as age
              from #db.employee as e
              order by #e.department

22.5.6 OFFSET句またはLIMIT句

OFFSET句およびLIMIT句は, SELECT句の計算結果から指定範囲の行のみを取り出す. 標準SQLに定められているのがOFFSET句, ベンダーによる拡張がLIMIT句であり, これらは機能的に等価である. どちらも広く受け入れられているため, SML#ではこれら両方をサポートする.

sqlOffsetOrLimitClauseの構文を以下に示す.

sqlOffsetOrLimitClause ::= sqlOffsetClause  | sqlLimitClause
sqlOffsetClause ::= offset sqlatexp row˙rows  (sqlFetchClause)?
sqlFetchClause ::= fetch first˙next sqlatexp  row˙rows only
row_rows ::= row  | rows
first_next ::= first  | next
sqlLimitClause ::= limit sqlexp  (sqlLimitOffsetClause)?
 | limit all  (sqlLimitOffsetClause)?
sqlLimitOffsetClause ::= offset sqlexp

予約語offsetの使い方が LIMIT句およびOFFSET句でそれぞれ異なることに注意が必要である. LIMIT句はOFFSET副句を持ち,予約語limitから始まる. OFFSET句はFETCH副句を持ち,予約語offsetから始まる. これらの副句を混ぜて使うことはできない. また, OFFSET句に定数でない式を書く場合は,式が括弧で囲まれていなければならない.

これらの句に現れる sqlexpまたはsqlatexpの型は ({},w)𝚒𝚗𝚝である. 従って, これらの句の中にカラム参照式を書くことはできない.

いくつかのデータベースエンジンでは, 1つのクエリの中でこれらの句を複数指定したり, 句の順序を入れ替えたりすることを許している. SML#では,標準SQLに従い, これらの副句は主句の後に続いて現れなければならない. また,OFFSET句およびLIMIT句はどちらも高々1つまでしか書けない.

22.5.7 相関サブクエリ

SELECTクエリがネストしているとき, 内側のSELECTクエリは外側のクエリのサブクエリである. 相関サブクエリとは, 外側のクエリのFROM句が導入するカラムを参照するサブクエリを言う. 以下は,標準SQLで書かれた相関サブクエリの例である.

SELECT e.department AS department, e.name AS name
FROM empoloyee AS e
WHERE e.salary > (SELECT avg(#t.salary)
                  FROM employee as t
                  WHERE t.department = e.department
                  GROUP BY ())

SML#では, サブクエリを SELECT句などsqlexpが書ける場所 (22.4.8節参照)と FROM句(22.5.2節参照)に 書くことができる. サブクエリは,以下の構文上の制約を満たすとき, 相関サブクエリであってもよい.

  1. 1.

    そのサブクエリおよび構文上そのサブクエリを囲む全てのSELECTクエリの FROM句がfrom...(exp)の形でないとき (22.5節参照), そのサブクエリは相関サブクエリであってもよい.

以下は,上述の標準SQLで書いた相関サブクエリをSML#で書いた 例である.

fn db => _sql select #e.department as department, #e.name as name
              from #db.empoloyee as e
              where #e.salary > (select avg(#t.salary)
                                 from #db.employee as t
                                 where #t.department = #e.department
                                 group by ())

ネストしたSELECTクエリのいずれかのFROM句が from...(exp)の場合, サブクエリは相関サブクエリと解釈されない. 例えば以下のように,上述の例のサブクエリのFROM句を from...(exp)に書き直したとき,

let
  val f = fn db => _sql from #db.employee as t
in
  fn db => _sql select #e.department as department, #e.name as name
                from #db.empoloyee as e
                where #e.salary > (select avg(#t.salary)
                                   from...(f db)
                                   where #t.department = #e.department
                                   group by ())
end

サブクエリの#e.departmenteは, 外側のfrom #db.employee as eeではなく, from ...(x db)が導入するeを参照する,と解釈される. 従って,x dbeを束縛するFROM句でなくてはならず, この例は型エラーとなる. 外側のFROM句をfrom...(exp)にした場合も,

let
  val f = fn db => _sql from #db.empoloyee as e
in
  fn db => _sql select #e.department as department, #e.name as name
                from...(f db)
                where #e.salary > (select avg(#t.salary)
                                   from #db.employee as t
                                   where #t.department = #e.department
                                   group by ())

サブクエリは相関サブクエリとみなされず, サブクエリの#e.departmenteは未定義となり, 型エラーとなる.

GROUP BY句と相関サブクエリは,期待される通りに組み合わせることができる. 例えば,以下は, 各部署の平均給料を上回る給料をもらっている最年少の人の年齢を問い合わせる クエリである.

fn db => _sql
  select #e.department, (select min(#t.age)
                         from #db.employee as t
                         where (#t.department = #e.department
                                and (Some) #t.salary > min(#e.salary))
                         group by ())
  from #db.employee as e
  group by #e.department

このクエリでは,外側のGROUP BY句でグループ化したカラム #e.salaryを,サブクエリの中でmin関数を用いて集約している. GROUP BY句の型は構文上の文脈から計算されるが (22.5.4節参照), SML#コンパイラはグループが相関サブクエリからも参照されている ことを正しく認識する.

以下は,注意深い読者のための補足説明である. 相関サブクエリは,構文上,他のクエリの内側に なければならないが, だからといって,相関サブクエリ式を評価した結果得られるSQL評価式が, MLの静的スコープのように構文上ネストするクエリとだけ相関する, というわけではない. 相関サブクエリが書ける場所の制約は, あくまで相関サブクエリの型を計算できるようにするための規則に過ぎない. 相関サブクエリを含むSQL評価式が第一級市民である以上, SML#の言語機能を駆使して,あるクエリAの内側で作った 相関サブクエリBを取り出し別のクエリCに埋め込むことも, 型さえ合っているならば可能である. このとき,Cに埋め込まれたBが参照する外側のテーブルは, Aのものではなく,Cのものである. SQLクエリがどのような経緯で組み立てられたとしても, その組み立て操作の型が正しいならば, 結果として作られるSQLクエリは正しい.