Spring JPAを使用したアプリケーション開発において、パフォーマンス診断と実行SQLの可視化は、本番環境で発生するボトルネック特定に不可欠なスキルです。本記事では、spring.jpa.show-sqlformat_sqlによる基本的なSQLログ出力から、Hibernate統計情報、DataSource-Proxy、P6Spyを活用した高度なクエリ分析、N+1問題の検出方法、スロークエリの特定とEXPLAIN ANALYZEの使い方、そしてインデックス設計の基本まで、Spring JPAパフォーマンス診断の実践的なテクニックを体系的に解説します。

実行環境と前提条件

本記事の内容を実践するにあたり、以下の環境を前提としています。

項目 バージョン・要件
Java 17以上
Spring Boot 3.4.x
Spring Data JPA 3.4.x(Spring Boot Starterに含まれる)
Hibernate 6.6.x(Spring Data JPAに含まれる)
データベース PostgreSQL 16 / MySQL 8.x / H2 Database
ビルドツール Maven または Gradle
IDE VS Code または IntelliJ IDEA

事前に以下の準備を完了してください。

  • JDK 17以上のインストール
  • Spring Boot + Spring Data JPAプロジェクトの基本構成
  • Maven または Gradle によるプロジェクトビルド環境

spring.jpa.show-sqlとformat_sql設定によるSQLログ出力

最も基本的なSQLの可視化方法は、Spring BootとHibernateの標準設定を利用することです。

基本設定(application.yml)

1
2
3
4
5
6
spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

この設定により、実行されるSQLがコンソールに整形されて出力されます。

出力例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Hibernate: 
    select
        u1_0.id,
        u1_0.email,
        u1_0.name,
        u1_0.created_at
    from
        users u1_0
    where
        u1_0.id=?

バインドパラメータの表示

SQLの?プレースホルダに実際にバインドされる値を確認するには、追加のログ設定が必要です。

1
2
3
4
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.orm.jdbc.bind: TRACE

この設定により、以下のようにバインドパラメータが出力されます。

DEBUG org.hibernate.SQL - select u1_0.id,u1_0.email,u1_0.name from users u1_0 where u1_0.id=?
TRACE org.hibernate.orm.jdbc.bind - binding parameter (1:BIGINT) <- [1]

SQLコメントの追加

どのメソッドからSQLが発行されたかを追跡するには、SQLコメントを有効化します。

1
2
3
4
5
spring:
  jpa:
    properties:
      hibernate:
        use_sql_comments: true

これにより、生成されたSQLにJPQLやメソッド名がコメントとして追加されます。

1
2
3
4
5
6
7
8
/* select u from User u where u.id = :id */ select
    u1_0.id,
    u1_0.email,
    u1_0.name
from
    users u1_0
where
    u1_0.id=?

設定の優先順位と注意点

設定 説明 本番環境
spring.jpa.show-sql 標準出力へのSQL出力 無効推奨
hibernate.format_sql SQLの整形 無効推奨
org.hibernate.SQL=DEBUG ログフレームワーク経由の出力 条件付きで有効
hibernate.use_sql_comments SQLへのコメント追加 無効推奨

本番環境ではshow-sqlは無効にし、必要に応じてログレベルで制御することを推奨します。

Hibernate統計情報(hibernate.generate_statistics)の活用

Hibernateの統計情報機能を有効にすると、セッション単位でのクエリ実行回数、キャッシュヒット率、エンティティのロード数など、詳細なパフォーマンスメトリクスを取得できます。

統計情報の有効化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true

logging:
  level:
    org.hibernate.stat: DEBUG
    org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: DEBUG

統計情報の出力例

Session Metrics {
    1500000 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    2500000 nanoseconds spent preparing 3 JDBC statements;
    15000000 nanoseconds spent executing 3 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    5000000 nanoseconds spent executing 1 flushes (flushing a total of 10 entities and 5 collections);
    0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}

プログラムからの統計情報取得

統計情報はプログラムからも取得可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.demo.service;

import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.springframework.stereotype.Service;

import jakarta.persistence.EntityManagerFactory;

@Service
public class StatisticsService {

    private final Statistics statistics;

    public StatisticsService(EntityManagerFactory entityManagerFactory) {
        SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
        this.statistics = sessionFactory.getStatistics();
        this.statistics.setStatisticsEnabled(true);
    }

    public void printStatistics() {
        System.out.println("=== Hibernate Statistics ===");
        System.out.println("Query execution count: " + statistics.getQueryExecutionCount());
        System.out.println("Query execution max time: " + statistics.getQueryExecutionMaxTime() + "ms");
        System.out.println("Slowest query: " + statistics.getQueryExecutionMaxTimeQueryString());
        System.out.println("Entity load count: " + statistics.getEntityLoadCount());
        System.out.println("Entity fetch count: " + statistics.getEntityFetchCount());
        System.out.println("Collection load count: " + statistics.getCollectionLoadCount());
        System.out.println("Collection fetch count: " + statistics.getCollectionFetchCount());
        System.out.println("Second level cache hit count: " + statistics.getSecondLevelCacheHitCount());
        System.out.println("Second level cache miss count: " + statistics.getSecondLevelCacheMissCount());
    }

    public void clearStatistics() {
        statistics.clear();
    }
}

統計情報で確認すべき重要なメトリクス

メトリクス 説明 注意すべき値
getQueryExecutionCount() 実行されたクエリ総数 想定より多い場合はN+1の可能性
getQueryExecutionMaxTime() 最も遅いクエリの実行時間 100ms以上は要調査
getEntityFetchCount() フェッチされたエンティティ数 ロード数との乖離が大きい場合は注意
getSecondLevelCacheHitCount() 2次キャッシュヒット数 低い場合はキャッシュ設定見直し

DataSource-ProxyによるSQL/実行時間ログの取得

DataSource-Proxyは、JDBCレベルでのSQL実行を詳細にログ出力できるライブラリです。実行時間、バインドパラメータ、バッチ処理の詳細を確認できます。

依存関係の追加

1
2
3
4
5
6
<!-- Maven -->
<dependency>
    <groupId>net.ttddyy</groupId>
    <artifactId>datasource-proxy</artifactId>
    <version>1.11.0</version>
</dependency>
1
2
// Gradle
implementation 'net.ttddyy:datasource-proxy:1.11.0'

DataSourceのプロキシ設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.demo.config;

import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;

@Configuration
public class DataSourceProxyConfig {

    @Autowired
    private DataSourceProperties dataSourceProperties;

    @Bean
    @Primary
    public DataSource dataSource() {
        DataSource originalDataSource = dataSourceProperties
            .initializeDataSourceBuilder()
            .build();

        return ProxyDataSourceBuilder.create(originalDataSource)
            .name("QueryLoggingProxy")
            .logQueryBySlf4j(SLF4JLogLevel.INFO)
            .logSlowQueryBySlf4j(100, TimeUnit.MILLISECONDS, SLF4JLogLevel.WARN)
            .countQuery()
            .multiline()
            .build();
    }
}

出力例

Name:QueryLoggingProxy, Connection:1, Time:15, Success:True
Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
Query:["select u1_0.id,u1_0.email,u1_0.name from users u1_0 where u1_0.id=?"]
Params:[(1)]

スロークエリの自動検出

上記の設定では、100ミリ秒を超えるクエリが自動的にWARNレベルでログ出力されます。

WARN  - Name:QueryLoggingProxy, Connection:3, Time:250, Success:True
Type:Prepared, Batch:False, QuerySize:1, BatchSize:0
Query:["select * from large_table where status = ? order by created_at desc"]
Params:[('ACTIVE')]

クエリ実行回数のカウント

テストコードでN+1問題を検出するために、クエリ実行回数をアサートできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.example.demo.test;

import net.ttddyy.dsproxy.QueryCountHolder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class UserServiceIntegrationTest {

    @Autowired
    private UserService userService;

    @BeforeEach
    void setUp() {
        QueryCountHolder.clear();
    }

    @AfterEach
    void tearDown() {
        QueryCountHolder.clear();
    }

    @Test
    void findAllUsersWithOrders_shouldExecuteOptimalQueries() {
        // Given: 10人のユーザーと各ユーザーに5件の注文がある状態

        // When
        userService.findAllUsersWithOrders();

        // Then: N+1問題がなければ2クエリ以下で取得できるはず
        assertThat(QueryCountHolder.getGrandTotal().getSelect())
            .as("SELECT文の実行回数が最適化されていること")
            .isLessThanOrEqualTo(2);
    }
}

P6Spyによるクエリ分析と可視化

P6Spyは、JDBCドライバレベルでSQLをインターセプトし、実行時間やパラメータを詳細にログ出力するツールです。設定がシンプルで、既存のプロジェクトへの導入が容易です。

依存関係の追加

1
2
3
4
5
6
<!-- Maven -->
<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>
1
2
// Gradle
implementation 'p6spy:p6spy:3.9.1'

接続URL・ドライバの変更

1
2
3
4
5
6
spring:
  datasource:
    # 元のURLの前に「jdbc:p6spy:」を追加
    url: jdbc:p6spy:postgresql://localhost:5432/mydb
    # P6SpyDriverに変更
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver

spy.propertiesの設定

src/main/resources/spy.propertiesを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 実際のJDBCドライバを指定
driverlist=org.postgresql.Driver

# ログ出力形式をカスタマイズ
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=%(currentTime)|%(executionTime)ms|%(category)|%(connectionId)|%(sqlSingleLine)

# ログ出力先
appender=com.p6spy.engine.spy.appender.Slf4JLogger

# スロークエリの閾値(ミリ秒)
executionThreshold=100

# バインドパラメータを展開して表示
excludecategories=info,debug,result,resultset

# 日付フォーマット
dateformat=yyyy-MM-dd HH:mm:ss.SSS

出力例

2026-01-13 17:30:45.123|15ms|statement|1|select u1_0.id,u1_0.email,u1_0.name from users u1_0 where u1_0.id=1
2026-01-13 17:30:45.150|250ms|statement|1|select o1_0.id,o1_0.user_id,o1_0.total from orders o1_0 where o1_0.user_id in (1,2,3,4,5,6,7,8,9,10)

P6Spyの利点

機能 説明
実行時間の計測 クエリごとの実行時間をミリ秒単位で記録
パラメータの展開 ?プレースホルダを実際の値に展開して表示
スロークエリ検出 閾値を超えたクエリを自動的にハイライト
導入の容易さ URL変更と設定ファイル追加のみで導入可能

N+1問題の検出方法

N+1問題は、JPAアプリケーションで最も頻繁に発生するパフォーマンス問題です。効率的に検出する方法を解説します。

N+1問題とは

親エンティティN件を取得した後、各親に紐づく子エンティティを個別に取得するためにN回の追加クエリが発生する現象です。

sequenceDiagram
    participant App as アプリケーション
    participant DB as データベース
    
    App->>DB: 1回目: SELECT * FROM users
    DB-->>App: ユーザー100件を返却
    
    loop 各ユーザーに対して
        App->>DB: SELECT * FROM orders WHERE user_id = ?
    end
    
    Note over App,DB: 合計101回のクエリ(1 + 100 = N+1)

検出方法1: Hibernate統計情報

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Test
void detectN1Problem() {
    Statistics stats = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();
    stats.clear();

    // 処理を実行
    List<User> users = userRepository.findAll();
    users.forEach(user -> user.getOrders().size()); // Lazy Load発生

    // N+1問題が発生している場合、クエリ数がユーザー数+1になる
    long queryCount = stats.getQueryExecutionCount();
    System.out.println("実行されたクエリ数: " + queryCount);
    
    // 期待値は2以下(ユーザー取得 + 注文一括取得)
    assertThat(queryCount).isLessThanOrEqualTo(2);
}

検出方法2: DataSource-Proxyのカウント機能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Test
void detectN1ProblemWithDataSourceProxy() {
    QueryCountHolder.clear();

    // 処理を実行
    userService.findAllUsersWithOrderDetails();

    // SELECTクエリの実行回数を検証
    long selectCount = QueryCountHolder.getGrandTotal().getSelect();
    
    // N+1問題がない場合は2クエリ以下
    assertThat(selectCount)
        .as("N+1問題が発生していないこと")
        .isLessThanOrEqualTo(2);
}

検出方法3: ログ分析

SQLログを有効にし、同じテーブルへのSELECTが連続して発生していないか確認します。

1
2
3
4
5
6
-- N+1問題のパターン
select * from users;
select * from orders where user_id = 1;
select * from orders where user_id = 2;
select * from orders where user_id = 3;
-- ... 繰り返し

N+1問題の解決策

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 解決策1: JOIN FETCH
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();

// 解決策2: @EntityGraph
@EntityGraph(attributePaths = {"orders"})
List<User> findAll();

// 解決策3: @BatchSize(エンティティクラスに設定)
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
@BatchSize(size = 100)
private List<Order> orders;

スロークエリの特定とEXPLAIN ANALYZEの使い方

実行時間の長いクエリを特定し、実行計画を分析することでパフォーマンス改善の糸口を見つけられます。

Hibernateによるスロークエリログ

1
2
3
4
5
6
7
8
9
spring:
  jpa:
    properties:
      hibernate:
        # 100ミリ秒以上のクエリをログ出力
        session:
          events:
            log:
              LOG_QUERIES_SLOWER_THAN_MS: 100

また、Hibernate 6.6では以下の設定も利用可能です。

1
2
3
4
5
spring:
  jpa:
    properties:
      hibernate:
        log_slow_query: 100

PostgreSQLでのEXPLAIN ANALYZE

1
2
3
4
5
6
7
EXPLAIN ANALYZE 
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'ACTIVE'
ORDER BY u.created_at DESC
LIMIT 100;

実行計画の読み方

Limit  (cost=1234.56..1234.78 rows=100 width=256) (actual time=15.234..15.456 rows=100 loops=1)
  ->  Sort  (cost=1234.56..1245.67 rows=5000 width=256) (actual time=15.230..15.345 rows=100 loops=1)
        Sort Key: u.created_at DESC
        Sort Method: top-N heapsort  Memory: 50kB
        ->  Hash Left Join  (cost=100.00..1000.00 rows=5000 width=256) (actual time=5.123..12.456 rows=5000 loops=1)
              Hash Cond: (u.id = o.user_id)
              ->  Seq Scan on users u  (cost=0.00..500.00 rows=5000 width=128) (actual time=0.015..3.456 rows=5000 loops=1)
                    Filter: (status = 'ACTIVE')
              ->  Hash  (cost=80.00..80.00 rows=2000 width=128) (actual time=2.345..2.345 rows=2000 loops=1)
                    Buckets: 2048  Batches: 1  Memory Usage: 200kB
                    ->  Seq Scan on orders o  (cost=0.00..80.00 rows=2000 width=128) (actual time=0.010..1.234 rows=2000 loops=1)
Planning Time: 0.456 ms
Execution Time: 15.789 ms

実行計画で注目すべきポイント

項目 説明 注意点
Seq Scan テーブル全件スキャン 大きなテーブルでは要インデックス
actual time 実際の実行時間 costより重要
rows 処理行数 予測と実際の乖離に注意
loops 繰り返し回数 高い値はネストループの可能性

MySQLでのEXPLAIN ANALYZE

1
2
3
4
5
6
7
EXPLAIN ANALYZE 
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'ACTIVE'
ORDER BY u.created_at DESC
LIMIT 100;

JPA経由でのEXPLAINの実行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Repository
public class QueryAnalysisRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public List<Object[]> explainQuery(String sql) {
        // PostgreSQLの場合
        String explainSql = "EXPLAIN ANALYZE " + sql;
        return entityManager.createNativeQuery(explainSql).getResultList();
    }
}

インデックス設計の基本

適切なインデックス設計は、クエリパフォーマンスに決定的な影響を与えます。

JPAエンティティでのインデックス定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entity
@Table(
    name = "users",
    indexes = {
        @Index(name = "idx_users_email", columnList = "email", unique = true),
        @Index(name = "idx_users_status_created", columnList = "status, created_at"),
        @Index(name = "idx_users_name", columnList = "name")
    }
)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String status;

    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt;
}

インデックス設計の原則

原則 説明
WHERE句の条件列 検索条件に頻繁に使用される列
JOIN条件の列 外部キーや結合条件に使用される列
ORDER BY句の列 ソートに使用される列
カーディナリティ 値の種類が多い列ほど効果的
複合インデックス よく一緒に使用される列はまとめる

複合インデックスの順序

複合インデックスでは、列の順序が重要です。

1
2
3
4
5
// 良い例: statusで絞り込み → created_atでソート
@Index(name = "idx_status_created", columnList = "status, created_at")

// クエリ例
SELECT * FROM users WHERE status = 'ACTIVE' ORDER BY created_at DESC;

インデックスが効かないケース

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- 1. 関数を使用した場合
SELECT * FROM users WHERE LOWER(email) = 'test@example.com';
-- 解決策: 関数インデックスを作成するか、データを正規化

-- 2. LIKE検索で前方一致以外
SELECT * FROM users WHERE name LIKE '%田中%';
-- 解決策: 全文検索の導入を検討

-- 3. NULL比較(一部のDB)
SELECT * FROM users WHERE deleted_at IS NULL;
-- 解決策: 部分インデックスの作成

-- 4. 型の不一致
SELECT * FROM users WHERE id = '123';  -- idがBIGINTの場合
-- 解決策: 適切な型でバインド

インデックスの確認クエリ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- PostgreSQL
SELECT
    tablename,
    indexname,
    indexdef
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;

-- MySQL
SHOW INDEX FROM users;

よくある誤解やアンチパターン

アンチパターン1: 本番環境でのshow-sql有効化

1
2
3
4
# 本番環境では絶対に避けるべき設定
spring:
  jpa:
    show-sql: true  # パフォーマンス低下の原因

標準出力への書き込みはI/Oコストが高く、高負荷環境でボトルネックになります。

アンチパターン2: すべての関連をEAGERフェッチ

1
2
3
4
5
6
7
// アンチパターン
@ManyToOne(fetch = FetchType.EAGER)  // 常にJOINが発生
private Department department;

// 推奨
@ManyToOne(fetch = FetchType.LAZY)
private Department department;

アンチパターン3: インデックスの過剰作成

1
2
3
4
5
6
7
// アンチパターン: すべての列にインデックス
@Table(indexes = {
    @Index(columnList = "col1"),
    @Index(columnList = "col2"),
    @Index(columnList = "col3"),
    // ...
})

インデックスはINSERT/UPDATE/DELETE時にオーバーヘッドが発生します。実際のクエリパターンに基づいて必要なものだけを作成してください。

アンチパターン4: 統計情報を本番で常時有効

1
2
3
4
5
6
# 本番環境では負荷が高い
spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true

本番環境では必要な時だけ動的に有効化する仕組みを検討してください。

アンチパターン5: EXPLAIN ANALYZEの結果を無視

実行計画を確認しても、以下の点を見落とすケースがあります。

  • 予測行数と実際の行数の乖離
  • Seq Scanの見落とし
  • actual timeではなくcostだけを見ている

まとめと実践Tips

本記事では、Spring JPAパフォーマンス診断のための様々な手法を解説しました。

診断ツールの使い分け

用途 推奨ツール
開発時のクイック確認 show-sql + format_sql
詳細なパラメータ確認 P6Spy
テストでのN+1検出 DataSource-Proxy
統計的な分析 Hibernate Statistics
本番環境のモニタリング APMツール(Micrometer等)

実践Tips

  1. 開発初期からSQLログを確認する習慣をつける - 問題は早期発見が重要
  2. テストコードでクエリ数をアサートする - リグレッションを防止
  3. スロークエリの閾値を設定する - 問題を自動検出
  4. 定期的にEXPLAIN ANALYZEを実行する - 実行計画の変化を監視
  5. インデックスは実際のクエリパターンに基づいて設計する - 不要なインデックスは害
  6. 本番環境と開発環境でデータ量を近づける - 本番でしか発生しない問題を防止

パフォーマンス改善のチェックリスト

  • N+1問題が発生していないか確認
  • 適切なフェッチ戦略(LAZY/EAGER)を選択しているか
  • 必要なインデックスが作成されているか
  • 不要なカラムを取得していないか(Projectionの活用)
  • バッチ処理が適切に設定されているか
  • 2次キャッシュを活用できないか検討

これらの手法を組み合わせることで、Spring JPAアプリケーションのパフォーマンスを継続的に監視・改善できます。

参考リンク