Hatena::ブログ(Diary)

Dev3TechHack

2012-12-19

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

02:09

このエントリはJavaEE Advent Calendar 2012の20日目です。昨日は@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の意義であるデータ永続化装置が何かを気にしなくて良い、という精神をガン無視してること。DBOracleから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 Query421
JPQL1232
Criteria API804
Entity34

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など、別の実装の場合

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

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



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

megascusmegascus 2012/12/20 04:33 Entityが早いのは多分キャッシュが効いているからですね。
同じエンティティを複数回取ろうとした場合、最初のEntityの取得にはSQLが発行されますが、それ以外はメモリ上の同じインスタンスへのリファレンスが返されます。

今回のコードの場合、最初にEntityをpersistする時にメモリ上に保持してしまい、それのインスタンスが取得されているのではないでしょうか。

寺田さんの「Native Queryはパフォーマンス的に良くなかったはず…」に関しては不明です。。。
JDBCに比べるとオブジェクトの型を指定できないのでリフレクションによるオーバーヘッドはありそうですが。

hiranasuhiranasu 2012/12/20 09:22 megascusさん

たしかに、EntityはpersistしてもGCされるタイミングはわからないので、今回の場合はヒープに残っていたんですね。同列に書くのはまずかったかもしれません。ご指摘ありがとうございます!

hagi44hagi44 2012/12/20 11:57 SQL トレース出力すればキャッシュかどうかわかるんじゃないでしょうか?手元に環境ないので確認できませんが・・

megascusmegascus 2012/12/20 21:51 いやいや、残っていたのかもではなく、仕様上キャッシュされることが決まってます。
L1キャッシュと呼ばれているものですね。

https://blogs.oracle.com/carolmcdonald/entry/jpa_caching

確かめるにはhagi44氏がおっしゃっているようにSQLをトレース出力すれば一発ですね。

hiranasuhiranasu 2012/12/21 00:43 hagi44 さん megascus さん
SQLトレースしてみたら、何も出ませんでした。

■ 追加したプロパティ
<property name="eclipselink.logging.level.sql" value="FINE"/>

■ ログ
…(データ投入のINSERT文が大量に)

[EL Fine]: sql: 2012-12-21 00:34:08.417--ClientSession(1549421830)--Connection(978045210)--INSERT INTO PRODUCT (ID, NAME) VALUES (?, ?)
bind => [2 parameters bound]
[EL Fine]: sql: 2012-12-21 00:34:08.417--ClientSession(1549421830)--Connection(978045210)--INSERT INTO PRODUCT (ID, NAME) VALUES (?, ?)
bind => [2 parameters bound]
35 (findを1,000回実行した後の時間測定表示)

仰る通り、キャッシュから引いてるんですね。
persistしてcommitしたら、デタッチ状態になって、そしたら適当なタイミングGCに回収されるんだと認識していました。
ご指摘、ありがとうございます!!

nowokaynowokay 2012/12/26 01:40 JPQLの場合は、NamedQueryにするとJPQLのパース結果をキャッシュするのでパフォーマンスがあがると思います。
その場合の結果も気になります。

寺田 佳央寺田 佳央 2012/12/28 00:53 すいません、時間があいての回答になってしまい。

キャッシュではなく、Named Query にすると、アプリケーションの起動時もしくは、初期化時に、事前にプリコンパイルされて実行されます。一方、Native Query にすると構文が毎回評価、コンパイルされるので、パフォーマンスが変わって来ます。

"select * from product where id = "
の 100 の部分をそれぞれ変数に変えてテストして頂くと如何でしょう?

ちなみに、JPA の書籍である、
「Pro JPA 2: Mastering the Java™ Persistence API」
にも、下記のような記載がございます。

7.7.1. Named Queries
First and foremost, we recommend named queries whenever possible. Persistence providers will often take steps to precompile JPQL named queries to SQL as part of the deployment or initialization phase of an application. This avoids the overhead of continuously parsing JPQL and generating SQL. Even with a cache for converted queries, dynamic query definition will always be less efficient than using named queries.

寺田 佳央寺田 佳央 2012/12/28 01:04 今、JPQL の部分のコードをちゃんと拝見したのですが、JPQL の部分なのですが、NamedQuery にして試して頂けないでしょうか。

@NamedQuery(name="findProducts", query="select p from product p where p.id = :id")

Query query = em.createNamedQuery("findProducts");
query.setParameter("id", id);
List list = (List<Customer>)query.getResultList();

どうぞ宜しくお願いします。

megascusmegascus 2012/12/28 07:01 NamedQueryでキャッシュさせるとCriteriaよりもちょっと早い程度まではなりそうですけど、どこまで早くなるのか・・・・
ワクワクキタイアゲ。

hiranasuhiranasu 2012/12/28 09:15 nowokayさん 寺田さん megascusさん

皆様、貴重なご意見ありがとうございます!NamedQueryにしておくと、プリコンパイルされるのは知りませんでした。それであればJPQLはNativeQueryと同等までは行けそうですね。今日は時間が取れないので、明日実験し、ご報告させて頂きます!

hiranasuhiranasu 2013/01/03 16:09 次エントリで、NamedQueryにして追実験してみました!遅くなり申し訳ありません。

http://d.hatena.ne.jp/hiranasu/20130103/1357196494