プログラミング
11行のコードでより良いSQLを
A better SQL in 11 lines of code (prela-lang.org)
要約
UCLA RePLで開発中の新しいクエリ言語「Prela」は、SQLとは大きく異なるが、その核となるアイデアは非常にシンプルである。このチュートリアルでは、Prelaの主要原則を理解するためにPythonで簡易版を構築する。Prelaは、すべてのリレーション(テーブル)を2つの列のみを持つバイナリリレーションに分解し、関数合成のようにリレーションの合成(.select)や結合(&)、フィルタリング(.where, .eq)を行うことで、簡潔かつ強力なクエリを実現する。これにより、SQLで20行以上かかるクエリを11行程度で記述可能になる。
全文翻訳
PrelaはUCLA RePLで開発中の新しいクエリ言語です。この言語はSQLとは大きく異なりますが、その核となるアイデアは非常にシンプルです。この短いチュートリアルでは、Prelaの主要原則を理解するためにPythonで玩具版を構築します。このチュートリアルを終える頃には、以下のクエリがどのように機能するかを知っているでしょう:movie.where(company.s(country).eq("[us]") & keyword.eq("character-name-in-title")) .select(title & cast.s(person).s(alias).s(text)) おそらく、これが何をしているのかはすでに推測できるでしょう。このクエリは、アメリカの会社によって制作され、タイトルにキャラクター名が含まれるすべての映画を見つけ、タイトルと各キャストメンバーのエイリアスを出力します。SQLでの同等のクエリは20行以上に及ぶことに注意してください。Prelaの最初の特別な点は、バイナリリレーション、つまり2つの列を持つテーブルしかないことです。これは最初は非常に制限的であるように聞こえるかもしれませんが、複数の列を持つ広いテーブルを「バイナリ化」するのは簡単です。映画のテーブルがあると仮定しましょう:ID title year 646 The Godfather 1972 478 Seven Samurai 1954 583 Casablanca 1942 この3列のテーブルを、各行番号を列の値にマッピングする3つのバイナリリレーションに分解できます。movie = Rel([(646, 0), (478, 1), (583, 2)]) title = Rel([(0, "The Godfather"), (1, "Seven Samurai"), (2, "Casablanca")]) year = Rel([(0, 1972), (1, 1954), (2, 1942)]) ヒントこのチュートリアルでは、snipを使用してコードセルをノートブックのような環境に接続します。あるセルで行われた変更は、後のセルに反映されます。上記のmovie、title、yearリレーションは、それぞれ元のテーブルのID、タイトル、年の列を表します。titleとyearでは行番号が先に来ますが、movie(IDとも呼ばれていません)では後になることに注意してください。その理由は後で明らかになるでしょう。バイナリリレーションに焦点を当てる動機は、それらが関数を一般化することです。関数は、それらが合成できるため、プログラムのビルディングブロックであるため強力です。関数は各入力に一意の出力をマッピングしますが、リレーションは入力を複数の異なる出力にマッピングできます。ある意味で、リレーションは非決定性関数と見なすことができます。それはすべて非常に抽象的なので、例に戻りましょう。話を簡単にするために、各入力に正確に1つの出力をマッピングするリレーションに焦点を当てます。つまり、それらはすべて偶然関数です。リレーションを「呼び出す」ことは、そのリレーションを辞書に変換し、値をルックアップすることになります:print(dict(movie)[646], dict(title)[0], dict(year)[0]) ここで、Prelaの最初で最も重要な演算子であるリレーション合成を紹介する準備ができました。関数合成は、一方の関数を最初に適用し、次に他方の関数を出力に適用することによって機能します。2つのリレーションrとsの合成は、それ自体リレーションであり、まずxをrでマッピングしてあるyを得て、次にyをsでマッピングして最終的な「出力」を得ます。これは、sを辞書dに変換し、rの(x, y)ペアを反復処理し、yがdで見つかった場合に最終的に(x, d[y])を出力することによって実装できます:def select(r, s): d = dict(s) return [ (x, d[y]) for x, y in r if y in d ] 例を使用して、以下のクエリはmovieとtitleを合成して、各映画IDをそのタイトルにマッピングするリレーションを取得します:print(movie.select(title)) titleをyearに変更して何が得られるか試してみてください。合成の力は、複数の.select呼び出しを連鎖させるときに真に現れます。映画を制作した会社に各映画をマッピングする外部キー列と、映画会社のための別のテーブルを追加すると仮定しましょう:ID title year company ... ... ... 0 ... ... ... 1 ... ... ... 2 ID name country 0 Paramount [us] 1 Toho [jp] 2 Warner Bros. [us] 同じように分解すると、さらに4つのリレーションが得られます:company = Rel([(0, 0), (1, 1), (2, 2)]) id2row = Rel([(0, 0), (1, 1), (2, 2)]) name = Rel([(0, "Paramount"), (1, "Toho"), (2, "Warner Bros.")]) country = Rel([(0, "[us]"), (1, "[jp]"), (2, "[us]")]) 次に、映画の制作会社の国を見つけることができます。これは.select呼び出しの連鎖によって行われます。ここでは.sで省略します:print(movie.s(company).s(id2row).s(country)) これは、チュートリアルの冒頭のスニペットの最後の行にあったcast.s(person).s(alias).s(text)で起こったことと同じです。これまでのすべてのクエリは単一の列の値を返しました。複数の属性を選択するには、&演算子を導入します。.selectがrの2番目の列をsの1番目の列と一致させるのに対し、&は両方の最初の列でrとsを結合し、それらの2番目の列をペアにします:def and_(r, s): d = dict(s) return [ (x, (y, d[x])) for x, y in r if x in d ] したがって、title & yearは、各映画行を両方の属性を一度にマッピングします:print(title & year) 結果は依然としてバイナリリレーションであることに注意してください。&は単に値をタプルにネストします。つまり、他のリレーションと同様に合成を続けることができます。これがクエリが複数の列を返す方法です:print(movie.select(title & year)) 次に、どの行を選択したいかを指定する方法が必要です。.eq(v)述語はリレーションをフィルタリングし、2番目の列がvと等しいペアのみを保持します:def eq(r, v): return [ (x, y) for x, y in r if y == v ] 単独では、.eqはその適用対象のリレーションを狭めるだけです。以下のクエリは依然として映画行を国にマッピングしますが、すべてではなくなります:print(company.s(country).eq("[us]")) 最後に、制限演算子.whereは、上記の述語を取り、それを使用して別のリレーションをフィルタリングします。def where(r, s): d = dict(s) return [ (x, y) for x, y in r if y in d ] この述語を.whereに渡すと、映画のフィルターになります:print(movie.where(company.s(country).eq("[us]"))) これはコードからそのまま読み取れます:「会社の国が[us]である映画」。クエリが長くなってきたので、リファクタリングしましょう:american = company.s(country).eq("[us]") print(movie.where(american)) あれ、CTEをプレーンなPython変数で作成しましたか?はい!これは、Prelaクエリが演算子で構成されており、すべてのサブ式が有効なクエリであるため可能です。複数の条件をどのように持つことができますか?幸運な偶然は、&が引数を結合するため、.where内にネストされると論理的なANDとして機能することです:print(movie.where(american & year.eq(1942))) 1942年のアメリカ映画はCasablancaだけです。.selectは、フィルターを生き残った映画について表示したい列を取得します:print(movie.where(american & year.eq(1942)).select(title & year)) よりクリーンなクエリのために、述語をselect句にプッシュすることもできます:print(movie.where(american).select(title & year.eq(1942))) そして、それが言語のほぼすべてです!Prelaはグループ化と集計、およびその他の一般的な演算子もサポートしています。私たちは言語の完全なドキュメントに取り組んでいますので、現時点では詳細については論文を参照してください。演習として、最初のスニペットが実行されるように必要なリレーションを定義してみてください。# keyword = ... # ... print(movie.where(company.s(country).eq("[us]") & keyword.eq("character-name-in-title")) .select(title & cast.s(person).s(alias).s(text))) 玩具Prela用の自己完結型Pythonプログラムは、ここにあります。これは6NF分解としても知られています。これがオーバーヘッドを導入することを心配している場合は、この投稿をチェックして、PrelaがCPSで間接性をコンパイル解除する方法を確認してください。↩︎ Jupyterなどとは異なり、snipは状態の破損を避けるために常に最初から実行されます。↩︎ .selectメソッドの構文は、Rel.selectをselect()に転送するのと同じトリックを使用します。↩︎ ここでは、行番号を会社IDとして使用してズルをしています。↩︎ 解答はこのページ上のどこかに隠されています ;)↩︎