プログラミング
モダンなリレーショナルクエリ言語に求めるもの
Things I want in a modern relational query language (sporks.space)
要約
この記事は、SQLの強力さは認めつつも、その実装の古さや使いにくさがNoSQLの普及を招いた一因であると指摘しています。SQLから学び、より良い構文、パーサー、ユーザー定義型、そしてパターンマッチングなどを備えた新しいリレーショナルクエリ言語の必要性を論じています。これにより、開発者はリレーショナルデータをより効率的かつ安全に操作できるようになると期待されます。
全文翻訳
これは長年放置されていた非常に古いドラフトです。最近のAcadiaのような新しいクエリ言語に関する議論が、このドラフトを見直し、改訂し、公開するきっかけとなりました。NoSQLが普及した最大の原因の一つは、SQLがその背後にある考え方によって強力な言語であるにもかかわらず、しばしば不器用で古風な方法で実装されていることだと考えています。SQLから学んだ言語があれば、リレーショナルデータをプログラマーがより良く操作できるようになるでしょう。実世界の状況で私が dealt with してきたことや、より良いクエリ言語がどのように役立ったかについて、似たようなことを考えていきたいと思います。他に何ができるかについて、ぜひ議論したいです。参考までに、RDBMSに関する私の経験は主にMySQLとDb2ですが、SQLite、SQL Server、Oracle、Postgresも(習熟度の高い順に)それなりに使用してきました。
より良い構文
私は美学にはこだわりませんが、多くの人はそうではありません。プログラマーは幼児のようなもので、ブロッコリーではなくクラフトディナーを欲しがります。PL/Iに基づいた構文は1970年代のIBMの選択であり、今日では通用しないでしょう。人気が出るにつれて、そのような言語はおそらくCやPythonの美学を構文的に取り入れるでしょうが、おそらくMLやPrologの影響もあるでしょう(Rustが示しているように)。
より良い構文があれば、より良いパーサーも期待できます。私は特にMySQLのパーサーを嫌っています。構文の不条理さ(DELIMITERのようなもの)がない場合でも、問題がどこにあるのか、あるいはそれが何であるのかを実際には決して教えてくれません。しかし、従来のRDBMSの実装にはより良いSQLパーサーが存在します – Oracleは、何が期待されているかを伝えることで、驚くほどエラー報告が優れています。私が書く例はあくまで見せるためであり、私は何にも固執していませんし、どのような構文を要求するものでもありません。これらの例での私の影響は、おそらくF#(MLファミリー)、Erlang(Prolog風)、Elixir(ErlangとRuby風)から来ています。
関数型プログラミング言語が関数型プログラミングに敵対的でないこと
データがどのように欲しいかを記述するSQLの4GLの特性は、手でループ処理するのではなく、SQLの最も強力な武器です。これは、遅延評価のような多くの関数型プログラミングパラダイムに非常に近いものです – こんにちはHaskell。残念ながら、ほとんどのSQL方言の標準ライブラリはこの点ではやや貧弱です。1980年代の手続き型プログラム向けに最適化されています。ほとんどのSQL方言は、SQLの宣言的な性質に逆行する、本質的に手続き型であるストアドプロシージャをサポートするようになりました。これは、言語と標準ライブラリが容易にするスタイルを模倣するほとんどのユーザーSQLコードに反映され、カーソルなどのミュータブルな状態や関数よりも手続きを扱うことが多くなります。デフォルトが重要です。
より不透明でないクエリプランナー
4GLであることは、コンパイラとオプティマイザーがほとんどのコードを最適化する上で強力な点ですが、クエリをより高価にする間違いを犯しやすいです。しかし、SQL最適化の専門家でない限り、プランナーは謎めいています。(ここでも、MySQLの「explain」ツールがいかに悪いかについて特別言及します。)これは厳密にはPLT関連ではありませんが、現在のSQL実装における弱点であり、コンピュータ科学者は多くのことを学んできました。
より良いユーザー定義型
一部のRDBMSは、ユーザー定義データ型の指定のためにドメインの概念を提供していますが(SQL仕様のオプション部分です)、できることには制限がある場合があります(通常は範囲やチェックの糖衣構文です)。Postgresだけがそれをサポートしているように見えました。Oracleは最近サポートしたようですが(ただし、Postgresよりも柔軟性が高いようです)。残念ながら、私はどちらも十分に使いこなせていないため、実際にはどのように機能するかについてはあまり詳しくありません。しかし、ドメインはCoddのThe Relational Modelでカバーされており、これはRDBMSの基礎となるテキストです。PostgresのIngresにおける遺産を考慮すると、IngresはQUELに基づいており、QUELはSQLよりもCoddのRDBMSのビジョンに近かったため、Postgresがそれに従ったのも理にかなっています。
Sum型、判別和、パターンマッチング
ここでモダンな関数型プログラミング技術をどのように適用できるかを示すスキーマは、スタックフレーム情報を提供するこの関数です。文脈として、ここで言及されているオペレーティングシステムであるIBM iは、「Services」という傘の下でシステム管理のための多くのSQL関数を提供しています。これはDBAからシステム管理者に移行した人々がデバッグの際に非常に役立ちますが、残念ながら不器用です。なぜなら、実質的に互いに排他的な列の「グループ」があり、そのために多くのNULL可能なフィールドがあり、列挙型(enum)のような文字列フィールドがあるからです。これらの一部は単にスキーマ設計の悪さ(おそらく単一のテーブルで返さなければならないという事実によって助けられていないかもしれません – 複数のテーブルを返すことも興味深い方向性でしょう)ですが、文字列ベースの列挙型は列挙型として機能するテーブルに対する外部キー制約で修正できます。一部は、実装の言語表現力によるものです。
このアイデアを使用して、より冗長でエラーが発生しやすいクエリを改善する例を考えてみました。
// 簡単化のために多くのことを省略しています。例えば、displacementや追加のenumケース、アドホックなenumの定義(型外で宣言することも可能)などです//
// 各フレームタイプは、類似していますが、同一ではなく、異なる意味論または修飾子を持っています。
type MachineInterfaceInfo = {
ActivationGroup: long;
ASP: long;
Library: string;
}
// ここで文脈がない人のために、IBM iは複数のプログラムモデルをサポートしています:
// - Javaプログラム、ランタイムがシステムに特別な洞察を提供します
// - OPMプログラム、古い管理ランタイムプログラムABI
// - ILEプログラム、新しい管理ランタイムプログラムABI
// - AIXプログラム、システムコールエミュレーション経由
// - LIC、IBM iカーネル
// これらすべての種類のプログラムのスタックトレースを生成できます。一部のプログラムは、各タイプのフレームエントリを含むコールスタックを持つ場合があります。
type FrameType =
// 他のレコードタイプからフィールドを継承します。
| ILE { MachineInterfaceInfo | ServiceProgram: string; Module: string; }
| OPM { MachineInterfaceInfo | Program: string; }
| AIX { Bitness: enum(32 | 64); LibArchive: Option(string); Module: string, Syscall: bool; }
| Java { MethodType: enum(DirectExecution | Glue | Interp | JIT | MMI); ClassName: string; Signature: Option(string); }
table Frame = {
ThreadID: long;
FrameType: FrameType;
Function: Option(string);
}
function StackInfo(JobID: string) : Frame
// パターンマッチングでフィルタリングするSQLライクなselect。
select Function from StackInfo("1234/JOB/5678") where AIX { Bitness: 64 } = FrameType
// これはILEとOPMのFrameTypeを返します
select Function from StackInfo("1234/JOB/5678") where MachineInterfaceInfo { Library: "QSYS" } = FrameType
// これはAIXのFrameTypeを返します
select Function from StackInfo("1234/JOB/5678") where AIX { LibArchive: "libc.a" } = FrameType
// これはAIXのFrameTypeを返します
select Function from StackInfo("1234/JOB/5678") where AIX { LibArchive: None } = FrameType
// パターンマッチングを使用して情報を表示する関数。
function FrameFullySpecifiedProgramName(frame : Frame) : string =
match frame.FrameInfo with
| OPM { Program: program } -> program
| ILE { ServiceProgram: srvpgm, Module: module } -> "#{srvpgm}/#{module}"
| AIX { LibArchive: None, Module: module } -> module
| AIX { LibArchive: lib, Module: module } -> "#{lib}(#{module})"
| Java { ClassName: class } -> class
// すべての可能なタイプをマッチさせる必要があります。それ以外は_で破棄します。
| _ -> "?"
// パターンマッチングに基づくオーバーロードとデストラクチャリングを使用する関数。
function FrameJavaFunctionDef(frame : Frame { Java { Signature: None } = .FrameInfo }) : string = "#{frame.Function}()"
function FrameJavaFunctionDef(frame : Frame { Java { Signature: signature } = .FrameInfo }) : string = "#{frame.Function}(#{signature})"
// 非Javaフレームでこれを呼び出すとエラーになります。なぜなら、どのパターンもマッチしないからです。互いに排他的な列のセットを折りたたむことができれば、視覚化もはるかに容易になります。それらが、例えば、より大きな列の各行にサブ列として表示されたり、タイプごとに異なる文字列として表示されたりする場合、左右にスクロールする量が大幅に減ります。
外部キーが ma