NoahOrblog

某・福島にある大学のコンピュータ理工学部の大学生のお話。

N+1問題

Noahです。

ペアプロ等で指摘されたことで、重要だと思ったら1つずつメモを書こうと思い、書く。

続…かないかもしれない

今日はN+1問題について(ここのコードは深夜に書いたメモ程度でなんと言うか、補助的役割である👀)

N+1の問題

まず、1つのクエリで複数のidを取得し、その後繰り返しのクエリで、idのスライスのそれぞれをDBから抜いてくること。 idの数をNとすると、合計N+1のクエリが発生する。

// func Query() (Data, error){
//    内部では、1つの構造体Dataのものを返すようになっている
// }
// func Query2ByID(id string) (User, error){
//    内部では、idから紐付けられる1つの構造体Userを返すようになっている(Userモデル内でid はUNIQUEとする)
// }

func f() {
    // あるデータをDBから抜いてくる
    //  中に複数のidがあるような構造体
    //  (e.g.)
    //  type Data struct {
    //      Subs []Sub `db:"-"`
    //  }
    //  type Sub struct {
    //      ID string `db:"user_id"`
    //  }
    //  type User struct {
    //      ID string `db:"id"`
    //      Name string `db:"name"`
    //  }

    //構造体Dataを一件DBから取得するクエリ ......(1)
    data, err := Query()
    if err != nil {
        log.Fatal(err)
    }

    // idsをもとにDBからユーザ情報を抜く
    for i, sub := range data.Subs {
        //構造体Userを、Sub.IDから紐付けてDBから抜いてくるクエリ ......(2)
        user, err := Query2ByID(sub.ID)
        if err != nil {
            log.Fatal(err)
        }
        // なんか処理する
    }
}

愚直にこんな感じで書いてしまうと、(1), (2) で、合計N+1のクエリを実行してしまうため、パフォーマンスが非常に悪い。

良い方法として、mapを利用した解決策がある

解決策

// func Query() (Data, error){
//    内部では、1つの構造体Dataのものを返すようになっている
// }
// func Query2ByIDs(id []string) ([]User, error){
//    内部では、idから紐付けられる構造体Userを返すようになっている(Userモデル内でid はUNIQUEとする)
//    先程との変更点は、複数のidを受け取り、複数の結果を返すようになっている
// }

func g() {
    // あるデータをDBから抜いてくる
    //  中に複数のidがあるような構造体
    //  (e.g.)
    //  type Data struct {
    //      Subs []Sub `db:"-"`
    //  }
    //  type Sub struct {
    //      ID string `db:"user_id"`
    //  }
    //  type User struct {
    //      ID string `db:"id"`
    //      Name string `db:"name"`
    //  }

    //構造体Dataを一件DBから取得するクエリ ......(1)
    data, err := Query()
    if err != nil {
        log.Fatal(err)
    }

    // idをKeyとして、Userモデルを引っ張れるmapを作成
    userMap := map[string]User{}
    for i, sub := range data.Subs {
        userMap[sub.ID] = User{} // keyとvalueを初期化しておく
    }

    // idのスライスを作る
    ids := make([]string, len(data.Subs))
    for i, sub := range data.Subs {
        ids[i] = sub.ID
    }
    // idのスライスからそれらに紐付けられている複数のUserを引っ張ってくる ......(2)
    users, err := Query2ByIDs(ids)
    if err != nil {
        log.Fatal(err)
    }

    // usersをidをkeyにmapに入れていく
    for _, u := range users {
        userMap[u.ID] = u
    }

    // 以降、userMapからkeyをIDにするだけでモデルを取得できる
}

このようにすると、(1), (2) のクエリが2個で済むようになり、パフォーマンスの向上が図れる。

書いてるときは、パフォーマンス等を全く考慮しないで書いていたので、ちゃんと考慮したいと思った次第…