I have never had a need to do a random SELECT on a MySQL DB until this project I'm working on. After researching it seems the general populous says that using RAND() is a bad idea. I found an article that explains how to do another type of random select.
Basically, if I want to select five (5) random elements, I should do the following (I'm using the Kohana framework here)?
<?php
final class Offers extends Model
{
/**
* Loads a random set of offers.
*
* #param integer $limit
* #return array
*/
public function random_offers($limit = 5)
{
// Find the highest offer_id
$sql = '
SELECT MAX(offer_id) AS max_offer_id
FROM offers
';
$max_offer_id = DB::query(Database::SELECT, $sql)
->execute($this->_db)
->get('max_offer_id');
// Check to make sure we're not trying to load more offers
// than there really is...
if ($max_offer_id < $limit)
{
$limit = $max_offer_id;
}
$used = array();
$ids = '';
for ($i = 0; $i < $limit; )
{
$rand = mt_rand(1, $max_offer_id);
if (!isset($used[$rand]))
{
// Flag the ID as used
$used[$rand] = TRUE;
// Set the ID
if ($i > 0) $ids .= ',';
$ids .= $rand;
++$i;
}
}
$sql = '
SELECT offer_id, offer_name
FROM offers
WHERE offer_id IN(:ids)
';
$offers = DB::query(Database::SELECT, $sql)
->param(':ids', $ids)
->as_object();
->execute($this->_db);
return $offers;
}
}
If not, what is a better solution?
That approach will work, as long as your offer_id's are sequential and all continuous - if you ever remove an offer, you might have gaps in the id's that would then be a problem.
I've read the same things about the MySQL rand() function on large table sets, but I would think you could do it faster by counting the table rows, then using PHP's built in rand(0, count) to generate a few index ID's you can grab in a SELECT. I suspect it would have the same affect but without all the performance concerns.
Related
I have an mobile app that request a key in my server, The key structure contains 7 characters as follows:
# + [0-9] + [0-9] + [0-9] + [A-Z] + [A-Z] + [0-9]
#876EU8, #668KI2 .......
Whereas the key initially has seven characters, in this case three numbers, two letters and one number, doing the math this gives a maximum of 676,000 keys.
To gerate this keys I'm using this code in PHP:
function generateRandomString($length = 2) {
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
$randomKeyNumber = rand(100,999);
$randomKeyLetter = generateRandomString();
$randomKeyLast = rand(0,9);
$randomKey = "#".$randomKeyNumber.$randomKeyLetter.$randomKeyLast;//Returns a key like #876TG9
The next code check if the key exists inside the database, If exists he random another key, if not he insert the key in database and return this key to my app
This code works perfectly, but assuming the system has already generated a total of 650,000 keys, in the case of this code it would always generate the same keys, and the likelihood of it generate a key that does not exist yet is very small.
How can I solve this problem and avoid future problems? (There is no problem in creating the keys in an orderly manner, eg 000AA0, 000AA1, 000AA2, 000AA3 .... 999ZZ9)
What you can do is make a PDO::query() to issue a SELECT COUNT(*) or simply a SELECT * statement with all the keys you already have added, and then use PDOStatement::fetchColumn() to retrieve the number of rows that will be returned (i.e. in this case, all of them)
This is a manual example
<?php
$sql = "SELECT COUNT(*) FROM Keys";
if ($res = $conn->query($sql)) {
/* Check the number of rows that match the SELECT statement */
if ($res->fetchColumn() > 0) {
/* Issue the real SELECT statement and work with the results */
$sql = "SELECT name FROM fruit WHERE calories > 100";
foreach ($conn->query($sql) as $row) {
print "Name: " . $row['NAME'] . "\n";
}
}
/* No rows matched -- do something else */
else {
print "No rows matched the query.";
}
}
$res = null;
$conn = null;
?>
This is the code you need for your case:
<?php
$sql = "SELECT * From Keys";
if ($res = $conn->query($sql)) {
/* Check the number of rows that match the SELECT statement */
if ($res->fetchColumn() > 0) {
/* and then you get the id of the last one on the list, and to that one you add 1 */
$last_id = $conn->lastInsertId();
$new_id = $last_id + 1;
/* then you insert that in some place inside the key itself, that way you don't need to worry than two keys can be equal */
}
else {
/* No rows matched, just create a key and add to the database here */
}
}
¿>
Alternatively you can make a query SELECT statement combined with the countRows in PDO, it doesn't work all the times in the portable apps and/or databases, but like we don't know more about your app we can't know if this is gonna work.
PS. Don't use rand(). Use mt_rand() instead. It is more efficient with the resources of the server ;)
I feel this is a more logic problem than anything. A database has pictures saved via a source reference and booleans for tags e.g. isLandscape=1. I had made a system to traverse pages of results based on types asked. The following is an example of what I'm facing. I only see the same 12 pictures from page 0 -> page 22. Then I start to see new ones. I think I have just been overlooking this bug since I had not noticed it until now. One thing I noticed was page22*12pictures = 264 which is the same as the first new picture id that is seen. You can see the error here (just change the p to different pages).
<?php
$pictureid = -1;
$startpage = 0;
$viewsection = -1;
$uid = -1; //user id
$amntperrow = 4; //how many pictures per row, must correlate with doThumb()'s switch case amounts
$maxrows = 3; //how many rows of pictures to drop
if(isset($_GET['pid']) && is_int(intval($_GET['pid']))) $pictureid = clean($_GET['pid']);
if(isset($_GET['sec']) && is_int(intval($_GET['sec']))) $viewsection = clean($_GET['sec']);
if(isset($_GET['p']) && is_int(intval($_GET['p']))) $startpage = clean($_GET['p']);
$result = generateResult(array("isFlowers"), $startpage);
//**snip** -- drawing thumbnails would happen here
function generateResult($types, $page) {
global $amntperrow;
global $maxrows;
$sqlWheres = "";
$idAmnt = ($amntperrow*$maxrows)*$page;
if(isset($types) && !empty($types)) {
if(count($types) >= 1) {
for($i = 0; $i<count($types); $i++) {
$sqlWheres .= $types[$i] . "='1'";
if($i < count($types)-1) $sqlWheres .= " AND ";
}
}
}
$result = "SELECT * FROM pictures WHERE ";
if(!empty($sqlWheres)) $result .= $sqlWheres . " AND " ;
$result .= " private='0' AND id >='" . $idAmnt . "' LIMIT " . ($amntperrow*$maxrows);
return $result;
}
?>
This seems like a glaring bug that I am overlooking. Thanks for the help.
What is the difference between these two queries?
SELECT *
FROM pictures
WHERE private = '0' AND id >= '24'
LIMIT 12;
and
SELECT *
FROM pictures
WHERE private = '0' AND id >= '36'
LIMIT 12;
Answer: potentially no difference at all. The database engine can decide in either case that it wants to return pictures with ids 100 through 111 - that result set meets all of the conditions of either query.
Try a query like this instead:
"SELECT *
FROM pictures
WHERE private = '0'
ORDER BY id
LIMIT " . $idAmnt . ", " . ($amntperrow * $maxrows)
The ORDER BY id is really the key. Paging through database results is generally done with a combination of ORDER BY and LIMIT.
I have Adjacency list mode structure like that and i want to count all title of parent according level like Food = (2,4,3), Fruit = (3,3)
tree tabel structure
after that make tree like that
by this code i m getting right total like for Food =9, Fruit = 6
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = 0;
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count += 1 + $this->display_children($row['title'], $level+1);
}
return $count;
}
call function
display_children(Food, 0)
Result : 9 // but i want to get result like 2,4,3
But i want to get count total result like that For Food 2,4,3 and For Fruit 3,3 according level
so plz guide how to get total according level
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = "";
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
if($count!="")
$count .= (1 + $this->display_children($row['title'], $level+1));
else
$count = ", ".(1 + $this->display_children($row['title'], $level+1));
}
return $count;
}
Lets try this once..
If you want to get amounts by level, then make the function return them by level.
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree WHERE parent="'.$parent.'"');
$count = array(0=>0);
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count[0]++;
$children= $this->display_children($row['title'], $level+1);
$index=1;
foreach ($children as $child)
{
if ($child==0)
continue;
if (isset($count[$index]))
$count[$index] += $child;
else
$count[$index] = $child;
$index++;
}
}
return $count;
}
Note that its hard for me to debug the code as i dont have your table. If there is any error let me know and i will fix it.
Anyways result will be array
which should contain amounts of levels specified by indices:
$result=display_children("Food", 0) ;
var_export($result);//For exact info on all levels
echo $result[0];//First level, will output 2
echo $result[1];//Second level, will output 4
echo $result[2];//Third level, will output 3
And by the way there is typo in your database, id 10 (Beef) should have parent "Meat" instead of "Beat" i guess.
If you want to see testing page, its here.
This article has all you need to creates a tree with mysql, and how count item by level
If you don't mind changing your schema I have an alternative solution which is much simpler.
You have your date in a table like this...
item id
-------------+------
Food | 1
Fruit | 1.1
Meat | 1.2
Red Fruit | 1.1.1
Green Fruit | 1.1.2
Yellow Fruit | 1.1.3
Pork | 1.2.1
Queries are now much simpler, because they're just simple string manipulations. This works fine on smallish lists, of a few hundred to a few thousand entries - it may not scale brilliantly - I've not tried that.
But to count how many things there are at the 2nd level you can just do a regexp search.
select count(*) from items
where id regexp '^[0-9]+.[0-9]+$'
Third level is just
select count(*) from items
where id regexp '^[0-9]+.[0-9]+.[0-9]+$'
If you just want one sub-branch at level 2
select count(*) from items
where id regexp '^[0-9]+.[0-9]+$'
and id like "1.%"
It has the advantage that you don't need to run as many queries on the database, and as a bonus it's much easier to read the data in the tables and see what's going on.
I have a nagging feeling this might not be considered "good form", but it does work very effectively. I'd be very interested in any critiques of this method, do DB people think this is a good solution? If the table were very large, doing table scans and regexps all the time would get very inefficient - your approach would make better use of the any indexes, which is why I say this probably doesn't scale very well, but given you don't need to run so many queries, it may be a trade off worth taking.
An solution by a php class :
<?php
class LevelDepCount{
private $level_count=array();
/**
* Display all child of an element
* #return int Count of element
*/
public function display_children($parent, $level, $isStarted=true)
{
if($isStarted)
$this->level_count=array(); // Reset for new ask
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = 0; // For the level in the section
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count += 1 + $this->display_children($row['title'], $level+1,false);
}
if(array_key_exists($level, $this->level_count))
$this->level_count[$level]+=$count;
else
$this->level_count[$level]=$count;
return $count;
}
/** Return the count by level.*/
public function getCountByLevel(){
return $this->level_count;
}
}
$counter=new LevelDepCount();
$counter->display_children("Food",0);
var_dump($counter->getCountByLevel());
?>
If you modify your query you can get all the data in one swoop and without that much calculations (code untested):
/* Get all the data in one swoop and arrange it for easy mangling later */
function populate_data() {
$result = mysql_query('SELECT parent, COUNT(*) AS amount, GROUP_CONCAT(title) AS children FROM tree GROUP BY parent');
$data = array();
while ($row = mysql_fetch_assoc($result)) {
/* Each node has the amount of children and their names */
$data[$row['parent']] = array($row['children'], int($row['amount']));
}
return $data;
}
/* The function that does the whole work */
function get_children_per_level($data, $root) {
$current_children = array($root);
$next_children = array();
$ret = array();
while(!empty($current_children) && !empty($next_children)) {
$count = 0;
foreach ($current_children as $node) {
$count += $data[$node][0]; /* add the amount */
$next_children = array_merge($next_children, explode($data[$node][1])); /* and its children to the queue */
}
ret[] = $count;
$current_children = $next_children;
$next_children = array();
}
return $ret;
}
$data = populate_data();
get_children_per_level($data, 'Food');
It shouldn't be difficult to modify the function to make a call per invocation or one call per level to populate the data structure without bringing the whole table into memory. I'd suggest against that if you have deep trees with just a few children as it is a lot more efficient to get all the data in one swoop and calculate it. If you have shallow trees with a lot of children, then it may be worth changing.
It would also be possible to put everything together in a single function, but I'd avoid re-calculating data for repeated calls when they are not needed. A possible solution for this would be to make this a class, use the populate_data function as the constructor that stores it as an internal private property and a single method that is the same as get_children_per_level without the first parameter as it would get the data off its internal private property.
In any case, I'd also suggest you use the ID column as a "parent" reference instead of other columns. To start with, my code will break if any of the names contains a comma :P. Besides, you may have two different elements with the same name. For example, you could have Vegetables -> Red -> Pepper and the Red will get slumped together with the Fruit's Red.
Another thing to note is that my code will enter an infinite loop if your DB data is not a tree. If there is any cycle in the graph, it will never finish. That bug could be easily solved by keeping a $visited array with all the nodes that have already been visited and not pushing them into the $next_children array within the loop (probably using array_diff($data[$node][1], $visited).
Hi I am creating a system that processes and ID and a UID, The UID we are generating randomly but I am a little stuck, I need to always generate a UID that does not currently exist in the db as the field is a unique field used on the front end so as not to expose the real ID.
So to recap, I am trying to generate a unique id that does not currently exist in the DB the part I haven't got working is the cross checking in the db so it sometimes will give a number that already exists in the db even though it shouldn't thanks in advance.
This is my code so far:
function uniqueID($table)
{
$db = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
$possible = '1234567890';
$code = '';
$characters = mt_rand(7,14);
$i = 0;
while($i < $characters)
{
$code .= substr($possible, mt_rand(0, strlen($possible)-1), 1);
$i++;
}
$result = $db->query('
SELECT uniqueID
FROM '.$table.'
WHERE uniqueID = "'.$code.'"
LIMIT 1
');
$totalRows = $result->num_rows;
if(!$result)
{
return $db->error;
}
else
{
if($totalRows > 0)
{
return uniqueID($table);
}
else
{
return $code;
}
}
}
http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_uuid
To generate unic UID you can use time, i think it was a very small chanse that records will be added in the same second, with two random data.
write some function which return it to you like that
function generate_uid(){
return md5(mktime()."-".rand()."-".rand());
}
In PHP there's a function called uniqid()
http://php.net/manual/en/function.uniqid.php
I could talk about generating ids, like the others did, but this is not your question.
Your query seems fine. If it returns 0 rows but you seem to find the code in the database, then most likely it only looks the same, but actually isn't. It could be padded by whitespace.
One way to solve this is by selecting the last row of your user database and have your script to check for the id field (you can achieve this by performing a select ordering by ID in descendent mode) then you can use that info for randomize numbers greater than that ID.
EDIT
$result = $db->query('
SELECT uniqueID
FROM '.$table.'
');
$already_in_database = array();
while ($row = mysql_fetch_array($result)){
$already_in_database[] = $row['UID'];
}
$new = rand(0,$some_max_value);
while(in_array($new,$already_in_database)){
$new = rand(0,$some_max_value);
}
I figured out what the problem was already, As I mentioned to everyone the code generation was not the issue! The issue was that the cross check was not working correctly. So all I did was removed this loop
while($i < $characters)
{
$code .= substr($possible, mt_rand(0, strlen($possible)-1), 1);
$i++;
}
As this was causing my unique ID to end up wrong.
I have a game script thing set up, and when it creates a new character I want it to find an empty address for that players house.
The two relevant table fields it inserts are 'city' and 'number'. The 'city' is a random number out of 10, and the 'number' can be 1-250.
What it needs to do though is make sure there's not already an entry with the 2 random numbers it finds in the 'HOUSES' table, and if there is, then change the numbers. Repeat until it finds an 'address' not in use, then insert it.
I have a method set up to do this, but I know it's shoddy- there's probably some more logical and easier way. Any ideas?
UPDATE
Here's my current code:
$found = 0;
while ($found == 0) {
$num = (rand()%250)+1; $city = (rand()%10)+1;
$sql_result2 = mysql_query("SELECT * FROM houses WHERE city='$city' AND number='$num'", $db);
if (mysql_num_rows($sql_result2) == 0) { $found = 1; }
}
You can either do this in PHP as you do or by using a MySQL trigger.
If you stick to the PHP way, then instead of generating a number every time, do something like this
$found = 0;
$cityarr = array();
$numberarr = array();
//create the cityarr
for($i=1; $i<=10;$i++)
$cityarr[] = i;
//create the numberarr
for($i=1; $i<=250;$i++)
$numberarr[] = i;
//shuffle the arrays
shuffle($cityarr);
shuffle($numberarr);
//iterate until you find n unused one
foreach($cityarr as $city) {
foreach($numberarr as $num) {
$sql_result2 = mysql_query("SELECT * FROM houses
WHERE city='$city' AND number='$num'", $db);
if (mysql_num_rows($sql_result2) == 0) {
$found = 1;
break;
}
}
if($found) break;
}
this way you don't check the same value more than once, and you still check randomly.
But you should really consider fetching all your records before the loops, so you only have one query. That would also increase the performance a lot.
like
$taken = array();
for($i=1; $i<=10;$i++)
$taken[i] = array();
$records = mysql_query("SELECT * FROM houses", $db);
while($rec = mysql_fetch_assoc($records)) {
$taken[$rec['city']][] = $rec['number'];
}
for($i=1; $i<=10;$i++)
$cityarr[] = i;
for($i=1; $i<=250;$i++)
$numberarr[] = i;
foreach($cityarr as $city) {
foreach($numberarr as $num) {
if(in_array($num, $taken[]) {
$cityNotTaken = $city;
$numberNotTaken = $number;
$found = 1;
break;
}
}
if($found) break;
}
echo 'City ' . $cityNotTaken . ' number ' . $numberNotTaken . ' is not taken!';
I would go with this method :-)
Doing it the way you say can cause problems when there is only a couple (or even 1 left). It could take ages for the script to find an empty house.
What I recommend doing is insert all 2500 records in the database (combo 1-10 with 1-250) and mark with it if it's empty or not (or create a combo table with user <> house) and match it on that.
With MySQL you can select a random entry from the database witch is empty within no-time!
Because it's only 2500 records, you can do ORDER BY RAND() LIMIT 1 to get a random row. I don't recommend this when you have much more records.