[Back-end]N+1題


N+1


N+1は、Java Persistence APIを使用して関連付けられたエンティティを作成する際に発生する可能性のある問題です.
N+1問題は性能に大きな影響を及ぼす.

N+1問題


クエリーが関連関係で発生したイベントで関連付けられたエンティティを作成すると、クエリーされたデータの数に応じて関連付けられたクエリーが追加され、データが取得されます.
これをN+1問題と呼ぶ.

ディレイロード(LAZY)、インスタントロード(EAGER)



遅延ロードの場合、メンバーエンティティがクエリーされると、Teamエンティティがプロキシオブジェクトとしてインポートされます.
次に、実際のTeamオブジェクトを使用するときに初期化します.データベースにクエリーがあります.
たとえば、getTeam()を使用してTeamをクエリーすると、エージェントオブジェクトがクエリーされます.
getTeam.getXXX()を使用してチームフィールドにアクセスすると、クエリーが行われます.
このように遅延ロードを使用すると、SELECTクエリはそれぞれ2回行われます.
これは,2回のネットワークを介してクエリを行うことを意味する.
すぐにロードすると、プロキシオブジェクトではなくクエリーを使用して実際のオブジェクトをTeamオブジェクトにインポートします.

注意事項


実務ではなるべく遅延ロードのみを使うようにしているそうです.
すぐにロードすると、予期せぬSQLが発生し、N+1の問題が発生します.
@ManyToOneと@OneToOneのように、@XXXToOneの基本的な機能はすぐにロードすることです.
@OneToManyと@ManyToManyのデフォルトは遅延ロードです.

の原因となる


  • データをすぐにロードします.

  • データインポート後のデータからサブエンティティを再問合せするには、遅延ロードを使用します.
  • アルバムエンティティ

    @Entity
    public class Album {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @Column(nullable = false)
        private String albumTitle;
    
        @Column(nullable = false)
        private String locales;
    
        // @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, fetch = FetchType.EAGER) // 2번 상황
        @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, fetch = FetchType.LAZY) // 1번 상황
        private List<Song> songs = new ArrayList<>();
    }

    楽曲本体

    @Entity
    public class Song {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @Column(nullable = false)
        private String title;
    
        @Column(nullable = false)
        private int track;
    
        @Column(nullable = false)
        private int length;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "album_id")
        private Album album;
    }

    サブエンティティはクエリーされていません。

    @Test
    public void N1_쿼리테스트_1() throws Exception{
        List<Album> albums = albumRepository.findAll();
    }
    直ちにロードするとN+1の問題が発生します.

    サブエンティティの問合せ:

    @Test
    @Transactional // 테스팅에서 LAZY 전략시 필수
    public void N1_쿼리테스트_2() throws Exception{
        List<Album> albums = albumRepository.findAll(); // (1) N+1 발생하지 않음
        for (Album album : albums) {
            System.out.println(album.getSongs().size()); // (2) Song에 접근 N+1 발생.
        }
    }
    遅延ロード、即時ロード時にN+1の問題が発生.

    N+1トラブルシューティング方法


    パッチ結合


    照会でテーブルにサインして持ってきてください.LAZYとEAGERの2つのソリューション
    @Query("select DISTINCT a from Album a join fetch a.songs")
    List<Album> findAllJoinFetch();
    
    @Test
    @Transactional // 테스팅에서 LAZY 전략시 사용해야 동작
    public void FetchJoin_테스트() throws Exception{
        List<Album> albums = albumRepository.findAllJoinFetch();
        for (Album album : albums) {
            System.out.println(album.getSongs().size()); // Song에 접근 !
        }
    }

    パッチ結合の欠点

  • JPAが提供するPageable機能は使用できません.
  • 1:N関係を持つ2つのエンティティにパッチグループを使用することはできません.
  • バックアップ・サイズの変更


    設定したSizeに従ってデータをプリロードします.
    JPAのページングAPI機能のように、一定数のデータをインポートするときに一緒に使用すると便利です.
    欠点は、グローバルパッチ戦略をEAGERに変更する必要があることです.
    Java
    @BatchSize(size = 5)
    @OneToMany(mappedBy = "album", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private List<Song> songs = new ArrayList<>();

    参考資料


    https://velog.io/@woo00oo/N-1-%EB%AC%B8%EC%A0%9C
    https://wwlee94.github.io/category/blog/spring-jpa-n+1-query/
    https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1