views:

103

answers:

4

Hello,

I'm writing an application on top of CodeIgniter to better organize my ebook collection. I'm nearly done, but I realize my 'browse' page is running far too many queries - two per book - to get their information. Obviously not at all ideal, especially since I have about 1000 books to put into this system.

I currently have one model function which gets all of the books (will eventually be modified to take parameters - that's the next step) and another that gets the meta information for each returned book. The second function is the one which makes two queries for each book - one to get the information in the book table and another to get the tags associated with the book. Here are the two model functions:

Get the list of books:

function get_books() {
    $this->db->select('isbn')->order_by('title');
    $query = $this->db->get('books');
    $result = $query->result();
    return $result;
}

Get the book meta information:

function get_book_info($isbn) {
    // Grab the book from Amazon
    $amazon = $this->amazon->get_amazon_item($isbn);

    // Get the book info
    $this->db->select('title, publisher, date, thumb, filename, pages');
    $query = $this->db->get_where('books', array('isbn' => $isbn));
    $bookResult = $query->row();

    // Get the book's tags
    $this->db->select('tag');
    $this->db->from('tags AS t');
    $this->db->join('books_tags AS bt', 'bt.tag_id = t.id', 'left');
    $this->db->where('bt.book_id', $isbn);
    $this->db->order_by('t.tag');
    $tagQuery = $this->db->get();
    foreach ($tagQuery->result() as $row) {
        $tagResult[] = $row->tag;
    }
    $tagResult = implode(', ', $tagResult);

    // Send data
    $data = array(
        'isbn' => $isbn,
        'thumb' => $bookResult->thumb,
        'title' => strip_slashes($bookResult->title),
        'file' => $bookResult->filename,
        'publisher' => strip_slashes($bookResult->publisher),
        'date' => date('F j, Y', strtotime($bookResult->date)),
        'pages' => $bookResult->pages,
        'tags' => $tagResult,
        'rating' => $amazon->Items->Item->CustomerReviews->AverageRating,
        'raters' => $amazon->Items->Item->CustomerReviews->TotalReviews
    );
    return $data;
}

I'm certain there's a way to write one or two queries that will gather all the records into objects I can then filter through, rather than having to write two queries for each one, but I have no idea where to even start trying to write that. Any suggestions are welcome.

Thanks much, Marcus

A: 

If I got you right: in the table books there is all the data about the books: so doing this:

$this->db->select('*')->order_by('title');
$query = $this->db->get('books');
$result = $query->result();
return $result;

should return you all the data about your books and you shouldn't need to cycle again to get data.

Lex
I'm okay with the first query. I would rather have one extra query than have to hold the entire contents of my books table in memory every time someone wants to view a specific set. My question is more about the second model function. The first was supplied for reference.
Marcus
A: 

I'm not familiar with CodeIgniter at all, but I think there are some general practices you can incorporate.

  • If this is a browse page - isn't there pagination? Paginating the results should drastically cut down the number of queries you have to run per page load.
  • Have one function (say, get_books_info()) you call that retrieves all the tags & meta info for all the books returned by your get_books() function. Then reference that array from your get_book_info(). You can even trigger get_books_info() from get_book_info() - so you only need to do the work if you need the data. Kind of lazy loading I think.
Pickle
There will be pagination eventually, but I need to get the optimized queries first and then just apply the limits and offsets to them.
Marcus
+2  A: 

What you want to do is:

  1. Grab a sorted list of all of your books and their tags,
  2. Style them out as HTML, with tags and ratings.

Grab your books and tags together, have a variable to keep track of the last ISBN you wrote out, and only build up your entry when the ISBN changes. So, pull a set like this:

Book   | Tag
------ | ----------------
Book A | Fiction
Book A | Fantasy
Book B | Mystery
Book C | Science Fiction

Then, write out the "basic book info" for every time the book changes within your loop. Obviously, you'll want more fields than just Book and Tag (e.g., ISBN).

If your Amazon information comes from Amazon, you're probably going to have no choice about making repetitive calls to their API (unless they have a "batch" mode or something, wherein you could submit an array of ISBN's?).

David T. Macknet
Agreed. I'm okay with the multiple calls to Amazon - that's all "nice to have" data. I've saved the core book metadata to the db. I like the sample resultset you show, but I don't have a clue how to write that sort of query. Any tips to get me started?
Marcus
Inspired by your approach, I asked another question on how to generate the result above and implemented my solution based on it. Code is below.
Marcus
Sorry to have been asleep while you solved it on your own ... but glad you got it!
David T. Macknet
+1  A: 

With some help from this topic and in others in creating a better query, I was able to resolve this with the following code:

function get_book_info() {

    /*
     * SELECT b.isbn, b.title, b.publisher, b.date, b.thumb, b.filename, b.pages, t.tag
     * FROM books AS b
     * INNER JOIN books_tags AS bt ON b.isbn = bt.book_id
     * INNER JOIN tags AS t ON bt.tag_id = t.id
     * ORDER BY b.title, t.tag
     */

    $this->db->select('b.isbn, b.title, b.publisher, b.date, b.thumb, b.filename, b.pages, t.tag');
    $this->db->from('books AS b');
    $this->db->join('books_tags AS bt', 'b.isbn = bt.book_id', 'inner');
    $this->db->join('tags AS t', 'bt.tag_id = t.id', 'inner');
    $this->db->order_by('b.title, t.tag');
    $query = $this->db->get();
    $result = $query->result();

    $counter = '';
    $record = $meta = $tags = array();
    $count = count($result);
    $i = 1;

    foreach ($result as $book) {
        // If this is not the last row
        if ($i < $count) {
            // If this is the first appearance of this book
            if ($counter != $book->isbn) {
                // If the meta array already exists
                if ($meta) {
                    // Add the combined tag string to the meta array
                    $meta['tags'] = implode(', ', $tags);
                    // Add the meta array
                    $record[] = $meta;
                    // Empty the tags array
                    $tags = array();
                }
                // Reset the counter
                $counter = $book->isbn;
                // Grab the book from Amazon
                $amazon = $this->amazon->get_amazon_item($book->isbn);
                // Collect the book information
                $meta = array(
                    'isbn' => $book->isbn,
                    'title' => strip_slashes($book->title),
                    'publisher' => strip_slashes($book->publisher),
                    'date' => date('F j, Y', strtotime($book->date)),
                    'thumb' => $book->thumb,
                    'file' => $book->filename,
                    'pages' => $book->pages,
                    'rating' => $amazon->Items->Item->CustomerReviews->AverageRating,
                    'raters' => $amazon->Items->Item->CustomerReviews->TotalReviews
                );
                // Add the tag to the tags array
                $tags[] = $book->tag;
            } else {
                // All we need is the tag
                $tags[] = $book->tag;
            }
        // If this is the last row
        } else {
            // If this is the first appearance of this book
            if ($counter != $book->isbn) {
                // Grab the book from Amazon
                $amazon = $this->amazon->get_amazon_item($book->isbn);
                // Collect the book information
                $meta = array(
                    'isbn' => $book->isbn,
                    'title' => strip_slashes($book->title),
                    'publisher' => strip_slashes($book->publisher),
                    'date' => date('F j, Y', strtotime($book->date)),
                    'thumb' => $book->thumb,
                    'file' => $book->filename,
                    'pages' => $book->pages,
                    'rating' => $amazon->Items->Item->CustomerReviews->AverageRating,
                    'raters' => $amazon->Items->Item->CustomerReviews->TotalReviews
                );
                // Add the tag to the tags array
                $tags[] = $book->tag;
                // Add the combined tag string to the meta array
                $meta['tags'] = implode(', ', $tags);
                // Add the meta array
                $record[] = $meta;
            } else {
                // All we need is the tag
                $tags[] = $book->tag;
                // Add the combined tag string to the meta array
                $meta['tags'] = implode(', ', $tags);
                // Add the meta array
                $record[] = $meta;
            }
        }
        $i++;
    }

    return $record;
}

There may very well be a better way to handle this, but this was how my logic saw it. And only one query, total.

Marcus
Looking good, from what I can tell. Well done!
David T. Macknet