Mathematics possesses not only truth, but a beauty - a beauty cold and austere, like that of a scupltureBertrand Russell
Also relates to PHP
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 Jan 30, 2004 at 00:45:22. [Comments for Selecting Next And Previous- 4]
Also relates to Databases
Another little SQL snippet here for MySQL.
This one is useful when summarizing <select> data from an HTML
form (perhaps stored as an ENUM datatype).
SELECT @total := COUNT(*),
@offices := COUNT(IF(type='offices',1,NULL)),
@units := COUNT(IF(type='units',1,NULL))
FROM `tblunits`;
SELECT CONCAT(@total, ' [',
@offices, ' office',
IF(@offices != 1,'s',''), ', ',
@units, ' unit',
IF(@units != 1, 's',''),
']');
Once again these two queries utilise variables, and make use of conditional testing
within a COUNT function to calculate the summaries. The CONCAT
could just as easily have been done in PHP, but I chose to use
the PEAR::Package::DB getOne() call. Short and simple!
Posted on Nov 14, 2003 at 03:42:03. [Comments for MySQL Summarizing- 0]
Also relates to Databases
Here is a code snippet demonstrating the use of MySQL variables in a set of queries. The problem was to find the longest standing entry in a table for a custom CMS I am building. The date added is a timestamp datatype.
SELECT @min := MIN(added) FROM tbldirectory;
SELECT @interval := UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(added),
@name := name
FROM tbldirectory WHERE added = @min;
SELECT @weeks := MOD(FLOOR(@interval / (7 * 24 * 60 * 60)),52) as weeks,
@years:= FLOOR(@interval / (7 * 24 * 60 * 60 * 52)) as years,
CONCAT(@years, ' year', IF (@years!=1,'s',''), ' and ',
@weeks, ' week', IF (@weeks!=1,'s','')) as duration,
@name as name;
I wanted to display the length of time in years and weeks, so once a variable
(@interval) was set to the duration in seconds, I used some
basic arithmetic with the FLOOR and MOD functions to get
the appropriate values. To keep the final select query readable I stored the week and year calculations in
variables and then used the CONCAT function to build the required string.
Using the variables and functions in MySQL queries is a great way to reduce unnecessary formatting of the data within the API, and now the query result is ready to go straight into the template. The MySQL Control Centre has a useful SQL editor for building complex query strings step by step.
Posted on Nov 10, 2003 at 02:22:21. [Comments for Using MySQL Variables- 0]
Also relates to Databases
Here is a good tutorial on Date Arithmetic with MySQL. It demonstrates a small part of the depth of the Date API in MySQL which can dramtically cut down on uneccessary user defined date functions in PHP and the like. Simon Willison has also written a brief follow up to this on Storing Dates in MySQL and the MySQL Unix conversion functions, which allow a DateTime datatype to be readily converted to the native Unix timestamp format for PHP processing.
This has got me thinking briefly about the best choice for MySQL storage between Timestamp and DateTime. For this blog, I have used the Timestamp datatype in my schemas for all date fields. Both datatypes have access to the full set of date manipulation functions in the MySQL API, and there is no difference in memory use. So, example, for the dates on this blog I have the following call as part of a MySQL SELECT statements:
SELECT […] DATE_FORMAT(added, '%e %M %Y - %T') as create_time FROM […].
Posted on Jul 13, 2003 at 23:54:59. [Comments for MySQL Date Datatypes- 2]