H2   H2 Databaseの基本的な使い方
  2026/09/12


H2 Databaseのインストール

H2 Databaseは、 HSQLDB、Apache Derbyと並んで有名な、Java VMで動く軽量RDBMSです。
Windows11でのインストール方法です。

  1. 上記ホームページからAll Platforms(zip)をダウンロードし、適当なディレクトリに展開します。
    ここでは C:\Wintools\h2 に展開したとします。
  2. C:\Wintools\h2\bin\h2.bat を起動します。Webブラウザが立ち上がり、DB接続画面になるので、 JDBC URL:を初期表示の「jdbc:h2:~/test」から自分の好きな場所、 例えば「jdbc:h2:C:/usr/dbms/h2/testdb」に変更します。 ここでURLの末尾の部分は主ファイル名になるので、ディレクトリになっている必要はありません。 つまりこの例では C:\usr\dbms\h2 というディレクトリは予め作っておく必要がありますが、 その下に testdb というディレクトリは不要です。
    ユーザー、パスワードもそれぞれ "SA", "" でOKです。
  3. 接続するとUIでSQL実行が可能です。例えばCREATE TABLE文やSELECT文を実行できます。


コマンドライン(シェル)

H2は上記のWebのGUI画面が便利に使えますが、コマンドラインからも操作できます。
使い方はこちらに載っています。 簡単には次のようにして実行します。

java -cp C:\Wintools\h2\bin\h2-2.4.240.jar org.h2.tools.Shell -url jdbc:h2:C:/usr/dbms/h2/testdb -user testuser -password testpass
SQLを記述したファイルをコマンドラインから実行するには RUNSCRIPT というコマンドを実行します。
sql>RUNSCRIPT FROM './tables/item_categories.sql';
sql>RUNSCRIPT FROM './tables/items.sql';
"FROM"は省略できないので注意してください。 また、RUNSCRIPTで実行するファイルにはコメントを書けます。 コメントは「-- コメント」「/* コメント */」「// コメント」の3種類で書けます。 これは兄弟ともいえるRDBMSのHSQLDBの\iコマンドと全く共通です。
なお、H2 DatabaseもHSQLDBと同様に、URLの末尾に「jdbc:h2:C:/usr/dbms/h2/testdb;IFEXISTS=TRUE」 のように「;IFEXISTS=TRUE」を付けると、そのデータベースファイルが既に存在する場合に限り使用するようにできます。
これにより、URLのタイプミスの場合にあちこちに空のデータベースファイルが作られるのを防止できます。


サーバーモードで実行する

H2にもHSQLDBと同様のサーバーモードがあります。起動するには以下のようにします。

java -cp C:\Wintools\h2\bin\h2-2.4.240.jar org.h2.tools.Server -tcp -tcpPassword ShutDownPass123 -ifExists
H2には3種類のサーバーがあり、-tcp で最も一般的なTCPサーバーが起動します (これを付けないと3種類のサーバーが全て起動してしまいます)。
また-tcpPasswordは別のプロセスからシャットダウンを掛ける際の「合言葉」のようなものです。
-ifExistsはこのサーバーに接続しに来るURLのデータベースファイルがない場合に新規に作ろうとせずエラーにする指示です。
その他のオプションはこちらに記載されています。
では次にサーバーを停止する方法です。 簡単には、起動したコンソール上のプロセスをCTRL+CかSIGTERMで止めればよいのですが、 別のコンソールから止めるには次のようにします。
java -cp C:\Wintools\h2\bin\h2-2.4.240.jar org.h2.tools.Server -tcpShutdown "tcp://localhost" -tcpPassword ShutDownPass123
-tcpを付けない点に注意してください。-tcpを付けると、既存のプロセスを止めるとともに今後は自分が起動してしまいます。
起動、停止方法の次は接続方法です。一般的にはJavaプログラムからJDBCで接続することになると思いますが、 Shellツールから接続するには次のようにします。
java -cp C:\Wintools\h2\bin\h2-2.4.240.jar org.h2.tools.Shell -url jdbc:h2:tcp://localhost/./stdb -user testuser -password testpass
URL部分は jdbc:h2:tcp://localhost/{サーバー上のパス}の形状になります。 「サーバー上のパス」は絶対パスで指定することもできますし、上の例のようにサーバーを起動したときの作業ディレクトリからの相対パスにもできます。 また「jdbc:h2:tcp://localhost/~/stdb」のように「~」文字を入れると、 サーバーを起動したユーザーのホームディレクトリからの相対パスの意味になります。


コマンドラインからのスクリプト実行

対話型のシェルではなくて、バッチ処理などでスクリプトだけ実行したい場合は、RunScriptというJavaクラスを使います。
下の例では testdb というデータベースに接続し(なければ自動で作られます)、items.sqlに書かれたSQLを実行します。

java -cp C:\Wintools\h2\bin\h2-2.4.240.jar org.h2.tools.RunScript ^
  -url jdbc:h2:C:/usr/dbms/h2/database/testdb -user testuser -password testpass -script items.sql -showResults


データ型

H2のデータ型はHSQLDBによく似ています。 概ね同じ説明になりますが以下の通りです。

整数型にはTINYINT、SMALLINT、INTEGER、BIGINTがあり、これらはJava世界のそれぞれbyte、short、int、longの精度に対応します。

小数型にはREAL、FLOAT、DOUBLE PRECISIONがあり、REALはJavaのfloatに、DOUBLE PRECISIONはJavaのdoubleに対応します。 FLOATは精度を24以下にすればfloat、それ以上はdoubleに対応します。

小数部を持つことも持たないこともできる数値型としては、NUMERICとDECIMALがあります。 これらは全く同じもので、java.math.BigDecimalの精度に対応します。 これらは DECIMAL とだけ指定できる他に、DECIMAL(10)やDECIMAL(12,3)のように精度を指定できます。

真偽値を保持するBOOLEANという型は、SQL標準にならってTRUE、FALSE、UNKNOWNという値をとりえます。

テキスト文字列型には、SQL標準と同じ名前のデータ型としては、固定長のCHARACTER、可変長のCHARACTER VARYING、 大量データを保持できるCLOB(Character Large OBject)があります。 先頭から2つのよく知られた商用RDBMSでの別名、CHARとVARCHARも使えます。

バイナリ文字列型には、 固定長のBINARY、可変長のBINARY VARING、そしてBLOB(Binary Large OBject)があります。

日付/時刻型にはDATE、TIME、TIME WITH TIME ZONE, TIMESTAMP, TIMESTAMP WITH TIME ZONE があります。
WITH TIME ZONE有無の使い分けについては こちらの公式チュートリアルページに説明があります。


自動的に現在時刻を持つ列の定義

H2では、INSERTまたはUPDATEの度にシステム時刻を自動的に設定する列を定義できます。 こちらの公式Featuresのページの通り、

UDATE TIMESTAMP GENERATED ALWAYS CURRENT_TIMESTAMP
と列を定義すればOKです。なお、INSERTの際だけシステム時刻を自動的に設定する列は、
CDATE TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
で定義できます。


CSVファイルの読み書き

H2の特徴的な機能が CSVREAD、 CSVWRITE というSQL関数です。 特にCSVREADを使うと、CSVファイルを読み取り専用のテーブルであるかのように扱えます。

INSERT INTO ITEMS(ITEM_NAME,CATEGORY_ID,SIZE_DESC,PRICE,MAKER_DESC,NOTES,DISPLAY_ORDER)
 SELECT * FROM CSVREAD('./tables/items.dat', NULL, 'UTF-8', CHR(9))
このようにINSERT文と共に使うと、CSVファイル "items.dat" の内容を(本物の)テーブルであるITEMS表にロードできます。
ちなみに、表の列定義とCSVファイル上の列の並び順が完全に一致している場合は、(列名,列名,...)の記述は省略できます。
CSVREAD関数の最初の引数はファイル名、3番目は文字コード(エンコーディング)、 4番目の引数はフィールド区切り文字です。CHR(9)はASCIIコード9、つまりタブ文字の事です。
問題の2番目の引数ですが、ここには 'ID,NAME,SECTION"' のようにSQLで扱う列名を定義でき、 その場合はデータファイルの先頭から全ての行をデータとして扱います。 一方、上の例のようにNULLを渡した場合、データファイルの先頭の行が各列名を定義した見出し行であると扱い、 データは2行目から取り始めます。「列名を渡しつつ、1行目は読み飛ばす」ことはできないので注意が必要です。
なお、CSVREAD,CSVWRITEの第3引数以降には次のような書き方もあります。
INSERT INTO ITEMS(ITEM_NAME,CATEGORY_ID,SIZE_DESC,PRICE,MAKER_DESC,NOTES,DISPLAY_ORDER)
 SELECT * FROM CSVREAD('./tables/items.dat', NULL, 'charset=UTF-8 fieldSeparator=|')

●数値でORDER BYする
CSVREAD関数を使ったSQL文にORDER BY句を付けることは可能ですが、 CSVファイルの全ての列は文字列として読み取られる点に注意が必要です。 つまり整数を想定している列でORDER BYしても、「1, 2, 20, 21, 225, 3, 4…」のような並び順になってしまいます。 これは変ですね。
これを整数の順に並べ直すには? 2つ方法があって、一つはLPAD関数を使って ORDER BY LPAD(DISPLAY_ORDER, 3, '0') とすれば、「001,002,003,004,020,021,225…」と並びます。 もう一つはCAST関数を使って ORDER BY CAST(DISPLAY_ORDER AS INT)とする方法です。 後者の方が、取りうる最大桁数を予め知っておく必要がない点では優れていますが、 その代わり整数に変換できない値が1行でも含まれているとエラーになるので注意が必要です。

●集合関数も使える
CSVREADはこのように集合関数とも合わせて使えます。素晴らしい!

SELECT MAX(CAST(EMPLOYEE_NUM AS INT)), MIN(CAST(EMPLOYEE_NUM AS INT)), SUM(CAST(EMPLOYEE_NUM AS INT))
  FROM CSVREAD('C:/usr/dbms/csv/csvdb/shiten.dat', null, 'UTF-8', CHAR(9))
 WHERE AREA_CODE = '5'

●CSVを読むためだけに使う
CSVREADはINSERT文と合わせて使うとテーブルへのロードに使えますが、 JDBCなどのアプリケーションの中で、ただ単にCSVファイルを読むためだけにSELECT文として使うこともできます。 以下の例では、H2はインメモリデータベースとして立ち上げているもののデータベース操作は一切しておらず、 単にCSVファイルをアプリケーションに読み取って表示するだけでプログラムを終了しています。

public static void main(String args[]) {
    Connection conn = null;
    Statement stmt = null;

    try {
        Class.forName("org.h2.Driver");
        conn = DriverManager.getConnection("jdbc:h2:mem:");
        System.out.println("connected.");

        stmt = conn.createStatement();
        ResultSet rset = stmt.executeQuery(
                "SELECT SHITEN_NAME, ADDRESS, EMPLOYEE_NUM " +
                  " FROM CSVREAD('C:/usr/dbms/csv/csvdb/shiten.dat', null, 'UTF-8', CHAR(9)) " +
                  "WHERE AREA_CODE = '5' ORDER BY CAST(EMPLOYEE_NUM AS INT) DESC");
        while (rset.next()){
            System.out.println(rset.getString(1) + "\t" + rset.getString(2) + "\t" + rset.getString(3));
        }
        rset.close();
    } catch(Exception ex) {
        ex.printStackTrace();
    } finally {
        try {
            stmt.close();
            conn.close();
        } catch(SQLException ex) {
            ex.printStackTrace();
        }
    }
}


Csvクラスを使う

H2 DatabaseはJavaで書かれているのでAPIもJavaですが、その中にCsvというユーティリティクラスがあります。 これを使うとH2と無関係に単にCSVファイルを読むだけの事もできます。

public static void main(String args[]) throws SQLException {
    Csv csv = new Csv();
    csv.setFieldSeparatorRead('\t');
    ResultSet rs = csv.read("C:/usr/dbms/csv/csvdb/shiten.dat", null, "UTF-8");
    ResultSetMetaData meta = rs.getMetaData();
    while (rs.next()) {
        for (int i = 0; i < meta.getColumnCount(); i++) {
            System.out.println(meta.getColumnLabel(i + 1) + ": " + rs.getString(i + 1));
        }
        System.out.println();
    }
    rs.close();
}
H2のデータベースの機能と全く関係ないやんけ!はい、全く関係ないです。 こちらの例では、データベースにJDBC接続すらしていません。 H2で作成したデータベースは全く参照せずにCSVをResultSetオブジェクトとして読んでいます。 なお、Csv#readメソッドはSELECT文を発行しているわけではないため、条件を満たす行だけをWHERE句のように指定して抽出することはできません。 必ず全ての行をResultSetに取り込むので、rs.next() ループの中でJavaコードを書いて自分でフィルターする必要があります。


MERGE文について

業務アプリケーションを作っていてどのRDBMSでもよくあるのが、 「フォームで入力されたレコードをDBに登録しようとするとき、 主キーが一致する行が既にあれば何もせず、更新行数(affected rows)に0を返す。 既存行がなければ1行INSERTし、更新行数に1を返す」という処理を1つのSQLで書きたいケースです。 他のRDBMSには「INSERT 〜 IF NOT EXISTS」というそのものずばりの構文を持つ物もありますが、 H2の場合はMERGE文でこれができます。

CREATE TABLE SCORES(
  NAME VARCHAR NOT NULL PRIMARY KEY,
  SCORE INTEGER
)

INSERT INTO SCORES(NAME,SCORE) VALUES ('Taro', 90)
更新数:1

INSERT INTO SCORES(NAME,SCORE) VALUES ('Jiro', 85)
更新数:1

INSERT INTO SCORES(NAME,SCORE) VALUES ('Taro', 93)
ユニークインデックスまたはプライマリキー違反(エラー)

MERGE INTO SCORES USING DUAL ON NAME = 'Taro'
 WHEN MATCHED THEN UPDATE SET SCORE = 93
 WHEN NOT MATCHED THEN INSERT (NAME,SCORE) VALUES ('Taro', 93)
更新数:1 (Updateが実行された)

MERGE INTO SCORES USING DUAL ON NAME = 'Saburo'
 WHEN MATCHED THEN UPDATE SET SCORE = 81
 WHEN NOT MATCHED THEN INSERT (NAME,SCORE) VALUES ('Saburo', 81)
更新数:1 (Insertが実行された)

MERGE INTO SCORES USING DUAL ON NAME = 'Taro'
 WHEN NOT MATCHED THEN INSERT (NAME,SCORE) VALUES ('Taro', 94)
更新数:0 (該当行が既にあれば何もしないので)

MERGE INTO SCORES USING DUAL ON NAME = 'Goro'
 WHEN NOT MATCHED THEN INSERT (NAME,SCORE) VALUES ('Goro', 82)
更新数:1 (Insertが実行された)
MERGE文をこの目的に使う場合、ポインツ(複数形)は2つあります。 この機能を活用することで、従来SQL2つで実装していた処理を1つで行うことができます。

●バインド変数を最適化する書き方
上の例の最後の2つのSQLには欠点があります。それは 'Taro' や 'Goro' が2回出てくるため、 アプリケーションから呼び出す際にバインド変数(パラメータ)をNAME列に対しては2回渡す必要があります。
つまりアプリケーション側がここはMERGE文を使っているのかINSERT文を使っているのかを把握している必要があり、 保守性が悪くなってしまいます。
これを改良するのに以下のような書き方も可能です。これの出典はOracle公式の こちらです。

MERGE INTO SCORES a USING (SELECT 'Taro' AS NAME, 94 AS SCORE) newrec ON a.NAME = newrec.NAME
 WHEN NOT MATCHED THEN INSERT (NAME,SCORE) VALUES (newrec.NAME, newrec.SCORE)
更新数:0 (該当行が既にあれば何もしないので)

MERGE INTO SCORES a USING (SELECT 'Goro' AS NAME, 82 AS SCORE) newrec ON a.NAME = newrec.NAME
 WHEN NOT MATCHED THEN INSERT (NAME,SCORE) VALUES (newrec.NAME, newrec.SCORE)
更新数:1 (Insertが実行された)
このSQLだと、バインド変数の数も並びもINSERT文と全く同じできます。つまりこうなるということですね。
MERGE INTO SCORES a USING (SELECT ? AS NAME, ? AS SCORE) newrec ON a.NAME = newrec.NAME
 WHEN NOT MATCHED THEN INSERT (NAME,SCORE) VALUES (newrec.NAME, newrec.SCORE)
ただし、H2 Databaseの場合は注意点があります。H2はSQLのコンパイル時に全てのデータ型が確定している必要があるため、 上記を実行すると「Unknown data type」(不明なデータ型)というエラーになります。 次のようにCASTを使用してデータ型を明示する必要があります。
MERGE INTO SCORES a USING (SELECT CAST(? AS VARCHAR) AS NAME, CAST(? AS INT) AS SCORE) newrec ON a.NAME = newrec.NAME
 WHEN NOT MATCHED THEN INSERT (NAME,SCORE) VALUES (newrec.NAME, newrec.SCORE)

  もくじ
H2   (C) 2002-2025 MISUMI URANO (2023/08/23 - 2026/09/12)