Showing posts with label Mysql ORDER BY. Show all posts
Showing posts with label Mysql ORDER BY. Show all posts

Thursday, 8 November 2018

Mysql: UNION after ORDER BY and LIMIT

My goal is to execute two different queries and then combine them.
My code is:
SELECT * FROM some tables WHERE ... ORDER BY field1 LIMIT 0,1 
UNION   
SELECT * FROM some tables WHERE ...
I get the following error:
#1221 - Incorrect usage of UNION and ORDER BY
It is important that ORDER BY is only for the first query. How can I perform this task?

 Answers


You can use parenthesis to allow the use of ORDER/LIMIT on individual queries:
(SELECT * FROM some tables WHERE ... ORDER BY field1 LIMIT 0, 1)
UNION   
(SELECT * FROM some tables WHERE ...)
ORDER BY 1   /* optional -- applies to the UNIONed result */
LIMIT 0, 100 /* optional -- applies to the UNIONed result */



just put everything in round brackets:
(SELECT * FROM table1 ORDER BY datetime  )
UNION   
(SELECT * FROM table2 ORDER BY datetime DESC)

How does MySQL's ORDER BY RAND() work?

I've been doing some research and testing on how to do fast random selection in MySQL. In the process I've faced some unexpected results and now I am not fully sure I know how ORDER BY RAND() really works.
I always thought that when you do ORDER BY RAND() on the table, MySQL adds a new column to the table which is filled with random values, then it sorts data by that column and then e.g. you take the above value which got there randomly. I've done lots of googling and testing and finally found that the query in my blog is indeed the fastest solution:
SELECT * FROM Table T JOIN (SELECT CEIL(MAX(ID)*RAND()) AS ID FROM Table) AS x ON T.ID >= x.ID LIMIT 1;
While common ORDER BY RAND() takes 30-40 seconds on my test table, his query does the work in 0.1 seconds. He explains how this functions in the blog so I'll just skip this and finally move to the odd thing.
My table is a common table with a PRIMARY KEY id and other non-indexed stuff like usernameage, etc. Here's the thing I am struggling to explain
SELECT * FROM table ORDER BY RAND() LIMIT 1; /*30-40 seconds*/
SELECT id FROM table ORDER BY RAND() LIMIT 1; /*0.25 seconds*/
SELECT id, username FROM table ORDER BY RAND() LIMIT 1; /*90 seconds*/
I was sort of expecting to see approximately the same time for all three queries since I am always sorting on a single column. But for some reason this didn't happen. Please let me know if you any ideas about this. I have a project where I need to do fast ORDER BY RAND() and personally I would prefer to use
SELECT id FROM table ORDER BY RAND() LIMIT 1;
SELECT * FROM table WHERE id=ID_FROM_PREVIOUS_QUERY LIMIT 1;
which, yes, is slower than Jay's method, however it is smaller and easier to understand. My queries are rather big ones with several JOINs and with WHERE clause and while Jay's method still works, the query grows really big and complex because I need to use all the JOINs and WHERE in the JOINed (called x in his query) sub request.
Thanks for your time!

 Answers


While there's no such thing as a "fast order by rand()", there is a workaround for your specific task.
For getting any single random row, you can do like this : https://thiscode4u.blogspot.com/2018/07/mysql-order-by-rand-case-study-of.html (I couldn't see a hotlink url. If anyone sees one, feel free to edit the link.)
The text is in german, but the SQL code is a bit down the page and in big white boxes, so it's not hard to see.
Basically what he does is make a procedure that does the job of getting a valid row. That generates a random number between 0 and max_id, try fetching a row, and if it doesn't exist, keep going until you hit one that does. He allows for fetching x number of random rows by storing them in a temp table, so you can probably rewrite the procedure to be a bit faster fetching only one row.
The downside of this is that if you delete A LOT of rows, and there are huge gaps, the chances are big that it will miss tons of times, making it ineffective.
Update: Different execution times
SELECT * FROM table ORDER BY RAND() LIMIT 1; /30-40 seconds/
SELECT id FROM table ORDER BY RAND() LIMIT 1; /0.25 seconds/
SELECT id, username FROM table ORDER BY RAND() LIMIT 1; /90 seconds/
I was sort of expecting to see approximately the same time for all three queries since I am always sorting on a single column. But for some reason this didn't happen. Please let me know if you any ideas about this.
It may have to do with indexing. id is indexed and quick to access, whereas adding username to the result, means it needs to read that from each row and put it in the memory table. With the * it also has to read everything into memory, but it doesn't need to jump around the data file, meaning there's no time lost seeking.
This makes a difference only if there are variable length columns (varchar/text), which means it has to check the length, then skip that length, as opposed to just skipping a set length (or 0) between each row.



I can tell you why the SELECT id FROM ... is much slower than the other two, but I am not sure, why SELECT id, username is 2-3 times faster than SELECT *.
When you have an index (the primary key in your case) and the result includes only the columns from the index, MySQL optimizer is able to use the data from the index only, does not even look into the table itself. The more expensive is each row, the more effect you will observe, since you substitute the filesystem IO operations with pure in-memory operations. If you will have an additional index on (id, username), you will have a similar performance in the third case as well.

Mysql: ORDER BY SLOW()

One constant source of database/mySQL related frustration is the need to fetch a random row in a given table. We do randomization a lot, especially for anything recommendation-related so that users don’t see the exact same set of recommendations over and over again. This worked fine when our tables were small and everything fit into memory easily, but now that our tables have 6+ million rows in them, the old standby ORDER BY RAND() LIMIT 1 is no good.

ORDER BY RAND() in and of itself isn’t the worst thing in the universe, it does use a temp table to do all that sorting which is certainly less than ideal to begin with, but by the very nature of ORDERs and LIMITs, mySQL can’t apply the LIMIT 1 until it has ordered your result set. Good luck to you if your result set contains thousands of rows. That’s why we’ve taken to calling it ORDER BY SLOW(). You really have to think about how to apply ORDER BY RAND() so that it is functional and fast on massive data sets.

Now, a better solution would be:
SELECT count(*) FROM foo
and save the result as num_rows. Then
SELECT * FROM foo LIMIT [random number between 0 and num_rows],1
you just selected a random row in two quick queries! congrats!

There’s nothing really wrong with that way but it involves an unnecessary extra round-trip to the DB server and just feels inelegant. Also don’t think you can avoid coming back in to PHP between queries; mySQL will not allow you to use a mySQL variable (or sub-select clause or function call) as a LIMIT. In PostgreSQL this would be relatively simple: SELECT * FROM table OFFSET RANDOM() LIMIT 1; (obviously you would need slightly more complex logic to make sure RANDOM() is returning a legitimate value within the range of allowable OFFSETs, but this can all be done inside of SQL)

SELECT * FROM `table` WHERE id >= (SELECT FLOOR( MAX(id) * RAND()) FROM `table` ) ORDER BY id LIMIT 1;

Unfortunately mySQL executes the inner select for every single row comparison, so that is at least as slow as the original.

SELECT * FROM Table T JOIN (SELECT CEIL(MAX(ID)*RAND()) AS ID FROM Table) AS x ON T.ID >= x.ID LIMIT 1;

By joining on a nested (or delayed) select in this way, the inner select statement is executed just once. Again there are potential complications with this solution as well: if your IDs are non-sequential (i.e. some get deleted, or they are not an auto_increment field), it will be biased towards higher numbers and not truly random. However even if that is the case you might decide that it’s better to lose true randomness in order to get your query to finish running in .02 seconds instead of 4 minutes.

How do you make that work if you want more than one row? I currently don’t know of any way to do it, although I’m sure it’s possible. One trick would be to find a way to make the inner query return Y rows where Y is the number of rows you want the entire query to return. I know that it’s possible to do that but I haven’t put quite enough thought into how that can be accomplished to have a solution yet.

Another thing we are going to try out is seeding every row with a random value, and then doing our selects based on those, periodically giving those rows new random values. My understanding is that this is how Wikipedia does it. I might also experiment with having a stored procedure build the SQL, dynamically setting a LIMIT before executing the query, thereby preventing mySQL from complaining about using a variable for LIMIT.

Tuesday, 11 September 2018

Randomly ordering data with MySQL with a random value column

It's possible to order data randomly with MySQL using ORDER BY RAND() or doing a count on the table and then using LIMIT to choose a random offset. However neither of these are suitable for dealing with large tables with many rows. In this post I present an alternative to these but it does require modifications to the table and additional business logic and is still not perfect.

Add a "random value" column

This solution requires adding an additional column on the table (and in turn additional business logic) so you should really only use this solution if you need to get random data frequently in a timely manner, such as on publicly accessible pages on a website.
So, if you are sure you really do frequently need some data ordered randomly from a large table, add this column:
ALTER TABLE `mytable` ADD `RandomValue` FLOAT NOT NULL,
ADD INDEX ( `RandomValue` );
Unfortunately it's not possible to make the default value for a column be RAND() , so the values must be initialised separately and business logic added every time an INSERT is done:
UPDATE mytable SET RandomValue = RAND();
Whenever a new record is added to the table make sure the RandomValue column is set:
INSERT INTO mytable (fields ..., RandomValue) VALUES (data ..., RAND());

Selecting a record at random

This needs to be done in a combination of SQL and scripting language. In this example I will use PHP because that's what I code with. $pdo is an already established connection to the MySQL database using PHP's PDO Data Object.
$row = false;
while(!$row) {
   
    $stmt = $pdo->query("SELECT RAND() as rand");
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    $rand = $row['rand'];

    $stmt = $pdo->query("SELECT * FROM test WHERE RandomValue > $rand ORDER BY RandomValue LIMIT 1");
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
   
}
Lines 4 to 6 get a random number from the database. I use a SQL query to get this from the database to ensure the type and number is compatible with what's stored in the RandomValue field. I could not achieve this using PHP's rand() or mt_rand() functions.
Lines 8 and 9 then fetch the record from the database by finding the record whose RandomValue is greater than the random number selected.
Because $rand could be less than MIN(RandomValue) both queries are wrapped within a while loop which keeps going until a row is found. I have seen similar solutions to mine which then look for the RandomValue < $rand but this weights the lowest RandomValue so it will be selected more frequently than the other numbers.
Why not just do "SELECT * FROM test WHERE RandomValue > RAND() ORDER BY RandomValue LIMIT 1" instead of doing the first query to get a random number?
I did try doing that but when I tested it by running the query several thousand times against a table with 9 equally spaced random values from 0.1 to 0.9, but for some reason the lower numbers were selected more frequently and 0.8 and 0.9 almost not at all.
When I changed the query to select a number first and then code that into the query string on the second query I got an almost even distribution each time. Note that this was of course on an ideally spaced RandomValue table, created entirely for testing the even-ness of distribution.

Pros and Cons

The pros for using this method is that it's almost instant to get the record from the database because the random value is indexed. Compare this with ORDER BY RAND() and using LIMIT offsets on larger tables which can get very slow.
The cons on the other hand make this not the most suitable solution, but it does work and is quick.
1. There needs to be an additional column in the database and business logic needs to be added so that every time a new record is inserted it sets a random value for this field. (Noting again that the default value for the column cannot be rand()).
2. It is potentially possible that two or more records could have the same random value resulting in only one of them ever being selected at "random".
3. If the number of records is not particularly large, and/or the random spread of numbers is not particuarly even, then some records are more likely to be selected than others.
4. It is potentially possible (although unlikely) that the loop will get stuck infinitely if the random numbers selected are always less than the lowest number stored in the database.

Conclusion

Fetching random records from a MySQL database is tricky. The easy and obvious solutions work fine until the table starts to have a decent number of records and then the queries take longer and longer to run as the tables get bigger.
I have seen many "solutions" while researching this which easily fail as a proper random solution; the most common of which (other than those I've already presented in earlier posts) is to work out the minimum and maximum values of a primary key, select a random number based on those and either select using = or >=. This particular solution pre-supposes that the primary key will always run in sequence with no holes because the = solution will fail if the record no longer exists and the >= solution has greater weight for records directly after a hole.
The solution I have presented here also has greater weight for records that have a greater gap between the RandomValue if the values are not more or less evenly populated. The smaller the table, the less even the distribution will be.
So in conclusion, there are many approaches to choosing a random record from a MySQL table but none is perfect: they either get too slow as tables increase in size, or are fast but not genuinely random as some records will have more weight than others.

Related posts:

Monday, 10 September 2018

An alternative to ORDER BY RAND() for MySQL

I've posted previously about how to randomly order a resultset with MySQL using RAND() but the issue with RAND() is it will be inefficient on large tables because each row needs to have the random number calculated before the resultset can be ordered. This post looks at an alternative which requires two queries but will be much more efficient for large tables.

Please note

I have written a later post titled "MySQL's LIMIT syntax can be slow for large offsets". While using LIMIT syntax as shown in this post is a lot faster than ORDER BY RAND() it's still slow for larger tables. I'm currently working on a better alternative.

The alternative, and a note about INNODB vs MyISAM tables

The alternative suggested in this post uses COUNT(*) first to get the number of records in the table and then picks the record by using MySQL's LIMIT syntax. Note that INNODB does not cache the count of a table like MyISAM does so it takes slightly longer to return the count.

Example table

I often use an example table containing fruit. Here's the output from SELECT * FROM fruit:
+----------+--------+-----------+
| fruit_id | name   | somevalue |
+----------+--------+-----------+
|        1 | Banana |         2 |
|        2 | Orange |         4 |
|        3 | Cherry |         3 |
|        4 | Apple  |         1 |
+----------+--------+-----------+

MySQL only solution

It's possible to do this entirely with MySQL SQL queries without having to run code in PHP or other programming language. The following SQL queries first gets a count from the table, then selects a random offset based on that count. It then prepares a statement so the calculated offset can be used and executes the statement. Note that the offset is cast as a signed integerl without this you'll get the error message "ERROR 1210 (HY000): Incorrect arguments to EXECUTE".
SELECT @count := COUNT(*) FROM fruit;
SET @offset = CONVERT(FLOOR(RAND() * @count), SIGNED);
PREPARE mystatement FROM "SELECT * FROM fruit LIMIT ?, 1";
EXECUTE mystatement USING @offset;
DEALLOCATE PREPARE mystatement;
The output from the above will result in a random record returned each time e.g.:
+----------+--------+-----------+
| fruit_id | name   | somevalue |
+----------+--------+-----------+
|        3 | Cherry |         3 |
+----------+--------+-----------+
This works from the MySQL command line but doesn't appear to work in tools like phpMyAdmin (which does successfully execute the SQL but doesn't output any data) or MySQL Query Browser. It does work programatically from PHP (and therefore will for other programming languages). For exmample this:
mysql_query('SELECT @count := COUNT(*) FROM fruit');
mysql_query('SET @offset = CONVERT(FLOOR(RAND() * @count), SIGNED)');
mysql_query('PREPARE mystatement FROM "SELECT * FROM fruit LIMIT ?, 1"');
$res = mysql_query('EXECUTE mystatement USING @offset');
$row = mysql_fetch_assoc($res);
print_r($row);
outputs this:
Array
(
    [fruit_id] => 1
    [name] => Banana
    [somevalue] => 2
)

Using a programming language

Doing this outside MySQL is done in a similar way by getting the count from the table first, working out a random offset and then running a second query to get the record. This example uses PHP:
$res = mysql_query("SELECT COUNT(*) FROM fruit");
$row = mysql_fetch_array($res);
$offset = rand(0, $row[0]-1);

$res = mysql_query("SELECT * FROM fruit LIMIT $offset, 1");
$row = mysql_fetch_assoc($res);

Conclusion

My example only uses a small table and in this instance would be easier to simply use "ORDER BY RAND()" but ordering in this way is inefficient and performance will start to suffer as the table grows. Instead it can be more efficient to run a couple of queries instead to work out an offset based on the number of records in the table, and then use MySQL's LIMIT syntax to pull that record out of the table as shown in this post.

Related posts:

Tuesday, 4 September 2018

MySQL - ORDER BY STR_TO_DATE does not work

I am trying to run a PDO query with PHP and MySql and then sort it by date.

My query is below:
    $query_params = array(
        ':website' => $site
    );

    $query = "
        SELECT
            DATE_FORMAT(date, '%d/%m/%Y') AS date,
            id
        FROM
            items as bi
        INNER JOIN
            basket as bb ON bi.item_number=bb.basket_item
        INNER JOIN
            orders as bo ON bb.basket_order=bo.order_number
        WHERE
            bi.website = :website
        ORDER BY
            STR_TO_DATE(date,'%d/%m/%Y') DESC
    "; 

    try {
        $stmt = DB::get()->prepare($query);
        $stmt->execute($query_params);
        $rows = $stmt->fetchAll();
    }
    catch(PDOException $ex) {} 

    foreach($rows as $row):
        $output .= "".$row["date"].",";
        $output .= "".$row["id"].",";
        $output .= "\r\n <br />";
    endforeach;

Where my output should be:
13/06/2014, 8676,
12/06/2014, 5765,
12/04/2014, 7683,
08/12/2013, 1098,
06/12/2013, 2003,
06/12/2013, 6755,

It doesn't seem to be sorting by anything:
12/06/2014, 5765,
12/04/2014, 7683,
13/06/2014, 8676,
06/12/2013, 2003,
06/12/2013, 6755,
08/12/2013, 1098,

Should STR_TO_DATE(date,'%d/%m/%Y') DESCnot be sorting as intended?

You already have a lovely date column in your table - why on earth try to sort by some formatted string based on that?
    SELECT
        DATE_FORMAT(date, '%d/%m/%Y') AS date,
        id
    FROM
        items as bi
    INNER JOIN
        basket as bb ON bi.item_number=bb.basket_item
    INNER JOIN
        orders as bo ON bb.basket_order=bo.order_number
    WHERE
        bi.website = :website
    ORDER BY
        date DESC

Sure, format the date output to the user however you like - but you are not only making the DB do a lot more by formatting each row of data then sorting by something that could be done natively by the database the way it was meant to be.
Edit: Interesting. I wonder if the fact that date is a semi-reserved word is causing your sort not to happen as expected?
Maybe try this:
    ORDER BY
        bo.date DESC

Friday, 5 June 2015

Mysql: Display MySQL database rows alphabetically

<?php

//connect to server with username and password, this is the default settings

//when MySQL is installed on Windows XP(Not recommended)

$connection = mysql_connect ("localhost","root", "") or die ("Cannot make the connection");

//connect to database

$db = mysql_select_db ("test",$connection) or die ("Cannot connect to database");

//our SQL query

$sql_query = "SELECT * FROM test ORDER BY name ASC";

//store the SQL query in the result variable

$result = mysql_query($sql_query);

if(mysql_num_rows($result)){

//output as long as there are still available fields
while($row = mysql_fetch_row($result)){
echo ("<a href=\"$row[2]\">$row[3]</a>");
echo (": $row[4]<br>");
}

}//if no fields exist
else{
echo "no values in the database";
}
?>

Tuesday, 2 June 2015

Mysql: Custom ordering (order rows by my order)

CREATE TABLE groups(
  groupno INT(11) DEFAULT NULL,
  eventno INT(11) DEFAULT NULL
);
INSERT INTO groups VALUES 
  (1, 4),
  (2, 8),
  (4, 3),
  (1, 5),
  (3, 1);

Output custom ordered records:
SELECT groupno, eventno FROM groups ORDER BY FIELD(groupno, 3,2,1,4);
+---------+---------+
| groupno | eventno |
+---------+---------+
|       3 |       1 |
|       2 |       8 |
|       1 |       4 |
|       1 |       5 |
|       4 |       3 |