The first step towards philosophy is incredulityDenis Diderot
Starting to get things sorted with the new look business site now, so time to catch up on some blogging. I am going to start off by looking at the common scenario of creating next and previous links for a data result set.
A typical application of the next and previous links is in a News Archive. These links offer intrinsic usability benefits presenting the user with less steps to view articles (eg they do not have to jump back to an index to find the link for the next article). However, selecting neighbouring items from a MySQL database is not that simple.
The obvious solution is to select the item with a unique key one less/more than the current record. But this has two drawbacks. Firstly it requires integers as unique keys, which is not always the case. It also assumes that there is a continuous sequence of unique keys with no breaks. This is rarely the case in a content management system, where some items might be deleted.
For this example I am going to assume that the unique key is not integer based, and instead focus on the timestamp at which each entry was added. So rather than adding or subtracting directly from a unique key, all that is required are a couple of comparative selection queries, having firstly established the timestamp for the current record.
SELECT @now := added
FROM table
WHERE uid = {x};
SELECT @prev := uid
FROM table
WHERE added < @now
ORDER by added DESC
LIMIT 0, 1;
SELECT @next := uid
FROM table
WHERE added > @now
ORDER by added ASC
LIMIT 0, 1;
This in itself is one solution, if, rather than creating SQL variables, the required data was just extracted directly in the second and third statements. But this will require two separate calls to the database abstraction layer from the API, with two sets of result data to manipulate. The code can be made more manageable at each layer if this is reduced to a single call. Having acquired the unique ID of the next and previous item, it is merely a case of collecting the data.
SELECT IF(uid = @prev,'p','n') as position, uid, title
FROM table
WHERE (uid = @prev) OR (uid = @next);
In this statement I have created an additional result field position which will be set to p or n
for the previous and next records respectively. This is because I generally use PEAR::DB as my database abstraction with a set
of custom functions. In this scenario, the first three statements can be executed in the standard way, $dbh->query($query),
and the final statement called via $dbh->getAssoc($query). This will return an associative array of a maximum two items
with keys of either n or p and the corresponding data.
Of course there is always more than one way of doing things, and I feel the logic of this alternative approach quite elegant, however, with limited permissions on my current servers, I can only test this on my local machine.
SELECT @row := 0;
DROP TABLE IF EXISTS temptable;
CREATE TEMPORARY TABLE temptable
SELECT @row := @row + 1 as rownum, table.* FROM table
ORDER BY added DESC;
SELECT @current := rownum
FROM temptable
WHERE uid = {x};
SELECT IF(rownum < @current, 'n', 'p') as position, uid, title
FROM temptable
WHERE (rownum = @current - 1) OR (rownum = @current + 1)
The code utilises the CREATE TEMPORARY TABLE statement to create a replica of the data that is being
examined, and adding an additional field to the temporary table to act as a unique integer identifier (rownum).
This resolves the problem outlined initially regarding the datatype of unique indexes, and sequence gaps.
There are no doubt copious other solutions to this problem. Of course an obvious choice is to do the work on the API layer. For example, the entire dataset could be collected and then PHP's array functions used to manipulate the dataset and extract the required information. However, I have found in the past this can get quite kludgey! Doing the work at the Database layer itself does not (appear to) harm the performance of the database, and helps keep all layers in the application that bit purer.
Posted on Friday, Jan 30, 2004 at 00:45:22.
Comments on Selecting Next And Previous (4)
α comment
This is a nice engineering solution to the problem :) I would guess it is fast also, here is another solution, with does the same job, but not as nice maby :) (presumtion: you already have the publish date from the post you want to fetch previous and next link for stored in $published. iow: already fetch the article)
SELECT published AS prev_published, title AS prev_title FROM post WHERE published < $published ORDER by published DESC LIMIT 0,1;
SELECT published AS next_published, title AS next_title FROM post WHERE published > $published ORDER BY published LIMIT 0,1;
Simple? Requires 2 SELECTs though… :( BTW, strict on comments: > maps to greater-than sign < maps to less-than sign ; maps to semicolon hehe
Posted by Magne
Tuesday, Aug 03, 2004 at 23:01:37
β comment
Thankyou Magne! And what you suggest is a simple method (once the publish date of current article is known).
Generally I like to keep the PHP layer to a necessary minimum, hence the incentive to utilise the MySQL API as outlined above to return a single associative array that can be passed straight to a template.
And, YES, my comments system is *poor*. Will improve it when time allows. Congrats on the ingenuity :)
Posted by Tom
Wednesday, Aug 04, 2004 at 00:57:45
γ comment
Magne - thank you for the tip on the SELECT statements. It's exactly what I've been looking for.
Tom - great stuff on your blog!
Posted by nprajan
Wednesday, Sep 07, 2005 at 21:20:23
δ comment
As of MySQL 4.1, you can now accomplish this through the use of subqueries:
Posted by Kevin
Tuesday, Jan 17, 2006 at 14:32:51