1

Help, please, with an efficient way to enforce a limit on the number of books a user marks as favorites.

user_book table

bookID | userID | Attr | addedTime  <- with index UNIQUE(bookID,userID)

The Attr column holds bitflags of attributes the user sets on a book. For eg:

  • 1 (binary 001) if user owns the book,
  • 2 (binary 010) if user read it,
  • 4 (binary 100) if the user marked it as favorite

So Attr=6 means the user does not own the book, but she read it and marked it as favorite

When a user tries to set the favorite flag on an existing record or add a new record with that flag set, I'd like to enforce a 25 favorites limit. If the limit has already been reached, I'd like to remove the flag from the oldest favorited record. My current implementation seems overly complex:

Suppose user 25 wants to set book 100 as favorite, I would do (pseudocode)

//sql to get all books the user has marked as favorite; oldest book first
SELECT bookID from user_book WHERE userID=25 AND (Attr & 4)>0 
    ORDER BY addedTime ASC 

if(($count = count($results)) > 24){ //if more than 24 books are favorited
    $bookIDsToUnset = array_slice($results,0,$count-24); //get books over limit
    $bookIDsToUnset = implode(',' , $bookIDsToUnset);

    //unset the favorite attribute from the books over the limit
    UPDATE user_book SET Attr = Attr^4 WHERE userID=25 
        AND bookID IN ($bookIDsToUnset)
}

//make bookID 100 a favorite
UPDATE user_book SET Attr = Attr | 4 WHERE userID=25 AND bookID=100

This requires up to 3 DB queries, so it seems inefficient. Can anyone suggest a better alternative?

BeetleJuice
  • 39,516
  • 19
  • 105
  • 165
  • 1
    It seems like an interesting UX detail to have the system automatically un-favorite a book, rather than refusing to favorite another book. I assume you have something in place to notify the user that this is going to happen? – random_user_name Aug 07 '16 at 03:33
  • @cale_b At the front-end I'll notify the user with a warning every time she favorites a book if there are more than 19 favorites, and with an encouragement to unset the favorite flag from books she no longer needs favorited. – BeetleJuice Aug 07 '16 at 03:38
  • One question: when the size of favorite list is greater than 24 why are you resetting all the favorites? Instead isn't it supposed to reset only one favorite in order to give room to the new favorite one? – 1000111 Aug 07 '16 at 04:51
  • @1000111 I would be resetting just favorites #25+. So if there are 25, it would be just one favorite: the result of `array_slice()` just after the `if` line – BeetleJuice Aug 07 '16 at 04:52
  • Okay then Gordon's approach is correct. Here I've modified Gordon's answer to meet your requirement `update user_book ub join ( select ub2.* from user_book ub2 where ub2.userId = 25 AND (Attr & 4)>0 order by addedTime desc offset 24 ) keep24 on ub.bookid = keep24.bookid and ub.userid = keep24.bookid set attr = attr ^ 4;` I found a typo and a missing condition in the query. – 1000111 Aug 07 '16 at 05:01
  • @1000111 Like Gordon, you set `ub.userid=keep24.bookid` (last line) and I don't understand why. Also, using `OFFSET` without `LIMIT` throws errors. See http://stackoverflow.com/questions/3173635/offset-mysql-without-limit – BeetleJuice Aug 07 '16 at 05:17
  • 1
    Ohh I see. `UPDATE user_book ub INNER JOIN ( SELECT ub2.* FROM user_book ub2 WHERE ub2.userId = 25 ORDER BYaddedTime DESC LIMIT 1 OFFSET 24 ) AS keep24 ON ub.bookid = keep24.bookid AND ub.userid = keep24.userid SET ub.attr = ub.attr ^ 4;` what about now? – 1000111 Aug 07 '16 at 05:22
  • Looks like it should work with the limitation that if a user already has 26+ favorites, it will only unset the 25th. Shouldn't be a big problem though; I could set the limit to a high number to catch everything after the offset. I'll give it a try – BeetleJuice Aug 07 '16 at 05:30
  • It did work for me. Thanks @1000111 – BeetleJuice Aug 15 '16 at 21:28

1 Answers1

2

You can unset the favorites in a single query:

update user_book ub join
       (select ub2.*
        from user_book ub2
        where ub2.userId = 25 and (attr & 4) > 0
        order by ub2.addedTime desc
        offset 24 limit 999
       ) keep24
       on ub.bookid = keep24.bookid and ub.userid = keep24.userid
    set attr = attr ^ 4;

With an index on user_book(userId, addTime) and user_book(userId, bookId), this should be pretty fast.

You can then insert the new favorite using your insert statement.

Note: I don't think it is a good idea to automatically remove favorites, but that seems to be your application design.

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
  • Hi Gordon. Thank you. I'm not strong enough at SQL yet to understand your answer fully, so I have a couple of Qs. On line 4, which table does `b2.userId` refer to? On the next to last line, why do you set `ub.userid=keep24.bookid`? And about the inner query: why are you selecting all books with `userId=25` instead of limiting selection to books that have been favorited? – BeetleJuice Aug 07 '16 at 04:41
  • Alos, using `offset` without `limit` is throwing errors for me. See http://stackoverflow.com/questions/3173635/offset-mysql-without-limit – BeetleJuice Aug 07 '16 at 05:18
  • Wow. Outstanding - had no idea you could do some of these things with mysql. – random_user_name Aug 07 '16 at 20:42
  • @BeetleJuice . . . Oops. The intention was the `offset`, but of course, `limit` is also needed. I made it 999. According to your rules, "1" should be sufficient. – Gordon Linoff Aug 07 '16 at 22:49