一時テーブルを利用してバッチ処理を改善

1. 概要

履歴ファイル作成バッチのリファクタリング(ほぼリメイク)を行い、一時テーブルを利用して検索を複数ステップに分けることで可読性やテスタビリティを向上させて、さらに処理時間を5時間以上から40分にまで短縮させることができましたので、その方法について説明します。

2. 背景

私は決済に関するプロダクトの開発に携わっており、そのプロダクトには決済に関する取引履歴を検索してファイルとして出力する「取引履歴ファイル作成バッチ」が存在していました。

このバッチの機能としては管理者用のアプリケーションの取引履歴検索画面から検索条件を指定して、ファイル作成ボタンを押すことで、バッチが起動されCSVファイルが作成されるというものになります。

取引履歴ファイル作成バッチの全体像

またバッチ内部では、以下の処理フローでファイルが作成されていました。

改善前のバッチの処理フロー

  1. [検索処理] 5000件だけ取引履歴テーブルに検索を実行して、関連するテーブルを結合してCSVファイルに必要な情報を取得する
  2. [ファイル作成処理] 1をcsvに書き込む
  3. 1へ戻る

3. 課題

しかしながらこのバッチには以下の3つの課題が存在していました。

  1. 可読性が低い
  2. テスタビリティが低い
  3. 性能が低い

3.1. 課題1. 可読性が低い

1つ目の課題は「可読性が低い」ことです。

具体的には、このバッチの[検索処理]の際に実行されているクエリは取引履歴テーブルだけでなく他にも様々なテーブルを結合しており、クエリが非常に巨大でコードを読むのが辛いものでした。

select 
  transaction.hoge,
  transaction.fuga,
  table_a.hogehoge,
  ... 他にも沢山 
  table_z.huge

from transaction as t

join table_a as a on t.id = a.transaction_id
join table_b as b on a.id = b.a_id
... 他にもテーブル結合が沢山
join table_z as z on y.id = z.y_id

where t.transaction_date between '2022-01-01' and '2022-02-01';

3.2. 課題2. テスタビリティが低い

2つ目の課題は「テスタビリティが低い」ことです。

1つ目の課題にあるようにこのバッチの検索クエリはかなり複雑なもので、お世辞にもテストコードが書きやすいものではありませんでした。

実際、このバッチにはテストコードがなく、手動テストのみでした。

3.3. 課題3. 性能が低い

3つ目の課題は「性能が低いこと」です。

具体的にはこのクエリはレコード数が多い巨大な取引履歴テーブルに対して何度もアクセスを行っては、テーブル結合をしており、クエリ全体としての処理時間がとても長いものでした。

例えば1000万件が検索対象となる条件の場合だと、バッチ終了に5時間以上かかり、場合によっては推定で50時間もかかるという状況でした。

巨大な取引テーブルへの結合

4. 一時テーブルを利用したバッチ処理のリメイク

そこで以上の3つの課題を解決するために、「2つの一時テーブルを利用して段階的に検索処理を行う」ように実装をリメイクしました。

処理フロー

  1. [検索処理1] idだけ検索し、id_tmpテーブルにselect&insertで挿入
  2. [検索処理2] idから5000件ずつselectして、テーブル結合して、select&insertしてcsv_tmpテーブルに挿入
  3. [ファイル作成処理] csv_tmpテーブルから5000件ずつselectしてcsvファイルへ書き込み

4.1. 検索処理1「取引idだけ検索」

まず検索処理1では取引テーブルから取引idだけを検索し、id_tmpテーブルという一時テーブルにselect&insertします。 このように、クエリを小さくシンプルすることで実装が読みやすくなり、同時にテストも書きやすくなります。

insert into id_tmp

select 
  t.hoge,
  t.fuga,
  ...
  t.huga
from transaction as t

where transaction_date between '2022-01-01' and '2022-02-01'

4.2. 検索処理2「取引idを元に付帯情報を検索」

検索処理2では、まずid_tmpテーブルから取引idを少しずつselectして、それらの取引idから取引テーブルへレコードを取得するサブクエリを構築します。

つぎにそのサブクエリに他のテーブルを結合して、csv_tmpテーブルという一時テーブルにselect&insertをします。

ここで[検索処理1]で取得した取引idをもとに取引テーブルへのサブクエリを作成しておくことで、取引テーブルとの結合レコード数を減らして性能劣化を防ぐことができます。

-- 一時テーブルへinsert
insert into csv_tmp

-- insertする内容をselect
select 
  t.hoge,
  a.huga,
  b.huga,
  ...
  z.huge,

-- 取引履歴のサブクエリテーブルを作成
from (
  select *
  from transaction
  where id in ( 
     1, 2, 5, 10, ... , 100
  )
) as t

-- 小さくなったサブクエリテーブルにテーブル結合
join t.id = a.transaction_id
join table_b as b on a.id = b.a_id
... 他にもテーブル結合が沢山
join table_z as z on y.id = z.y_id;

4.3. ファイル作成処理

最後にファイル作成処理ではcsv_tmpテーブルからレコードを少しずつselectしてはcsvに書き込みを繰り返してcsvファイルを作成します。

5. 改善の結果

このように改善したことで以下の3つのメリットが得られるようになりました。

5.1. 可読性が向上

まずは検索時のクエリが小さくなったことで可読性が向上したことが挙げられます。

元々は取引テーブルに別のテーブルを沢山結合していたのですが、クエリを段階的に実行する形式に変えることで1つ1つのクエリは小さくシンプルになり、可読性が向上しました。

5.2. テスタビリティが向上

次に挙げられる成果はテスタビリティが向上したことです。

1つ1つのクエリが小さくシンプルになったことで、それぞれのシンプルなクエリに対してテストを書けば良くなったので、結果としてテストが書きやすいようになりました。

それぞれのクエリごとにテストを書けるようになったことで、全体としてのテストケース数を抑えつつ網羅性を上げることもできるようになりました。

5.3. 処理性能が向上

最後に「処理性能が向上したこと」が挙げられます。

元々のクエリでは巨大なテーブルに対してjoinを行っていたためテーブル結合のコストが高くクエリの性能が劣化していました。

そのため、改善後のクエリではjoinではなく事前に検索した取引idを用いて取引テーブルから少しだけレコードを取得し、その結果のサブクエリテーブルに対して他のテーブルをjoinするようにすることで、テーブルの結合行数を減らして性能を上げることができました。

具体的には、処理時間が5時間以上から40分程度に短縮されました。

6. まとめ

今回は可読性・テスタビリティ・性能に課題があったバッチを一時テーブルを用いることで処理を改善する方法について説明しました。

なお、この方法のデメリットとしては管理するテーブル数が増加することがありますので、使い所は注意すべきかなと思います。

また今回はDBがRDB縛りだったりしたので、根本的に性能を上げたいような場合は別のアプローチを取ることをおすすめします。

以上

国内外のフィンテック企業のテックブログまとめ

はじめに

本記事は20230805時点で存在するフィンテック企業のテックブログについてまとめたリンク集です。 国内企業と国外企業に分けて章立てしていますが、ほとんど国内企業ばかりになってますね、、、もっといろんな企業を調べられるようにしたいです。

また以下のフォーマットで記入しています。 ex) - 企業名(事業ジャンル): URL

国内企業

国外企業

以上

コード例で深ぼるEffectiveJava~「第2章コンストラクタの代わりにstaticファクトリメソッドの使用を検討する」の深掘り~

本記事では名著Effective Java(第3版)で言及されているtipsをより深掘りするために、さまざまなコード例を交えて考えるというものになります。

今回は第2章 オブジェクトの生成と消滅の中の項目1「コンストラクタの代わりにstaticファクトリメソッドを検討する」について深掘りします。

目次

  1. Staticファクトリメソッドとは?
  2. Staticファクトリメソッドのメリット
  3. Staticファクトリメソッドのデメリット
  4. おわりに

1. Staticファクトリメソッドとは

Staticファクトリメソッドとは、「クラスのインスタンスを返す単なるstaticのメソッド」であり、クラスのコンストラクタの代替手段になります。

実装時のポイントは「publicにしたstaticファクトリメソッドから、privateのコンストラクタを呼び出すことでインスタンス生成をする」ということです。

Effective Java内ではBooleanのvalueOf()メソッドが例として挙げられていましたが、よりweb開発者に向けた例としてUserクラスでStaticファクトリメソッドの説明をします。

public class User {
    private int id;
    private String name;

    // プライベートコンストラクタ
    private User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // staticファクトリメソッド
    public static User createUser(int id, String name) {
        return new User(id, name);
    }

    public static void main(String[] args) {
        User user = User.createUser(1, "Alice");
        System.out.println(user); // 出力: User{id=1, name='Alice'}
    }
}

まずこのUserクラスはidとnameをフィールドに持っており、コンストラクタのメソッドの可視性はprivateになっています。

これに対しstaticファクトリメソッドはpublicになっており、ファクトリメソッド内からprivateのコンストラクタを呼び出してインスタンス生成をしています。

2. Staticファクトリメソッドのメリット

2.1. コンストラクタと異なり、名前を持つこと

1点目は「コンストラクタと異なり名前を持つこと」です。

噛み砕いて説明すると、複数の目的を持ったコンストラクタが複数存在する場合にコンストラクタだと同じ名称のメソッドになってしまうためメソッド名から役割を判別できないが、staticファクトリメソッドであればメソッド名を変えることで役割を明示できて可読性が上がるという感じです。

Userクラスを例に説明すると、管理者ユーザと通常ユーザで生成時の処理や渡したいパラメタが異なる際にcreateAdminUser()、createNormalUser()とファクトリメソッドを分けてあげることで、インスタンス生成のメソッドのわかりやすさが上がるという感じです。

public class User {
    private int id;
    private String name;

    // プライベートコンストラクタ
    private User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // 管理者ユーザーを生成するstaticファクトリメソッド
    public static User createAdminUser(int id, String name) {
        // ここで管理者ユーザーの生成ロジックを追加できる
        return new User(id, name);
    }

    // 通常ユーザーを生成するstaticファクトリメソッド
    public static User createNormalUser(int id, String name) {
        // ここで通常ユーザーの生成ロジックを追加できる
        return new User(id, name);
    }

    // その他のメソッド...
}

2.2. コンストラクタと異なり、その呼び出しごとに新たなオブジェクトを生成する必要がないこと

2点目は「コンストラクタと異なり、その呼び出しごとに新たなオブジェクトを生成する必要がないこと」です。

平たく説明すると、再利用可能なオブジェクトに関してはstaticファクトリメソッドを使うことでキャッシュできてメモリ効率やパフォーマンスが向上して良いですよという感じです。

こちらもUserクラスで説明すると以下の通りで、ゲストユーザに関しては一度作成したら、ファクトリメソッドが何回呼ばれても元のインスタンスを返すことで、インスタンス生成のオーバーヘッドを削減できるという感じです。

public class User {
    private int id;
    private String name;
    
    private static final User GUEST_USER = new User(0, "Guest");

    // プライベートコンストラクタ
    private User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // staticファクトリメソッド
    public static User createUser(int id, String name) {
        if (id == 0 && "Guest".equals(name)) {
            return GUEST_USER; // 既存のインスタンスを再利用
        }
        return new User(id, name); // 新しいインスタンスを生成
    }
}

2.3. コンストラクタと異なり、メソッドの戻り値型の任意のサブタイプのオブジェクトを返せること

3点目は「コンストラクタと異なり、メソッドの戻り値型の任意のサブタイプのオブジェクトを返せること」です。

具体的にはUserインタフェースのファクトリメソッド内でUserクラスのサブタイプとしてNormalUserを生成するのかAdminUserを生成するのか柔軟に切り替えられますとという感じのメリットになります。

まずインタフェースとしてUserを定義します。

public interface User {
    int getId();
    String getName();
}

次にUserインタフェースの実装クラスとしてAdminUserとNormalUserを定義します。

class NormalUser implements User {
  // 実装はAdminUserと同じ
}

class AdminUser implements User {
  // 実装はNormalUserと同じ
}

最後に、Userインタフェースの実装クラスを返すファクトリクラスを定義し、その中でstaticファクトリメソッドを定義します。ここではisAdminがtrueかfalseかによって、管理者ユーザを返すか通常ユーザを返すかを切り替えられるようになっています。

public class UserFactory {
    public static User createUser(int id, String name, boolean isAdmin) {
        if (isAdmin) {
            // 管理者ユーザーを返す
            return new AdminUser(id, name); 
        } else {
            // 通常ユーザーを返す
            return new NormalUser(id, name); 
        }
    }
}

2.4. 返されるオブジェクトのクラスは、入力パラメータの値に応じて呼び出しごとに変えられること

4点目は「返されるオブジェクトのクラスは、入力パラメータの値に応じて呼び出しごとに変えられること」です。

例えばUserクラスで説明すると、ファクトリメソッドの引数のisAdminがtrueならAdminUserを生成し、falseならNormalUserを生成するといった感じです。

コード例に関しては長所3と同じです。

2.5. 返されるオブジェクトのクラスは、そのstaticファクトリメソッドを含むクラスが書かれた時点で存在する必要がない

5点目は、「返されるオブジェクトのクラスは、そのstaticファクトリメソッドを含むクラスが書かれた時点で存在する必要がない」ということらしいです。

平たく説明すると、「ファクトリメソッドにすることで返すクラスを柔軟に変更できるから、将来的に生成したいクラスが変わっても容易に実装を切り替えられて便利」ということだと思います。

こちらもUserクラスで説明すると長所3とほぼ同じで以下のような感じになります。

public interface User {
    int getId();
    String getName();
}

public class UserFactory {
    // Userインタフェースを実装しているクラスのインスタンスを生成する
    public static User createUser(int id, String name) {
        // 一旦は、NormalUserクラスのインスタンスを返すようにしているが、将来的に別のUserインタフェースの実装を返すことも可能
        return new NormalUser(id, name); 
    }
}

class NormalUser implements User {
    private int id;
    private String name;

    public NormalUser(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

// 将来的に以下のようなUserを返すようにすることも可能
class AdminUser implements User {
    private int id;
    private String name;

    public AdminUser(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

ポイントは、Userインタフェースを実装したクラスをファクトリメソッドの返り値に設定しておくことで、将来的に別のUserを生成したくなっても簡単に実装が切り替えられるということです。

3. Staticファクトリメソッドのデメリット

3.1. publicあるいはprotectedのコンストラクタを持たないクラスのサブクラスは作れないこと

短所の1点目は「publicあるいはprotectedのコンストラクタを持たないクラスのサブクラスは作れないこと」です。

例えばUserクラスのコンストラクがprivateのものしかない以下のような場合を考えます。

public class User {
    private int id;
    private String name;

    // プライベートコンストラクタ
    private User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // staticファクトリメソッド
    public static User createUser(int id, String name) {
        return new User(id, name);
    }

    // その他のメソッド...
}

この場合はUserクラスを継承したAdminUserを定義することができないので、Userクラスのサブタイプを返すファクトリメソッドも作成できません。

public class AdminUser extends User { // エラー: Userクラスにアクセス可能なコンストラクタがない
    // ...
}

継承できない不変なクラスを作成するためにprivateのコンストラクタだけ用意する場合は、ファクトリメソッドを使うことが難しくなるため、その場合はコンポジションなど別の設計を導入する必要があると思われます。

その場合の対応は以下の通りです。

まずprivateコンストラクタしか持たせたくないUserクラスにUserクラスのファクトリメソッドを定義します。

public class User {
    private int id;
    private String name;

    // プライベートコンストラクタ
    private User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // staticファクトリメソッド
    public static User createUser(int id, String name) {
        return new User(id, name);
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

次にAdminUserクラスのフィールドにUserクラスを定義し、AdminUserのファクトリメソッドを用意して、そこでUserのファクトリメソッドを呼ぶ形の実装にします。 つまり、AdminUserクラスがUserクラスをCompose(内包)するようにして、Userクラスのサブタイプのような役割をAdminUserに持たせるという実装を取ることで、Userクラスのコンストラクタをprivateにした状態で実質サブタイプの役割を果たすクラスのstaticファクトリメソッドが使用できるようになります。

public class AdminUser {
    private User user;
    
    // コンストラクタではUserインスタンスを受け取ります
    public AdminUser(User user) {
        this.user = user;
    }

    // Userのメソッドを委譲
    public int getId() {
        return user.getId();
    }

    public String getName() {
        return user.getName();
    }

    // 管理者ユーザー固有のメソッド
    public void performAdminTask() {
        // 管理者タスクのロジック
    }

    // AdminUserのファクトリメソッド
    public static AdminUser createAdminUser(int id, String name) {
        User user = User.createUser(id, name); // Userのファクトリメソッドを使用
        return new AdminUser(user); // AdminUserインスタンスを生成
    }
}

3.2. プログラマがstaticファクトリメソッドを見つけるのが難しいこと

2点目の短所は「プログラマがstaticファクトリメソッドを見つけるのが難しいこと」です。

これはJavadocのようなAPIドキュメンテーションツールでコンストラクタがわかりやすくマーキングされるのに対して、ファクトリメソッドはマーキングされないことが多いため、目立たないということを言っているそうです。

ただ筆者的にはこの点はコードのコメントで補えば良い気もしています。

おわりに

この記事では「コンストラクタの代わりにstaticファクトリメソッドを検討する」という項目をUserクラスを用いて説明しました。 Effective Javaは有用な内容は書いてあるものの、コード例が少ない項目もあるので適宜コードに起こして理解を深めることが大事ですね。

参考

  • ブロック, ジョシュア (2018). Effective Java (柴田/芳樹, 翻訳). 第3版. 丸善出版. (原著出版日: 2018/10/30)

オフライン電子決済の事例

みなさん、「キャッシュレス決済したこと」はありますか? おそらく、多くの人が「1回はある」と答えると思います。

一方で、「オフラインでキャッシュレス決済したこと」がある人はどれほどいるでしょうか? おそらく、Yesと答える人はさほどいないのではないでしょうか?

本記事では、オフライン電子決済の事例の紹介を通して、オフライン電子決済の技術的な背景を知るきっかけ作りをしたいと思います。

目次

  1. ユーザのみオフラインでよいPayPay
  2. ユーザも店舗もオフラインで決済が可能なMondex
  3. ガラケーでのオフライン決済「DigiTally」
  4. 実はオフライン決済にも対応しているらしいデジタル中華人民元
  5. おわりに

1. ユーザのみオフラインでよいPayPay

まず最近注目のオフライン決済といえば、PayPayのオフライン決済機能は外せないでしょう[1]。

PayPayのオフライン決済機能は、支払いを行うクライアントのアプリがオフラインであっても店舗がオンラインであれば、支払いが可能というものです。

また、セキュリティの確保のため1日あたりのオフライン決済可能額が設けられているようです。

ユースケースとしては、以下の2点が想定されているようです。

  1. 屋内や地下などで店舗の端末にはWi-Fi経由でネットワークに接続されているが、利用者のモバイルネットワーク通信は繋がらない場合
  2. 大人数が集まるイベント会場でネットワークが混線するような場合

この機能は完全オフラインの決済ではないものの、日常生活で時々発生する不便を解消している点がとてもユニークで面白いですね。

また、この機能のリリースを受けて、「完全オフラインの決済が必要になるシーンは災害時、ネットワーク断絶時などが想定されるが、そもそも稀でそんな時は決済しないのでは?」ということが頭に浮かびました。

2. ユーザも店舗もオフラインで決済が可能なMondex

実は世界初のオフラインでの電子決済を実現したのは先述のPayPayでもなければ、決済大手のMastercardでもありません。

オフライン決済を世界初で実現したのは「Mondex」という商品で、誕生年はなんと1997年です[2, 3]。

Mondexの仕組みを簡単に説明すると、ユーザはMondexカードと個人間送金のためのデバイスを持ち歩くことで、別のユーザとオフラインでも送金が可能というものです。

ここでは深く掘り下げませんが、「トークン型の電子現金方式」に分類される決済手段であり、お金のトークンをユーザ間で送信するたびにトークンに電子署名を付加していくことで、不正利用発生時の追跡可能性を担保するものです。

Mondexは専用デバイスを持ち歩かなければいけないなど利便性の問題で普及しなかったそうですが、2023年現在、中央銀行デジタル通貨を検討する中で改めてオフライン決済可能なMondexの方式が注目されているようです[4]。

3. ガラケーでのオフライン決済「DigiTally」

また、他のオフライン決済の方式としては2017年にパイロット実験が行われた「DigiTally」と呼ばれるものがあります[3, 5]。

「DigiTally」はフィーチャーフォンでも利用できることを想定しており、送受信するユーザのフィーチャーフォンの電話番号と送金額をもとに生成した番号を相手に伝えることで送金を実現します。

ネットワークが繋がらず、銀行口座の保有割合が低い途上国でのユースケースが主に想定されているようです。

4. 実はオフライン決済にも対応しているらしいデジタル中華人民元

最後に現在最もユーザ数が多いオフライン決済としてデジタル人民元を紹介したいと思います[6]。

デジタル人民元自体は2021年にオンライン決済が可能な通貨としてローンチしており、世界の主要国の中で速攻でリリースされたとして話題になっていました。

2023年現在でアメリカも日本もCBDCをリリースすることすらしていない中で、ユーザ数が世界一多いデジタル通貨が今やオフラインでも利用できるとのことで衝撃です。

5. おわりに

本記事ではオフライン決済が導入されている事例を中心にまとめてご紹介しました。

これからオフライン決済がどこまで広がっていくのかはわかりませんが、いつか本当にお札や硬貨を持ち歩かなくてもよい時代が来たら最高ですね。

参考

[1] paypay株式会社. (2023, July).「PayPay」に国内主要コード決済初、インターネットにつながっていなくても決済ができる機能(特許出願中)を搭載!. https://about.paypay.ne.jp/pr/20230720/01/

[2] 日立評論. (1997, May). 電子マネーシステム「モンデックス」の新展開. https://www.hitachihyoron.com/jp/pdf/1997/05/1997_05_06.pdf

[3] CBC. (2022 Feb). The Mondex electronic money card hoped to make cash obsolete. https://www.cbc.ca/archives/the-mondex-electronic-money-card-hoped-to-make-cash-obsolete-1.5454888

[4] 日本銀行 決済機構局. (2020, July). 中銀デジタル通貨が現金同等の機能を持つための技術的課題. https://www.boj.or.jp/research/brp/psr/data/psrb200702.pdf

[5] Baqer, K., & Anderson, R. (2017). DigiTally: Piloting Offline Payments for Phones. https://www.usenix.org/system/files/conference/soups2017/soups2017-baqer.pdf

[6] Central Banking. (2023). PBoC launches offline CBDC payments. https://www.centralbanking.com/fintech/cbdc/7954211/pboc-launches-offline-cbdc-payments

[7] The Wall Street Jornal. China Creates Its Own Digital Currency, a First for Major Economy. https://www.wsj.com/articles/china-creates-its-own-digital-currency-a-first-for-major-economy-11617634118

[8] Bank of international(2023). Project Polaris: Handbook for offline payments with CBDC .https://www.bis.org/publ/othp64.pdf

正規表現のコンパイルをメモ化すると若干速くなるらしいのでローカル環境で検証してみた

「Effective Java 第3版」の第2章の項目6に「不必要なオブジェクトの生成を避ける」という内容のものがあり、そこで正規表現コンパイルはクラス変数にキャッシングした方がパフォーマンスを大幅に改善できるとのことが書いてあったので、実際に試して確認してみました。

検証環境

条件は以下の通りになりますが、環境によって結果は変わると思いますので参考までにどうぞ。

効率の悪い書き方

ちなみに改善前の"効率が悪い"書き方は以下の通りです。

String regex = "[a-zA-Z]+";
String input = "HelloWorld";
long startTime = System.nanoTime();
for(int i=0; i<1000000; i++) {
    // パターンの比較をするたびに、パターンのコンパイルが実行されるため非効率
    Pattern pattern = Pattern.compile(regex);
    boolean isMatch = pattern.matcher(input).matches();
}
long endTime = System.nanoTime();
System.out.println("Execution time without precompilation: " + (endTime - startTime)/1e6 + "ms");

効率の良い書き方

次に一度パターンをコンパイルして、変数patternにキャシングして使い回す"効率が良い"書き方は以下の通りです。

import java.util.regex.Pattern;

// パターンのコンパイルは1回だけ実行されるため、
// パターンの比較が増えてもパターンのコンパイル時間は一定であり効率的
Pattern pattern = Pattern.compile("[a-zA-Z]+");

String input = "HelloWorld";
long startTime = System.nanoTime();
for(int i=0; i<1000000; i++) {
    boolean isMatch = pattern.matcher(input).matches();
}
long endTime = System.nanoTime();
System.out.println("Execution time with precompilation: " + (endTime - startTime)/1e6 + "ms");

検証結果

検証結果は以下の通りであり、コンパイルを事前に行ってキャッシングをすると、キャッシングしない場合に比べて3倍ほど速くなるという結果が出ました。

効率の悪い書き方: 470.19 ms
効率の良い書き方: 155.09 ms

オーダーが変わるほど計算量が低くなるわけではないですが、地味に効いてくるポイントだと思いますので、頭の片隅においておくとよいかもしれません。

おわりに

今回はEffective Javaに掲載されていた正規表現コンパイルをキャッシングすることによる性能改善を試してみました。

また正規表現の比較のコストが高いことから、正規表現の比較前にガード節としてif文を書いておいて、そもそも比較する必要がない場合は処理を抜けるという方法も性能改善の観点から有用だと思います。(「リファクタリング」に書いてあったような、、、)

こういった地味だけどちょっと速くなるみたいなポイントも積極的に抑えていきたいと思いました~

おしまい

JOOQのbulk update, batch update, for文でのupdateはどれくらいの速度差があるのか?

目次

  1. はじめに
  2. jOOQとは
  3. 1万件のレコードをjOOQでアップデートする際の性能
  4. おわりに

1. はじめに

みなさんDBのデータ更新をする際になんとなくupdateしてませんか?

実は数万件単位のupdateをする際に、愚直にupdateするのとbulk updateするのとでは場合によっては100倍もの速度差がでることがあります。

本記事は, なんとなく機能要件を満たす開発スキルが身に付いてきた駆け出しのソフトウェアエンジニアに向けて効率的なupdateの方法とその仕組みについてご紹介したいと思います。

2. JOOQとは

読者のみなさんがWeb開発者なのであれば、データベースへアクセスするためにSQLやORM(Object Rerational Mapper)などを使ったことがあると思います。

例えば、RubyフレームワークRuby on Railsを使用している場合であればActive Record、LaravelではEloquentなどのORMを使ったことがあるかもしれません。

本記事ではJavaのORMであるjOOQに焦点を当てて効率的なupdateとその仕組みについて見ていきたいと思います。

jOOQとはJavaのORMであり、javaコンパイラからSQLシンタックスを生成することで型安全にしています[1]。

JOOQによるDBのupdate文には主に次の3種類があります。

  1. update
  2. batch update
  3. bulk update

1つ目は純粋なupdate文で1文が1クエリに相当します。 1文が1クエリに相当するので100回だけSQL文を実行すれば100回DBサーバへSQL文が送信され、DBサーバ内で100回接続・切断が行われることになります。 更新したい行ごとにクエリを書き分けられるため複数行の更新を柔軟に行えることが利点ですが、DBサーバへの通信のオーバヘッドとDBサーバ内のDB接続・切断のオーバーヘッドが大きいのが欠点です。

2つ目はbatch updateであり、複数のSQL文をまとめてDBサーバへ送信して、DBサーバでは1つのSQL文ずつ実行される方法です[2]。 例えば以下の例はInsertですが、updateに関しても以下のように柔軟な書き方で複数のクエリをまとめて書くことができます。

create.batch(
    create.insertInto(AUTHOR, ID, FIRST_NAME, LAST_NAME).values(1, "Erich"  , "Gamma"    ),
    create.insertInto(AUTHOR, ID, FIRST_NAME, LAST_NAME).values(2, "Richard", "Helm"     ),
    create.insertInto(AUTHOR, ID, FIRST_NAME, LAST_NAME).values(3, "Ralph"  , "Johnson"  ),
    create.insertInto(AUTHOR, ID, FIRST_NAME, LAST_NAME).values(4, "John"   , "Vlissides"))
.execute();

batch updateの利点は複数のクエリを1回の通信でDBサーバへ送れるため、DBサーバへの通信のオーバーヘッドが小さいことが挙げられます。 また柔軟にクエリの中身を構築できる点も利点として挙げられるでしょう。 一方で欠点としては、DB接続・切断がクエリごとに発生してしまうことによるオーバーヘッドが1つ目のupdate文と同様にあります。

3つ目はbulk updateであり、IN区やBETWEEN区で複数行がヒットした際に1つのクエリで一括でテーブルを更新する方法です。 例えば100行の更新をbulk updateで行う場合は、DBサーバへ1回SQL文を送信して、DBサーバ内で1回接続して、複数行更新して、DBを切断するという流れで処理がされます。 この方法の利点は1つのクエリで複数行の更新を行うためDBサーバへの通信のオーバヘッドが小さいという点があります。 また複数行の更新が走るものの、クエリとしては1つなので、DB接続・切断のオーバーヘッドを小さくできるという利点があります。 一方で欠点としては、クエリを書く際の柔軟性が低く、行ごとに更新したい内容が大きく異なる場合はクエリが複雑化する可能性があります。

3. 1万件のレコードをjOOQでアップデートする際の性能

先ほど説明した3種類のupdateの方法ですが、それぞれどれほどの性能差があるのでしょうか?

jOOQの公式ブログに1万件のupdate文を実行した実験が上がっていたので、参照してみましょう[3]。

3.1. 実験の条件

実験の条件は以下の通りになります。

  • 対象のテーブル
CREATE TABLE post (
  id INT NOT NULL PRIMARY KEY,
  text VARCHAR2(1000) NOT NULL,
  archived NUMBER(1) NOT NULL CHECK (archived IN (0, 1)),
  creation_date DATE NOT NULL
);
 
CREATE INDEX post_creation_date_i ON post (creation_date);
  • 初期データとして10000行のレコードをテーブルに挿入
INSERT INTO post
SELECT
  level,
  lpad('a', 1000, 'a'),
  0 AS archived,
  DATE '2017-01-01' + (level / 100)
FROM dual
CONNECT BY level <= 10000;
 
EXEC dbms_stats.gather_table_stats('TEST', 'POST');
  • WebサーバとDBサーバはネットワークが別れているため、通信コストがかかる

3.2. 実験結果

実験結果は以下の表の通りです。

なんと、bulk updateと純粋なupdateでは100倍もの差がついています。

「bulk updateが早いのは知っているよ」という人でも、batch updateとbulk updateでも約5倍ほど速度差がでる場合があるというのには驚きではないでしょうか?

query type execution time(second)
1 forループによるupdate, キャッシュなし PT4.546S
2 forループによるupdate, prepared statementのキャッシュあり PT3.52S
3 batch update PT0.144S
4 bulk update PT0.028S

4. おわりに

本記事ではupdate, batch update, bulk updateについて各方法のオーバーヘッド、性能差について解説しました。

それぞれ一長一短であるものの、複数件のupdateを行う場合はできるだけbulk updateを用いて、bulk updateではSQL文が過度に複雑化してしまうという場合のみbatch updateにするという形で実装を行うと良いのではないでしょうか。

以上

参考

[1] https://www.jooq.org/
[2] jOOQ, Batch execution, https://www.jooq.org/doc/latest/manual/sql-execution/crud-with-updatablerecords/batch-execution-for-crud/ https://qiita.com/keita_ide78/items/48fd62e9505d2ddad51f
[3] jOOQ, The Performance Difference Between SQL Row-by-row Updating, Batch Updating, and Bulk Updating, https://blog.jooq.org/the-performance-difference-between-sql-row-by-row-updating-batch-updating-and-bulk-updating/

なんでもかんでもテスト書けば良いわけではない, 「単体テストの考え方/使い方」

  1. なんでもテストすれば良いと思っていた
  2. ビジネスロジックからテストせよ
  3. テストも負債

1. なんでもテストすれば良いと思っていた

ソフトウェアエンジニアになって自動テストを覚えてから、その便利さゆえに全てのメソッドに単体テストを実装しないといけないと思っていた。

たしかKent Beckの「テスト駆動開発」という名著の中でも、全てのメソッドをテストするんだ!てきなことを言っていた気もする(うろ覚え)。

しかしながら、実際に現場に出てみると当たり前のように時間の制約があるため全てのメソッドにテストを入れることは不可能に近いことに気が付く。

ベンチャーのように時間がない中でも本当に単体テストは全てのメソッドに対して用意すべきなのだろうか?

2. ビジネスロジックからテストせよ

この問いに対して「単体テストの考え方/使い方」(Vladimir Khorikov 著)では「ビジネスロジックからテストせよ」とのことが書かれている。

その心は、「システムに取って重要度の高いコードを優先的にテストせよ」だと思う。

言われてみれば当たり前のことだが、自動テストという便利なトンカチを持ってから、全てが釘に見えていた自分ははっとさせられた。

確かにプロダクションコードの中には、テストしなくても明らかなコードもあるし、一方でビジネスロジックのように重要度の高いコードもある。

そんな中で、時間がない中でテストを作って元が取りやすいのは明らかにビジネスロジックである。

ついでに考えるとDDDやクリーンアーキテクチャも同じようなことを言っており, ビジネスロジックに重点をおいて開発を進めようということは実装においてもテストにおいても重要なんだと思われる。

3. テストも負債

また、「単体テストの考え方/使い方」で面白かった点がもう1つある。

単体テストも技術負債」という考え方である。

「え?単体テストがあればあるほど、リファクタリングがやりやすくなって、技術負債はなくなるんじゃないの?」と私は思ってしまったが、言われてみれば当たり前のことである。

なぜなら、テストも当然のように手入れが必要で保守の対象になるためである。

やたらめったら作った割に重要なコードをテストしているわけではないのに、保守にばかり時間がかかるという状況になってしまえば、全体としては生産性はマイナスになっているわけである。

思い当たる節がとてもある、、、

重要度の高いコードから効果的にテストすることを念頭において頑張りたい、、、、

以上

参考