玩式草子─ソフトウェアとたわむれる日々

第31回SQLiteでRDB再入門その1]

前回紹介したように、昨年末に64ビット版(x86_64)のPlamo64-1.0を公開した後、年明けからは32ビット版(x86)のパッケージを追従させる作業にかかっています。

前回の最後では、ここ数ヶ月のうちにx86用のパッケージを揃えてしまいたいと書きましたが、実際に取りかかってみると、バージョンが古くなっていてx86用のみならずx86_64用もバージョンアップした方がいいソフトウェアが多数出てきました。

ざっと見た感じでは、Plamo64-1.0を公開してから今までに、x86用のパッケージは300弱が更新されているのに対し、x86_64用のパッケージもその半数の150弱が更新されているようです。Plamo64-1.0の総パッケージ数は1100ほどなので、このペースで行くとx86用のパッケージが揃うのは今年の後半くらいになりそうで、たぶん、そのころにはx86_64用も半分くらいのパッケージを更新している計算になります。

パッケージの更新作業の際には、アーキテクチャごとのバージョンをチェックする作業が頻発するので、FTPサーバにあるパッケージのバージョンを一覧表示するページを作ってみました。

図1 Plamo-5.0の進捗状況のページ
図1 Plamo-5.0の進捗状況のページ

このページでは、FTPサーバのPlamo-5.0/{x86,x86_64}/ 以下のパッケージを調べて、カテゴリごとに両アーキテクチャでのパッケージの有無とバージョン、アーキテクチャ間でバージョンが異なる場合は古そうな方に色を付けて表示するようにしてみました。

最近はこのページを使ってどのソフトウェアから更新していくかを考えていますが、このページでわかるパッケージの有無やバージョンといった情報だけでは判断できない問題もあります。

パッケージの依存性問題

その問題とはパッケージの依存関係で、たとえば、Aというソフトウェアが提供するライブラリをBというソフトウェアが使っていて、Bの提供する機能をCが使っているような場合、手を付けやすそうだからと思ってCを先にバージョンアップしてみても、将来Aのバージョンを上げた際にBのバージョンも上げる必要が生じ、芋づる式にCもまた更新しなければならなくなったりすることがあります。

この問題は共有ライブラリに関して発生しがちです。特に共有ライブラリのバージョン番号が変わる際には、その共有ライブラリを参照しているバイナリファイルを全てビルドし直す必要があるので気を使います。

rpmなどの洗練されたパッケージ管理システムでは、そのパッケージに含まれるバイナリファイルを動かすために必要なライブラリをバージョン番号と共にパッケージに記録しておくことができますが、Plamo Linuxで採用しているシンプルなtgz、txz形式のパッケージでは、パッケージには依存性情報を記録できないため、バイナリファイルの依存関係は別途チェックしてやる必要があります。

バイナリファイルが必要とするライブラリはlddコマンドで調べることができるので、手元はこんな形のシェルスクリプトを使って、指定した共有ライブラリを利用しているバイナリファイルを調べていました。

リスト1 ライブラリの利用状況を調べるスクリプト
#!/bin/sh
dir=$1
lib=$2
for i in `find $dir `; do
    elf_chk=`file $i | grep ELF`
    if [ "$elf_chk.x" != ".x" ]; then
       lib_chk=`(ldd $i 2>/dev/null) | grep $lib `
       if [ "$lib_chk.x" != ".x" ]; then
           echo "$i: $lib_chk"
       fi
    fi
done

このシェルスクリプトをchklibs.shとして実行パーミッションを付けておくと、

$ ./chklibs.sh /usr/bin libcrypto

のように引数を与えれば、libcryptoライブラリを参照している、/usr/bin/以下のバイナリファイルを表示します。

$ ./libchk.sh /usr/bin libcrypto
/usr/bin/ssh:   libcrypto.so.1.0.0 => /usr/lib64/libcrypto.so.1.0.0 (0x00007f9f8ac34000)
/usr/bin/ssh-add:       libcrypto.so.1.0.0 => /usr/lib64/libcrypto.so.1.0.0 (0x00007fd604bef000)
/usr/bin/ssh-agent:     libcrypto.so.1.0.0 => /usr/lib64/libcrypto.so.1.0.0 (0x00007f7944552000)
/usr/bin/ssh-keygen:    libcrypto.so.1.0.0 => /usr/lib64/libcrypto.so.1.0.0 (0x00007ffaf104d000)
/usr/bin/ssh-keyscan:   libcrypto.so.1.0.0 => /usr/lib64/libcrypto.so.1.0.0 (0x00007f300c58b000)
/usr/bin/openssl:       libcrypto.so.1.0.0 => /usr/lib64/libcrypto.so.1.0.0 (0x00007f940a486000)
/usr/bin/dig:   libcrypto.so.1.0.0 => /usr/lib64/libcrypto.so.1.0.0 (0x00007feaa79ad000)
/usr/bin/host:  libcrypto.so.1.0.0 => /usr/lib64/libcrypto.so.1.0.0 (0x00007f334f124000)
....

従来、広く使われている共有ライブラリを更新する際などに、このスクリプトを使って更新の影響がどの程度まで及ぶかを調べていました。しかし、このスクリプトは処理にかなり時間がかかります。そこで、もう少し効率よくできないかと考えてみました。

依存関係データベースの試み

前述のchklibs.shスクリプトでは、指定したディレクトリ以下を再帰的に調べていってlddコマンドを実行し、ライブラリ参照の有無をチェックしています。

この作業には、指定したディレクトリ以下の全てのバイナリファイルをチェックする必要があるため、かなり時間がかかります。そんな時間のかかる処理を、調べたいライブラリごとに繰り返すのは無駄と言えそうです。

そこで、インストール済みのバイナリファイル全てについて、それぞれが必要とするライブラリの情報をあらかじめデータベースに登録しておき、検索時にはそれぞれのバイナリファイルを調べるのではなく、データベースから必要な情報を抽出できないかと考えました。

lddの出力は、下記のように一つのバイナリファイルに対して、そのバイナリファイルが参照している全ての共有ライブラリを表示します。

$ ldd /usr/bin/sed 
        linux-vdso.so.1 =>  (0x00007fff3a35b000)
        libacl.so.1 => /usr/lib64/libacl.so.1 (0x00007f526a0c6000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f5269d5c000)
        libattr.so.1 => /usr/lib64/libattr.so.1 (0x00007f5269b58000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f526a2cd000)

たとえばこの結果を「バイナリファイルと必要な共有ライブラリの組」として、/usr/bin/sed & (linux-vdso.so.1, libacl.so.1, libattr.so.1, ld-linux-x86-64-so.2) のような形に整理してやれば、バイナリファイルをキーに、必要なライブラリのリストをバリューとするような連想配列を作ることができます。PerlやPythonといったスクリプト言語ならこの手の連想配列の操作は得意でしょう。

しかし、今回調べたいのは「あるバイナリファイルが使っているライブラリ」よりも、あるライブラリを参照しているバイナリファイルという方向なので、連想配列のバリューのリストを全て調べて該当するキーを抽出するのはちょっと大変そうです。

加えて、上記/usr/bin/sed の例では参照しているライブラリは5つ程度ですが、KDEのソフトウェアのようにGUIを多用しているバイナリファイルでは、参照しているライブラリ数が100近くに達することも稀ではありません。また、調べるべきバイナリファイルの総数もシステム全体では数千の規模になるので、扱うべきデータ全体は数十万件のオーダになりそうです。

そのような規模のデータを効率よく扱うには、フラットなテキストファイルに記録するのではなく、専用のデータベースソフトを使った方がよさそうです。また、上述したように、ライブラリからバイナリファイルを調べるという検索方向を考えると、連想配列的な形で記録するBerkeley DBやgdbmよりも、表形式で記録するリレーショナルデータベース(RDB)の方がよさそうです。

Linuxで使えるRDBソフトウェアではPostgreSQLやMySQLが有名ですが、今回は勉強を兼ねてSQLiteを試してみることにしました。

SQLiteについて

SQLiteは、その名前に"SQL"を含んでいるように、リレーショナルデータベースを操作するための標準的な言語であるSQL(Structured Query Language)に対応したデータベースソフトウェアです。

MySQLやPostgreSQLとSQLiteの最大の違いは、MySQLやPostgreSQLはサーバ・クライアントモデルで設計されているのに対し、SQLiteはライブラリとして提供され、データベース機能を使いたいソフトウェアに組み込んで使う設計になっていることです。

サーバ・クライアントモデルで設計されているMySQLやPostgreSQLの場合、データベースそのものの管理はデーモンとして動作しているデータベースサーバが行い、データベースを使いたいクライアントソフトウェアは、SQLコマンドを使ってサーバに対して処理を依頼し、サーバが返す結果を受けとります。その際、クライアントの側では、実際のデータがどこに、どのような形で記録されているかを意識する必要はありませんし、調べる術(すべ)もありません。

一方、SQLiteの場合、データベースに関する機能はライブラリとして提供されます。すなわち、データベースを使いたいソフトウェアは、あらかじめSQLiteをライブラリとして組み込んでおき、そのライブラリを用いてSQLiteの形式で記録されているファイルを操作する、という形になります。そのファイル(=データベース)にデータを登録したり、データを検索したりする際にSQLコマンドを用いるわけです。

データベースの機能を提供するライブラリを組み込んで使う、と聞くと、C言語等のコンパイラで書いてリンクしてやらないといけないのかな、と思いがちで、実は筆者自身もそう思ってSQLiteは敬遠していました。

しかし、最近では各種スクリプト言語からSQLiteを使うための機能も充実しており、たとえばPythonではSQLiteを利用するためのsqlite3モジュールが標準で提供されていて、追加パッケージ等をインストールすることなくSQLiteを利用できます。このモジュールを試してみると、Pythonから普通のファイルを操作するのと同じような感覚でデータベースを操作することができました。

PythonからのSQLite利用例

実際にスクリプトを組んで行くのは次回に回して、まずはPythonからどのようにSQLiteを使うのかを紹介してみましょう。Pythonは対話的にも操作できるので、その操作例を以下に紹介してみます。

まずPythonを起動し、sqlite3モジュールをインポートし、SQLiteの機能を使えるようにします。

$ python
Python 2.7.2 (default, Jan 16 2012, 21:21:28) 
[GCC 4.5.3] on linux3
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlite3

次にデータベースと接続します。今回はmydb.sql3というデータベースを使ってみることにします。SQLiteの場合、利用したいデータベースはそのままファイルとして扱われるので、以下のコマンドを実行すると、mydb.sql3というファイルが存在しなければ新しく作成され、存在すればそのファイル(=データベース)と接続して利用できるようにします。

>>> conn = sqlite3.connect("mydb.sql3")

これでconnというデータベースへのコネクションオブジェクトが作成されます。データベースの操作は、このコネクションオブジェクトからカーソルオブジェクトを作って行います。

まず、各種データを収めるためのテーブル(mytable)を作ってみます。

>>> cur = conn.cursor()
>>> cur.execute('''CREATE TABLE mytable ( name TEXT, age INTEGER, phone TEXT);''')
<sqlite3.Cursor object at 0x7f788f99c6c0>

新しく作ったmytableにいくつかデータを登録してみます。

>>> cur.execute('''INSERT INTO mytable values('Alice', '30', '1234-5678');''')
<sqlite3.Cursor object at 0x7f788f99c6c0>
>>> cur.execute('''INSERT INTO mytable values('Bob', '25', '0000-1111');''')
<sqlite3.Cursor object at 0x7f788f99c6c0>
>>> cur.execute('''INSERT INTO mytable values('Carol', '20', '222-3333');''')
<sqlite3.Cursor object at 0x7f788f99c6c0>

コネクションオブジェクトにcommitコマンドを発行し、登録したデータを書き込みます。この操作をしないと登録したデータがデータベース(=ファイル)に書き込まれません。

>>> conn.commit()

次に、登録したデータを検索してみます。

>>> cur.execute('''SELECT * FROM mytable;''')
<sqlite3.Cursor object at 0x7f788f99c6c0>

この操作の結果、カーソルオブジェクトにはmytableにある全てのデータが抽出されているので、一つづつ取り出して表示してみます。

>>> for i in cur:
...   print i
... 
(u'Alice', 30, u'1234-5678')
(u'Bob', 25, u'0000-1111')
(u'Carol', 20, u'222-3333')
>>> exit()
$ 

SQLコマンドを用いた一連の操作はPostgreSQLやMySQLと変りませんが、SQLiteで面白いのはこうやって作成したデータベースが具体的なファイルとして存在していることです。

$ ls -l
合計 8,192
-rw-r--r-- 1 kojima users 2,048  2月 26日  17:53 mydb.sql3

このファイルはSQLiteの独自形式のためそのままでは読むことはできませんが、stringsコマンドで文字列を抽出してやると、登録したデータが記録されていることが分かります。

$ strings mydb.sql3 
SQLite format 3
tablemytablemytable
CREATE TABLE mytable ( name TEXT, age INTEGER, phone TEXT)
Carol
222-3333
0000-1111
Alice
1234-5678

SQLiteには、sqlite3という対話型コマンドが用意されており、このコマンドを使ってデータベースを操作することもできます。

$ sqlite3 mydb.sql3
SQLite version 3.7.10 2012-01-16 13:28:40
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> SELECT * FROM mytable;
Alice|30|1234-5678
Bob|25|0000-1111
Carol|20|222-3333
sqlite> SELECT name FROM mytable WHERE age >= 25;
Alice
Bob
sqlite> .quit
$

MySQLやPostgreSQLの場合、サーバ・クライアントモデルで設計されている都合上、自分一人しかユーザがいない環境で使う場合でも、データベースサーバを起動して、自分をそのサーバのユーザとして登録して、データベース作成の権限を与えて…、といった作業が必要になります。

このような手数を面倒だと感じる人には(筆者もそうですが⁠⁠、それらの作業をすっ飛ばして、直接データベースをファイルとして作ってしまうことができるSQLiteは魅力的でしょう。

データベースが不要になった時も、サーバ・クライアントモデルのデータベースではDROP TABLE mytable; といったSQLコマンドをデータベースサーバに送って削除してもらう必要がありますが、SQLiteでは不要になったファイルを削除すればデータベースを削除できます。

SQLiteのこのような手軽さは、筆者を含めてRDBソフトウェアは敷居が高いように感じていたユーザの認識を変えてくれることでしょう。事実、この手軽さに感心して、パッケージの依存情報管理データベースをPythonとSQLiteで書いてみる気になったのでした。

おすすめ記事

記事・ニュース一覧