I don't understand the GraphQL N+1 problem

You are correct -- using a join would let you make a single database query instead of 101.

The problem is that in practice, you wouldn't just have one join -- your review data model might include associations with any number of other models, each one requiring its own join clause. Not only that, but those models might have relationships to other models themselves. Trying to craft a single SQL query that will account for all possible GraphQL queries becomes not only difficult, but also prohibitively expensive. A client might request only the reviews with none of their associated models, but the query to fetch those reviews now include 30 additional, unnecessary views. That query might have taken less than a second but now takes 10.

Consider also that relationships between types can be circular:

{
  reviews {
    author {
      reviews {
        author
      }
    }
  }
}

In this case, the depth of a query is indeterminate and it is impossible to create a single SQL query that would accommodate any possible GraphQL query.

Using a library like dataloader allows us to alleviate the N+1 problem through batching while keeping any individual SQL query as lean as possible. That said, you'll still end up with multiple queries. An alternative approach is to utilize the GraphQLResolveInfo object passed to the resolver to determine which fields were requested in the first place. Then if you like, you can make only the necessary joins in your query. However, parsing the info object and constructing that sort of query can be a daunting task, especially once you start dealing with deeply nested associations. On the other hand, dataloader is a more simple and intuitive solution.