NativeQueryじゃだめ?〜JPAクエリ表現ごとのパフォーマンス比較

このエントリはJavaEE Advent Calendar 201220日目です。昨日は@den2snさんのJavaを知らない世代が今からはじめるJavaEE開発でした。明日は@javaflavorさんです。

先月、岡山を訪れた時、Javaエバンジェリストである寺田さん(@yoshioterada)に「今仕事でやってるプロジェクトにJPAを導入したんですが、JPQLになじめなくって、素のSQLをNative Queryで投げてるんです。」という話をしたら、「Native Queryはパフォーマンス的に良くなかったはず…」との情報を頂きました。それが本当なら、今のプロジェクトが危ない!正確に言うと、今のプロジェクトにJPAを持ち込んだ自分の身が危ない!
ということで今回は、JPAに用意されている、Native Query、JPQL、criteria API 、それぞれのAPIのパフォーマンスを比較してみることにしました。

まず、それぞれの復習です。

  • Native Query
    • SQLを文字列で記述し、そのままDBに渡す方法
    • 長所:新しいことを覚える必要がないこと、Oracle関数のようなDB依存のクエリも投げられること。
    • 短所:JPAの意義であるデータ永続化装置が何かを気にしなくて良い、という精神をガン無視してること。DBをOracleからMySQLに変えよう(予算的な理由で)、とかになった時に、死にます(工数的な理由で)。
List<Product> products = em.createNativeQuery("select * from product where id = 100").getResultList();
  • JPQL
    • JPAはNativeQueryよりもこちら推し。SQLによく似た、JPQLという文を文字列で記述し、内部でSQLに変換してもらってクエリを投げる方法
    • 長所短所は、NativeQueryの全く逆。別の層の実装が何でも良い、というのは、Javaの思想に一致します。Write once, run anywhere。…ちょっと違うか。
List<Product> products = em.createQuery("select p from product p where p.id = 100").getResultList();
  • Criteria API
    • JPQLは所詮文字列のため、スペルミスがあった場合、実行時まで気づきません。この問題に対応するため、型付け言語Javaの長所を生かして、メソッドチェインでクエリを組み立てます。
    • 長所は、スペルミス等の構文異常をコンパイル時に気づけること。
    • 短所は、感覚的でないこと…。
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> r = cq.from(Product.class);
cq.select(r).where(cb.equal(r.get("id"), "100"));
List<Product> products = em.createQuery(cq).getResultList();
  • Entity直接指定(おまけ)
    • これぐらい簡単なクエリなら、Entityで直接指定して取ってくることも可能です。
Product product = em.find(Product.class, 100);

では、実験です。

今回DBには、軽量JavaDBであるH2を使います。使い方は簡単。zipを落としてきて、h2-xxx.jarにクラスパスを通すだけ。今回は使用したのはバージョン1.3.170です。
続いて、JPAの実装には、RIであるeclipseLinkを使います。使用バージョンは2.4.1。zipをDLして、jlib配下にあるeclipselink.jarと、jlib/jpa配下にあるjavax.persistence_2.0.4.v201112161009.jarにクラスパスを通します。
ソースはこちら
実験では10万件のデータをDBに投入しておき、ランダムな1件を引いてくることを1000回ずつ、各クエリごとに行います。それを5回実行した平均の結果がこれ。

クエリ 時間(ミリ秒)
Native Query 421
JPQL 1232
Criteria API 804
Entity 34

NativeQueryを基準とすると、Criteria APIはその2倍、JPQLは3倍の時間がかかりました。Entityでの指定はどれよりもダントツ速い結果になりました。
おそらく、NativeQueryはそのままDBに渡しているだけに対して、JPQLはJPQL文のパース・SQL文の構築・実行、Criteriaはパースが不要でSQLの構築・実行のみ、これがそのまま結果に出ているのだと思われます。Entityが異様に速いのはなぜなんだ…。

(追記)Entityの取得が速いのは、persistした後インスタンスすぐにGCされるわけではない(いつGCされるかはわからない)のでL1キャッシュに入るので、実際にDBを探索しているのではなく、ヒープに乗っているインスタンスへの参照が返されているだけだからでは、とのご指摘を頂きました。megascusさん、ありがとうございます。

今回は最も単純なクエリでしか実験しなかったため、結果的にはNativeQueryでも問題ない結果になってしまい、寺田さんの真意はわからずじまいでしたが、

  • テーブル結合した場合
  • Prepared Statementにして、文キャッシュを効かせた場合
  • EclipseLinkでなく、Hibernateなど、別の実装の場合

などなど、どんな場面でどんな結果になるのか、機会があればこれらも試してみたい所です。

(余談) 今回DBにH2を使ったのは、直前にjUnit実践入門で読んでいたためです。12章に紹介があって、設定によって、OracleのフリやPostgresのフリをさせられますし、インメモリDBとしても動かせるのでスローテストの解消にも使えそうです。H2を探ってみるのも面白そうです!



【補足】次エントリで追加実験を行い、JPQL遅くない!という結果になったので、合わせてご覧下さい!