アルゴリズム | 株式会社Altus-Five / 株式会社Altus-Five は、技術力で勝負するシステム開発会社です。 Sun, 01 Jun 2025 17:12:04 +0000 ja hourly 1 https://wordpress.org/?v=6.8.2 /wp-content/uploads/2025/01/cropped-favicon-32x32.png アルゴリズム | 株式会社Altus-Five / 32 32 zip, compress, gzip, bzip2 – ファイル圧縮の形式に関する覚書 /blog/2018/06/14/file_compress_extensions/ /blog/2018/06/14/file_compress_extensions/#respond Wed, 13 Jun 2018 15:02:00 +0000 http://43.207.2.176/?p=307 本記事の内容 zip, compress, gzip, bzip2の違い(それぞれの歴史) zipとは? zipは1989年にフィル・カッツによって提案された圧縮形式で、現在最も多く用いられている圧縮形式であると同時に、 […]

The post zip, compress, gzip, bzip2 – ファイル圧縮の形式に関する覚書 first appeared on 株式会社Altus-Five.

]]>
本記事の内容
  • zip, compress, gzip, bzip2の開発経緯と技術について
  • 圧縮とアーカイブの違い
  • zip, compress, gzip, bzip2の性能比較(デモ)
  • bzip2の圧縮性能を支える技術のキーワード(BWT、ハフマン符号)

zip, compress, gzip, bzip2の違い(それぞれの歴史)

zipとは?

zipは1989年にフィル・カッツによって提案された圧縮形式で、現在最も多く用いられている圧縮形式であると同時に、後述する多くの圧縮形式に名前を貸し与えています。

ファイルは辞書を用いて圧縮され、Lempel, Zivらによって1977年に提案されたLZ77法が使われています。 また、文字列の符号化にはハフマン符号が用いられています(上記まとめたアルゴリズム名は『Deflate』)。

zipはアーカイバとしての機能を持ち、複数ファイルをまとめつつ、ファイルサイズを下げることが出来ます。

compressとは

Lempel, Zivらによって提案されたアルゴリズムには、 1977年に提案されたLZ77法と1978年に提案されたLZ78法の2種類があるのですが、 compressでは、LZ88法の改良版として提案された、Lempel-Ziv-Welchのアルゴリズム(LZW)が用いられています。 (ちなみに、LZWの提案は1984年。)

gzipとは

gzipはGNU ZIPの略称で、オープンソースプロジェクトの走りとも言える(というと偉い人から怒られるのですが…)GNUプロジェクトの一環として開発された圧縮方式です。 当時広まっていたcompressの代替を目して開発された方式で、zipと同様に圧縮はDeflateによります。

zipとは異なりアーカイブ機能はありません。

bzip2とは

bzip2は1996年に公開されたファイル圧縮形式で、gzipに加えて下記の処理が加わっており、圧縮効率がなお高まっています。

  • BWT(Burrows-Wheeler Transformation, バロウズ・ホイラー変換)を用いた前処理
  • Move to Front法による前処理

またgzipと同様、ハフマン符号を用いている点もポイントです。

これらの処理を行う分、処理速度ではgzipに軍配が上がるとされています。 こちらも、zipとは異なりアーカイブ機能はありません。

補足:圧縮とアーカイブの違い

用語定義
圧縮ファイルサイズを小さくすること
アーカイブ複数ファイルを一つにまとめること

アーカイブを行うプログラムを アーカイバ とも呼び、 上述した形式には、アーカイバとしての機能があるものとないものが存在します。

辞書、BWT、ハフマン符号 – 圧縮にかかわる技術を学ぶには

さて、ここまでの内容にはいくつかの用語が登場してきました。

  • 辞書を用いた圧縮
    • LZ77法
  • BWT(Burrows-Wheeler Transformation, バロウズ・ホイラー変換)
  • ハフマン符号

実は上記はいずれも、『高速文字列解析の世界 データ圧縮・全文検索・テキストマイニング』(岡野原、2012)の中で詳細に解説されています。

Altus-Fiveブログでは本書を読み解きながら、今後 これら技術に関するデモ実装をPythonで行いたい と考えておりますので、ご興味のある方はぜひブックマーク、もしくははてブを押していって頂けると嬉しいです。

おまけ:テキスト圧縮の性能比較 – zip, gzip, compress, bzip2を比べてみた

今回はデモとして、青空文庫からダウンロードした相対性理論の論文(sotaisei_riron.txt)を圧縮してみました。

圧縮方式BeforeAfter
zip29KB12KB
gzip29KB12KB
compress29KB15KB
bzip229KB10KB

このファイルについて4形式を比べた時、 bzip2の性能が最も良い という結果となっています。

なぜbzip2は性能が良いのか?

先ほどの繰り返しになりますが、

  1. 索引作成の前処理としてBWT(Burrows-Wheeler Transformation, バロウズ・ホイラー変換)、MTF (Move-To-Front) 法を行なっている
  2. 圧縮方式としてハフマン符号を用いており、入力テキストそれぞれに二分木(ハフマン木)を生成。最適な符号化を行なっている

といった理由が挙げられます。 上記キーワードの詳細は、すべて『高速文字列解析の世界』(岡野原,2012)に載っていますので(MTF法を除く)、よければご参照ください。 本記事はあくまで「歴史的経緯のまとめ」ですが、技術の深みの面白さについて伝えていくため、 今後、BWTやハフマン符号の概略を示す記事をAltus-Fiveブログで掲載したいと思っています。

参考記事

アルタスファイブのブログでは、データ構造に関する記事を定期的に執筆しております。 よろしければ、ご参照ください。

The post zip, compress, gzip, bzip2 – ファイル圧縮の形式に関する覚書 first appeared on 株式会社Altus-Five.

]]>
/blog/2018/06/14/file_compress_extensions/feed/ 0
2NF・3NF・BCNFは自動生成できる データベース正規化アルゴリズムとその実装 /blog/2017/05/30/db-ukey-detect2/ /blog/2017/05/30/db-ukey-detect2/#respond Mon, 29 May 2017 16:30:00 +0000 http://43.207.2.176/?p=387 前回のおさらい – データベース正規化を自動化してみようと思った経緯 前回の記事でやったこと 第一正規形のデータベースを入力すると、すべての候補キーを検出するアルゴリズム(擬似コード)を記述。二分木を使った処 […]

The post 2NF・3NF・BCNFは自動生成できる データベース正規化アルゴリズムとその実装 first appeared on 株式会社Altus-Five.

]]>
前回のおさらい – データベース正規化を自動化してみようと思った経緯
  • 超高速開発ツールの普及が進んでおり、弊社でも「Web Performer」を導入
  • 各社のツールを比較検討してみたけど、どれも現時点ではまだ、AIではない
  • もっとAIチックな機能を持たせてみたら、ユーザーが喜ぶのでは?
  • 例えば、「最適なデータベース設計の提案」はどうだろう?
  • 正規化という王道の手法があるので、実験がしやすそう
    • やってみよう

前回の記事でやったこと

第一正規形のデータベースを入力すると、すべての候補キーを検出するアルゴリズム(擬似コード)を記述。二分木を使った処理の内容を解説しました。

今回は、アルゴリズム(擬似コード)だけでなく、C++のソースコードも掲載しています。ぜひ、データベースの自動正規化を実行してみてください。

目的

第一正規形で与えられた任意の単一テーブルについて、ボイスコッド正規形への無損失分解を行うアルゴリズムを構築する。

目標としてはボイスコッド正規形までの分解でしたが、今回は尺の都合で、第二正規形(2NF)までの正規化を行うアルゴリズムを提案・実装しています。

2NF、3NF、BCNFはいずれも、関数従属性を取り扱うという点で共通しています。なので、2NFの自動化ができれば、3NFやBCNFへのステップアップにはさほどの困難はなさそうです。

今回も、用語については下記の書籍に則っています。

第一正規形(1st normal form, 1NF)とは?

正規化の対象であるRDBのRは「リレーショナル(Relational)」の頭文字ですが、これはリレーショナルモデルという数学的モデルに立脚しているということから付いた呼称です。同モデルの主要概念である「リレーション」の定義を満たすデータベースを第一正規形と呼び、具体的には、以下の条件を満たす必要があります。

  • 繰り返しグループ(同一セルに2つの値を含むなど)が存在しない
  • データにNULLは含まれない

今回は、入力されるテーブルが第一正規形であることを仮定して、議論を進めます。

第二正規形(2nd normal form, 2NF)を自動生成するアルゴリズム

第二正規化を自動化するにあたり、まずは第二正規形の定義を見てみましょう。

2NFの定義

1NFの条件を満たし、部分関数従属性を持たないテーブルは2NFである。

さて、この定義の中で「部分関数従属性」及び「関数従属性」についてご存知ない方は、以下の段落を読んでおいてください。

関数従属性(Functional depenency, FD)とは?

関数従属性とは、テーブルにおける、2つのカラム集合に関する概念です。

あるテーブルにおいて、AAというカラム集合の値が同じなら、BBというカラム集合の値も同じである時、BBはAAに関数従属していると言います。

この概念を説明する喩えとして、以下のテーブルを見てください。

上図は、与えられた「名詞」と「動詞」の組み合わせが「自然であるかどうか」を表すテーブルです。

この時、「鉛筆」という名詞だけに対して「自然」か「不自然」かの判定はできません。 「食べる」という動詞に対しても同様です。 しかし、「鉛筆」「食べる」という組み合わせに対しては、それが「不自然」であるという判定を下すことができます。

このことから、評価評価というカラム集合は、名詞名詞や動詞動詞といったカラム集合には関数従属しておらず、名詞,動詞名詞,動詞というカラム集合には関数従属していることが分かります。 なお、上記のテーブルには、他のいかなる関数従属関係もありません。

ここでは分かりやすさのために、カラム同士の関係性が明確な例をあえて選びました。 勿論一般に、関数従属関係はカラムの名称ではなく、テーブルの内容によって決まります。 例えば、以下のテーブルを見てみましょう。

上図は、はじめの図とよく似ていますが、「名詞と動詞の組み合わせが自然だと思うか?」というアンケートの結果をイメージした表に変わっています。 着目すべきは、「トマト」「投げる」という組み合わせについて、ある人は「自然」、またある人は「不自然」と答えていることです。テーブルがこのような状態になっていると、「トマト」「投げる」という組み合わせが自然か不自然か、データからは一意に定まりません。ゆえにこのテーブルでは、評価評価は名詞,動詞名詞,動詞に関数従属していないことになります。

このように、関数従属関係は、テーブル内のデータによって定まることに注意してください。

また、説明する相手がエンジニアであれば、以下の説明は分かりやすいかもしれません。

  • カラム集合AAの値をkey、BBの値をvalueとして、両者の関係を一つの連想配列に格納可能であるとき、BBはAAに関数従属しているという

上記の定義は、これから行う関数従属性の判定において活用することになります。

部分関数従属性(Partial functional dependency)とは?

部分関数従属性は、ある条件を満たす関数従属性のことです。 定義の紹介の前に、まず候補キーについて復習しておきましょう。

復習:候補キーとは?

以下の条件を満たすカラム集合を「候補キー」と呼びます。

  • テーブル内のデータを一意に定めることができる
  • カラムを一つ除くと、データを一意に定めることができなくなる(既約性)

例えば上図においては、回答者ID,名詞,動詞回答者ID,名詞,動詞が唯一の候補キーです。

また、ある集合に対し、要素を一つ以上取り除いた別の集合を真部分集合と呼びます。たとえば、回答者ID,名詞,動詞回答者ID,名詞,動詞に対して名詞,動詞名詞,動詞は真部分集合です。

以上の用語を用いると、部分関数従属性とは、ある候補キーの真部分集合から、その候補キーに含まれないカラム集合への関数従属性であると定義できます。

そして、第二正規形は、第一正規形であり、部分関数従属性を含まないデータベースとして定義されます。

実は、上図のテーブルは部分関数従属性を持たないため、第二正規形になっています(もっと言うと、さらに高次の正規形であるボイスコッド正規形になっています)。よって、この条件を満たさないような別の例を考えてみましょう。

上表は、3人の生徒に対し、3つの科目の試験を行った結果をイメージしたテーブルです。 各生徒について、各科目の評点の平均値を丸めたものを総合点としています。 テーブルより、評点評点は氏名,科目氏名,科目に関数従属しており、総合点総合点は氏名氏名に関数従属していることが分かります。 このテーブルには、部分関数従属性が存在するでしょうか?

まず、候補キーは氏名,科目氏名,科目であることが分かります。候補キーの真部分集合は氏名氏名もしくは科目科目の2通りですが、氏名氏名から総合点総合点への関数従属性が存在しており、これが部分関数従属性であることが分かります。

このような、部分関数従属性を持つ(第二正規化されていない)テーブルの弱点の一つとして、「更新による異常が起きやすい」という性質があります。例えば、上記のテーブルに対し、第4の科目である「Ruby on Rails」の評点を加えたとしましょう。 その際、全科目の平均値である総合点を更新する必要に迫られますが、以下のようなテーブルへと更新してしまう恐れがあります。

  • 各生徒に関する「Ruby on Rails」の評点を追加
  • 総合点の数値を再計算し、追加行に格納
  • しかし、元々テーブル内にあった「PHP」「HTML5」「JavaScript」行の総合点を更新し忘れてしまった

テーブルがこのような状態になっていると、氏名氏名に対して総合点総合点が一意に定まらず、複数ある値のどれが正しい値かわからない状態になります。テーブルの性質上総合点総合点は氏名氏名に関数従属すべきなのに、そうなっていない状態は一種の矛盾です。第二正規形でないテーブルは、更新による矛盾が起きやすいテーブルと言えます。

このテーブルから部分関数従属性を取り除くには、テーブルの分解が必要です。分解には射影(MySQLで言うところの、DISTINCT指定したSELECTコマンド)を用います。

今回の場合、以下の2つのテーブルに分解すれば、情報を損失させずに分解が可能です(無損失分解)。

第二正規化を自動で行うには?

第二正規化の自動化は、大きく3つのステップからなります。

  1. 候補キーを検出する
  2. 部分関数従属性を検出する
  3. 射影によって部分関数従属性を取り除く

このうち、1.については前回の記事でアルゴリズムを設計しましたので、今回は候補キーの一覧が手に入ることを前提に話を進めます。

部分関数従属性の検出

アルゴリズムを記述するために、幾つかの記法を決めておきましょう。 まず、カラム集合AAからカラム集合BBへの部分関数従属性が存在することを、A→BA→Bと表記します。先の例で言えば、氏名→総合点氏名→総合点が成り立っていました。 また、検出される全ての関数従属性をPFDPFDで表します(Partial Functional Dependencyの略)。 実装上は、PFDPFDは連想配列を使うと良いでしょう。

テーブルの全ての候補キーの集合をKK(花文字のKK)で表すこととし、個々の候補キーをK(∈K)K(∈K)で表します。 また、集合の記法にしたがって、カラム集合AAが候補キーKKの真部分集合であることを、A⊊KA⊊Kと表すことにします。 テーブルに含まれる全てのカラムからなる集合をCCとすると、KKに対する非候補キーの集合は、差集合C∖KC∖Kで表されます。(差集合C∖KC∖Kは、集合CCの要素のうち、KKに含まれない要素からなる集合のことです。)

アルゴリズムのアイデアは、候補キーの集合KKから任意の候補キーKKを取って、その真部分集合A⊊KA⊊Kが非キー属性c(c∈C∖K)c(c∈C∖K)に対する関数従属性A→cA→cを持つかどうか、しらみ潰しに調べるというものです。 まだ調べていない候補キーの集合をˆKK^で表すこととし(アルゴリズム開始時はˆK=KK^=K)、調べ終わった候補キーはˆKK^から取り除きます。 そして、ˆK=∅K^=∅(空集合)となった時にアルゴリズムが停止するようにします。

前回と同様、レコード数(テーブルの行数)をmmで表すこととし、テーブル全体をRR、個々のレコードをr1,r2,…,rm∈Rr1,r2,…,rm∈Rで表します。

以上の記法を用いて、すべての部分関数従属性を検出するアルゴリズムを記述してみましょう。

上記の擬似コードはアルゴリズム分野の書き方に倣い、代入を==ではなく:=:=、等値演算子を====ではなく==で書いていますので、ご注意ください。

下記は、擬似コードにコメントをつけたものです。

実装に関するコメント

候補キーK(∈K)K(∈K)について、その全ての真部分集合を列挙する際には、前回も活用した二分木を使うのが筋の良い方法です。

第二正規化の方法

部分関数従属性の検出ができれば、第二正規化を自動的に行うのはさほど難しくありません。部分関数従属性にしたがってテーブルを分解していくことで、第二正規化を行うことができます。

ただし、候補キーが複数あるような場合は処理が複雑になりますので、ある候補キーKKについて部分関数従属性が検出された場合、一旦それを取り除いておいてから、分解後のテーブルに対してもう一度部分関数従属性の検出を行うといった段階的な実装をしたほうが、バグは少なくなりそうです。 また、分解の順序によって、結果として得られるテーブルの構成がどのように変わるかという点について、議論の余地があります。

第二正規化を自動で行うC++プログラム

ディレクトリ構造は以下のようになっています。

.
├── makeDB_test.cpp
├── table.hpp
├── makeDependentData.hpp
├── dependency.hpp
└── hashVI.hpp

各ファイルの機能は以下の通りです。

ファイル説明
makeDB_test.cpp正規化を行う対象のテーブルがもつ関数従属性を指定して生成、第二正規化を実行
table.hppRDBMSのテーブル操作(SELECT, SELECT DISTINCT, etc.)や、候補キー検出・関数従属性検出・第二正規化アルゴリズムを実装。本プログラムの心臓部
makeDependentData.hpp指定された関数従属性をもつテーブル生成機能を実装
dependency.hpp関数従属性のプログラム上での表現(dependency構造体)を定義
hashVI.hppvector<int>型からハッシュ値を発行する関数オブジェクトを定義

プログラムに関する補足

各テーブルを二次元vector(vector< vector <int> >型オブジェクト)で擬似的に表現し、RDBMSのSELECT DISTINCTコマンドはハッシュを使って自力で実装しています。 対象テーブルの行数をmm, カラム数をnnとしたとき、SELECTコマンド、SELECT DISTINCTコマンドの計算量は以下の通りです。

コマンド計算量
SELECTO(mn)O(mn)
SELECT DISTINCTO(mn)O(mn)

ハッシュを使うなどデータ構造の工夫をしないと、両者の計算量は一致しません(DISTINCT指定すると遅くなってしまう)。 具体的な要件としては、vectorをkeyとして用いることのできるハッシュ(連想配列)が必要になります。今回はデータ型がintのみだったので簡単でしたが、任意の型の組み合わせだとややこしいことになりそうです。

第三正規化とボイスコッド正規化

さらに高度の正規化である第三正規化、ボイスコッド正規化についても、第二正規化と類似した手続きで行うことができます。

  • 取り除くべき関数従属系を検出
  • 検出された関数従属性に従い、テーブルを分解

その具体的な方法についても、本記事に反響があれば追記していきたいと思います。

The post 2NF・3NF・BCNFは自動生成できる データベース正規化アルゴリズムとその実装 first appeared on 株式会社Altus-Five.

]]>
/blog/2017/05/30/db-ukey-detect2/feed/ 0
超高速開発とAIに関する一考察 DBにおける候補キーの自動検出アルゴリズムを書いてみた /blog/2017/04/28/db-ukey-detect/ /blog/2017/04/28/db-ukey-detect/#respond Thu, 27 Apr 2017 17:04:00 +0000 http://43.207.2.176/?p=400 超高速開発に人工知能(AI)は寄与するか? 近年、これまでスクラッチ開発に重きを置いていた企業にも、超高速開発の気風が高まってきています。 超高速開発とは、システムの開発フローにおいて、設計・コーディング・テスト生成とい […]

The post 超高速開発とAIに関する一考察 DBにおける候補キーの自動検出アルゴリズムを書いてみた first appeared on 株式会社Altus-Five.

]]>
超高速開発に人工知能(AI)は寄与するか?

近年、これまでスクラッチ開発に重きを置いていた企業にも、超高速開発の気風が高まってきています。

超高速開発とは、システムの開発フローにおいて、設計・コーディング・テスト生成といった部分を自動化するための業務支援ツールを導入した開発手法を指します。

弊社では、キヤノンITソリューションズ製の「Web Performer」という超高速開発ツールを導入して、開発スピードを上げながら、より人間の智慧を必要とする部分にリソースを割く判断をしました。

このツールがどのような用途を持つかというと、例えばデータベースを設計するなら下記のような作業をスタッフが行います。

  • データベース項目の登録
  • 画面項目の登録
  • 画面項目とデータベース項目の紐づけ

要はグラフィカルにデータベース設計が出来て、入力された要件に従い、指定された言語のコードが発行されるという仕組みになっています。

しかし、「システムを作るシステム」というと、SF好きでなくても、AIを活用した対話的なものを多少なり想像するのではないかと思います。 「要件定義→概要設計→詳細設計→実装→テスト→運用」という開発フローでいうなら、もっと要件定義部分に踏み込んでくるような製品を待ち望んでいるのは、きっと弊社だけではないでしょう。

そこで、もう少しAIらしい要素をもたせて、要件定義部分に踏み込むようなツールが作れないか、ということを考えてみました。

システム自動生成AIの機能案 – DB正規化をやってくれる

人間がデータベースを設計する時に、考慮せねばならないこととして、正規化があります。

データベース正規化というのは、ある程度経験のあるプログラマであれば、はじめから適切に正規化されたテーブルを作ることが可能です。開発のボトルネックになることはあまりないため、自動化できないかと真面目に考察したことのある方はあまり多くないのではないでしょうか。

しかし、データベースを含むシステムの生成をツール上で行う文脈であれば、ありもののテーブル(Excelワークシート、csvファイル、etc.)から勝手に正規化をやってくれたら、便利に感じるのではないかな、と着想しました。

本記事では、「システムを作るシステム」設計者の立場を仮定し、 正規化という概念を知らないようなユーザーでも、サンプルとなるテーブルを与えれば、正規化されたテーブルが出力されるようなアルゴリズムを考えてみたいと思います。

目的

第1正規形で与えられた任意の単一テーブルについて、ボイスコッド正規形への無損失分解を行うアルゴリズムを構築する。

ここで、用語の定義は下記の文献に則っていますので、適宜ご参照ください。

第1正規形についての補足

入力が第1正規形であるということは、下記を満たすということを特記しておきます。

  • 繰り返しグループ(同一セルに2つの値を含むなど)が存在しない
  • データにNULLは含まれない

候補キーの検出

本記事で行うのは「候補キーの検出」までです。 得られた候補キー集合を用いてBCNFのテーブルを導くのは、今後の記事の内容となりますので、ご期待ください。

復習:候補キーとは?

候補キーとは、レコードを一意に特定できるようなカラムの集合のうち、既約(irreducible)なもの(の集合)を指します。

既約であるとは、候補キーから一つでもカラムを取り除くと、レコードを一意に特定出来なくなることを意味します。(irreducible = ir + reduce + ableですので、『減らせない』という意味ですね。)

候補キーとなりうるカラム集合の数

まず、そもそもカラムの集合が何通りあるかを算定しましょう。 ここでは{受注ID,客先名,客先担当者,受注数,単価}という5つのカラムからなるテーブルを想定します。 5つのカラムのうち、候補キーであるかどうか吟味すべきカラムの集合を、0/1の列で表すことにします。例えば(01101)は{客先名,客先担当者,単価}というカラムの組を指します。

すべてのカラムからなる集合(1111111111)は、もちろんレコードを一意に特定します。(そうでなければ、全く同じレコードがテーブル内に2つ以上存在することになりますが、それはリレーショナルモデルの仮定に反するため、ここでは考慮しません。)

さて、カラムの集合が何通りあるかですが、これは5桁の2進数のパターン数と等しくなることがお分かりでしょう。よって25=3225=32通りのカラムの集合があります。一般に、カラム数をnnとすると、2n2n通りのカラムの集合があり、組合せ数はカラム数の指数オーダであることがわかります。 よって、すべての組が候補キーかどうかをナイーブに(愚直に)調べていると、確実に指数時間かかってしまいますので、効率化のための工夫をしたいところです。

二分木の導入

カラムの集合が候補キーであるか、どのような順番で検証していくかを決めるために、二分木(Binary tree)を導入します。 カラムが5つの例の場合、対応する二分木は第1〜第5階層まであり、各階層がカラムに対応します。そして、ノードの0/1値はその階層に対応するカラムを、候補キーであるか吟味すべき集合に含めるか含めないかを表しています。二分木の先端である葉ノード(leaf node)がカラムの集合を表します。例えば二分木の根(root)からある葉ノードまでの経路上にあるノードの値が(01101)だったならば、{受注ID,客先名,客先担当者,受注数,単価}のうち{客先名,客先担当者, 単価}からなるカラムの組を表現する葉ノードであることがわかります。

葉ノードに到達したら、そのノードの表すカラムの組が、レコードを一意に決めるかチェックします。 チェックの方法について、2通りの表現を考えてみました。

MySQLの記法に従う場合

テーブル名を{table}、カラムの組を{column 1}, {column 2}, …, {column n}で表現します。 下記コマンドの結果を用いることで、カラムの組がレコードを一意に定めるかどうか判定出来ます。

SELECT COUNT(DISTINCT {table}.{column 1}, {table}.{column 2}, …, {table}.{column n}) >= COUNT({table}.{column 1}, {table}.{column 2}, …, {table}.{column n}) FROM table

出力が1ならばレコードを一意に定めること、0ならば一意に定めないことがわかります。 ただし、既約であるかどうかは、1つのカラムの組に関する結果からは判定出来ないことに注意してください。 前述した通り、二分木を用いた探索を用いるならば、再帰的な関数で表現するのが筋の良い方法です。しかし、MySQLはそういった処理にあまり向かない(設計思想上望ましい操作とは言えない)ため、別の言語で書くことを想定して、アルゴリズムを疑似コードで表現してみましょう。

擬似コードで表現する場合

ここからは、一からアルゴリズムを設計していくことになります。少なからず数学的な表記を使うことになりますが、なるべく平易に説明をするよう努めますので、お付き合いください。

テーブルに含まれる全てのカラムの集合をEE、その部分集合をCCで表すことにします。このとき、C⊆EC⊆Eという関係が成り立ちます(CCはEEに含まれる)。 EEには何通りの部分集合があるかというと、先ほどカラムの部分集合を(01101)のような2進数で表したことからもわかる通り、カラム数をnnとして2n2n通りあります。これらEEの全ての部分集合を2E2Eで表します(べき集合)。 2E2Eの要素が、nn桁の2進数に1対1対応することに注意すると、この先の議論が解釈しやすくなります。

与えられたテーブルに含まれるレコードの集合をRRとします。レコード数をmmとする時、個々のレコードはr1,r2,…,rm∈Rr1,r2,…,rm∈Rと表すことが出来ます。 カラムの組CCに対するレコード全体(テーブル)の射影をR(C)R(C)、個々のレコードririの射影をri(C)ri(C)で表すこととします。ただし、射影された結果同一のレコードが生じる時も、両者を異なるものとして区別することに注意してください。 (MySQLでいうところの、DISTINCT指定しないSELECTコマンドに対応します。)

いま取り扱いたいのは、射影されたレコードの一意性が保たれるかどうか、ということです。あるカラムの組CCを取り、射影されたレコードを上から順に見ていくとき、「既に見たレコード」の集合をˆR(C)R^(C)で表します。いま手に取ったレコードがˆR(C)R^(C)に含まれていないならば、その時点での一意性は担保されていることになります。

最後に、探索順序を制御する二分木について記法を決めておきます。まだ探索されていない葉ノードをLLと表します。探索開始時はL=2EL=2E(すべてのカラムの集合が未探索)となり、探索するに従いLLの要素が減っていきます。そして、L=∅L=∅(空集合)となったとき、アルゴリズムは停止します。

これで擬似コードを記述する準備が出来ました。

全ての超キーを検出するアルゴリズム

超キーとは、レコードを一意に定めることのできるカラムの組を指します。 他にも幾つかの解釈がありますので、理解しやすいものを覚えておくと良いでしょう。

  • 候補キーの2つの条件から既約性を取り除いたとき、該当するカラムの組の集合
  • それ自体が候補キーであるか、または候補キーを含むようなカラムの組の集合

さて、候補キーのみを検出するアルゴリズムをいきなり記述してもよいのですが、既約であるという制約の表現が少し難しいです。 ですので、まずは既約の制約を気にしなくてもよいように、すべての超キーを検出するアルゴリズムを記述することにしましょう。

下記は、実装シーンを少し意識して、上記の擬似コードにコメントをつけたものです。

いくつか、実装の際の注意をしておきます。

二分木の実装

Step 1の以下の処理には、実際には二分木を活用することになります。

LLというのはnn桁の2進数の集合でしたから、二分木の葉ノードに対応しています。 二分木は、再帰的な関数(recursive function)として実装するのがセオリーです。再帰的な関数に階層を表すパラメータfloorfloorを持たせ、floor==nfloor==nとなった時に得られた2進数についてStep 2を実行するのが、恐らくもっともシンプルな実装法です。

ハッシュの活用 – 計算量の最適化のために

上記の擬似コードはあくまで手続きの概要を示したものなので、実計算時間はもちろん、計算量にも効いてくる要素が曖昧になっていることに注意してください。 例えば、Step 3の以下の処理は、方法によって計算量が異なります。

この処理は、新規レコードrk(C)rk(C)が、既出レコード集合ˆR(C)R^(C)に含まれているかどうかを判定していますが、この処理にかかる時間は既出レコード集合のデータ構造に依存します。

例えば、既出レコード集合として配列を用いた場合、rk(c)∈ˆR(c)rk(c)∈R^(c)を判定する代表的な方法は線形走査(linear probing)です。その場合の計算量は、判定一回あたりO(|R|n)O(|R|n)となります。 一方、ˆR(c)R^(c)について適切なハッシュを用いて管理した場合、rk(C)∈ˆR(C)rk(C)∈R^(C)の判定はO(n)O(n)で行うことができます。

本題:全ての候補キーを検出するアルゴリズム

さて、いよいよ全ての候補キーを検出するアルゴリズムに入りましょう。

この場合の難しさは、既約性の判定も行わなければならないという点です。 一つの素朴なアイデアとして、前述のアルゴリズムのStep 3に「既約性の判定」を入れるというものが考えられます。

このとき、既約性の判定は、具体的にどのような手続きで行うべきでしょうか。 カラムの集合CCが既約であるためには、「CCの全ての部分集合」が、レコードを一意に特定出来ないものである必要があります。 ところが、その判定には困難が伴います。

  • 一般にCCの部分集合は要素数の指数オーダ存在する
  • 先のアルゴリズムではCCを取る順番については言及しなかったので、CCの部分集合C′(⊂C)C′(⊂C)についてはまだ未探索かもしれず、候補キーであるかわからないケースがある

よって、Step 3を書き換えるアプローチはあまり好ましくありません。

ではどうすればよいかと言うと、実は判定対象を決定するStep 1を書き換えると、効率的に既約性の判定が行えます。

上記の擬似コードは、Step 1をサブステップに分け、幾つかの処理を追加したものになっています。 変更点についてコメントしたものが下記の擬似コードです。

まずStep 1-1の「任意の未探索葉ノードC′C′を含まないCCを取る」という語義を説明します。 例えばアルゴリズム開始時は、二分木のすべての葉ノードは未探索です(L=2EL=2E)。このような時、上記の制約にしたがって取りうる葉ノードとしては、(0000…00000…0)のみとなります。 なぜなら、11を1つでも含む葉ノードCCは、必ず空集合(0000…0)(0000…0)を含んでしまうためです。

空集合の次に取るべき葉ノードは、11を1つだけ含む葉ノードです。仮に11を2つ以上含む葉ノードを取ったとすると、11を1つだけ含む葉ノードを含んでしまうでしょう。

以降も同様に、未探索のノードを含んでしまわないように探索を進めていきます。

そのような処理は難しそうだな、と思われたかもしれませんが、実は上記の制約を満たすよう二分木を探索することは簡単です。 その方法とは、「各ノードにおいて0/1を選択する際、0を先に選択する」よう実装することです。このように実装したならば、任意の未探索葉ノードC′C′を含まないような葉ノードに必ず行き着くのですが、興味があればぜひ、その証明を考えてみてくださいね。

そして、Step 1-2では、これまでに得られた候補キーの集合ˆR(C)R^(C)の中に、CCに含まれるようなカラムの集合C′C′があるかどうかを判定しています。もしあるならば、CCが既約でない超キーであることは明らかです(本記事の既約の定義、及び超キーの定義2.を参照)。既約でない超キーについては出力に含めるべきではないので、その時点でStep 1-1に戻ることが正当化されます。

Step 1-1, 1-2を加えたおかげで、Step 3に一切の変更を加えることなく、あるカラムの組CCが候補キーであることを確信できます。

なぜなら、それが候補キーではない超キーであるような場合は、Step 1-2の段階で判定対象から除外されていたはずだからです。

判定対象除外の実装テクニック – 二分木の枝刈り

Step 1-2で行う、判定対象除外の実装時に役立ち、計算時間を大幅に改善できるテクニックとして「枝刈り」を紹介します。ナップサック問題などの最適化問題を解く際にも使われるテクニックで、覚えておくと使えるタイミングがあるかもしれません。

二分木の探索中に、葉ノードではないノードSSにいたとします。そのノードが表す値を、(11∗∗∗)(11∗∗∗)のように、まだ未定の部分は∗∗を使って表現することにします。 この時、ある候補キーK∈L∗K∈L∗が存在して、SSがKKを(対応するカラム集合の意味で)含むならば、SS自身及びSSの子ノードは既約でない超キーを表すことがわかります。

より正確に書くと、2進数で表現したS,KS,Kのii桁目をそれぞれSi,KiSi,Kiで表したとき、以下の条件を満たすSSがKKを含む超キーと言えます。

  • Si≠∗Si≠∗ならばSi≥KiSi≥Ki
  • Si=∗Si=∗ならばKi=0Ki=0

超キーは候補キーではありませんので、そこから先の探索を行う必要がなく、処理を削減できます。 このような、目的に基づく条件判定と処理削減のテクニックを「枝刈り」と呼びます。適切な枝刈りは、プログラムの正当性を損なわずに処理速度を向上させることが出来ます。

課題:計算量はさらに改善可能か?

さて、枝刈りを導入した際の計算時間ですが、効率的ではあるものの、依然として指数時間かかってしまうように見えます。果たして、多項式時間で候補キーを検出する方法はないのでしょうか?

  • あるとしたら、それはどんな方法?
  • ないとしたら、どうやって証明できる?

上記も興味深いテーマではありますが、本記事では問の設定に止めておきます。 指数時間・多項式時間アルゴリズムを区別する「計算量理論」の入門書としては、下記の書籍などが挙げられます。上記の問について取り組みたいが、計算量理論の知識がない、という方向けの入門にはお勧めです。

また、「問題を解く多項式時間アルゴリズムが存在しないことの証明」には、よく「NP困難性の証明」が用いられます。そちらについてあまり数学を使わずに学びたい方には、以下の書籍がお勧めです。

以上が、単一テーブルから全ての候補キーを検出するアルゴリズムの構築とその紹介でした。次回は、このアルゴリズムで得られた候補キー集合を参照し、ボイスコッド正規形(BCNF)を得る方法について考えていきたいと思います。

The post 超高速開発とAIに関する一考察 DBにおける候補キーの自動検出アルゴリズムを書いてみた first appeared on 株式会社Altus-Five.

]]>
/blog/2017/04/28/db-ukey-detect/feed/ 0