Write a query that will display a list of all books, and for those that have a review, list the review.
Your quoted problem statement is not clear. First, it intends but does not express something like, "display the list of (bookID, reviewID) rows where ...". Second, it intends but does not express something like "... where the book has that review or the book doesn't have a review and the reviewID is ..." what? You are expected to know/guess/decide to finish this with ... "NULL"? The empty string? Who knows, it's not clear.
Or maybe they want "(bookName, review) rows where some book with that name either has some review where the text is (column) review or doesn't have a review and (column) review is ...". (Note that's "some", not "the".) Who knows, it's not clear.
(This sort of sloppiness permeates database pedagogy & culture.)
What trips me up is that I would have to access both the the tables to retrieve both the book name using the book id, and then create an association with its corresponding review.
That is what JOINs do.
A table holds rows whose values are related/associated in some way. We can describe a relation(ship)/association in terms of a predicate--a column-parameterized sentence template:
/* the rows where
book bookID has title bookName and costs $price
*/
select * from Book
Inside a FROM, each argument table column is renamed to be prefixed by its table name/alias and a dot. Then INNER JOIN generates every combination of a row from each. When there are no duplicates, INNER JOIN holds the rows that are associated per the conjunction/AND of argument associations. ON and WHERE both modify the FROM result to return rows associated by conjunction/AND of their condition.
...
FROM Book b JOIN Reviews r ON b.bookID = r.bookID
/* the rows where
book b.bookID has title b.bookName and costs $b.price
AND review r.reviewID is by customer r.custID of book r.bookID with text r.review
AND b.bookID = r.bookID
*/
SELECT adds & drops columns while dropping dot prefixes. When we don't want duplicates but might get some, we use DISTINCT.
/* the rows where
FOR SOME values for b.*, r.*,
bookID = b.bookID AND reviewID = r.reviewID
AND book b.bookID has title b.bookName and costs $b.price
AND review r.reviewID is by customer r.custID of book r.bookID with text r.review
AND b.bookID = r.bookID
*/
SELECT b.bookID, r.reviewID
FROM Book b JOIN Reviews r ON b.bookID = r.bookID
It happens that we frequently want the rows from INNER JOIN ON plus unmatched left table rows extended by NULL for right table columns. That's LEFT JOIN ON.
...
FROM Book b LEFT JOIN Reviews r ON b.bookID = r.bookID
/* the rows where
book b.bookID has title b.bookName and costs $b.price
AND review r.reviewID is by customer r.custID of book r.bookID with text r.review
AND b.bookID = r.bookID
OR book b.bookID has title b.bookName and costs $b.price
AND NOT EXISTS r.reviewID, r.custID, r.book, r.review [
review r.reviewID is by customer r.custID of book r.bookID with text r.review
AND b.bookID = r.bookID
]
AND r.reviewID IS NULL AND r.custID IS NULL AND r.book IS NULL AND r.review IS NULL
/*
So you want:
SELECT b.bookID, r.reviewID
FROM Book b LEFT JOIN Reviews r ON b.bookID = r.reviewID
or maybe:
SELECT DISTINCT b.bookName, r.review
FROM Book b LEFT JOIN Reviews r ON b.bookID = r.reviewID
CROSS JOIN vs INNER JOIN in SQL Server 2008
Is there any rule of thumb to construct SQL query from a human-readable description?
When there are duplicates or NULLs, querying is complicated, because their presence becomes more and more obscurely related to a given situation in terms of AND, OR, NOT, FOR SOME, etc. (It's straightforward to generate predicates, but what they say is complicated.)