SQL / PHP - Optimize query inside a while loop - php

I have a fairly simple query:
$r = $dbh->prepare("SELECT user FROM this_users_rented WHERE user_by=:user LIMIT $offset, $rowsperpage");
$r->bindParam(':user', $userdata['username']);
$r->execute();
($offset and $rowsperpage is representing the offset of the list, based on the current page, and how many records there should be shown per page. (Example: 0,100))
This will gather all the data from this_users_rented where the user_by is = $userdata['username'];
I am running this query in a WHILE LOOP:
while($data=$r->fetch()):
//Get data from table: this_users_rented to print out in the while loop.
$stmt = $dbh->prepare("SELECT * FROM xeon_users_rented_stats WHERE urs_user=:user");
$stmt->bindParam(':user', $data['user']);
$stmt->execute();
$refStat = $stmt->fetch();
endwhile;
So, imagine that there is hundreds of records in the $r query - yielding hundreds of queries to be run (due to the lack of optimization of the $stmt query)
So my question is, how can I optimize the $stmt query?

You have a LIMIT clause on your user table so you could use following trick to overcome the MySQL limitation that you can't use a LIMIT clause in a subselect for the preparing of the statement:
$r = $dbh->prepare("
SELECT x.*
FROM xeon_users_rented_stats x
INNER JOIN (
SELECT
user_by
FROM this_users_rented
WHERE user_by = :user
LIMIT $offset, $rowsperpage
) t
ON x.urs_user = t.user_by
ORDER BY x.urs_user;"
);
$r->bindParam(':user', $userdata['username']);
$r->execute();
while($refstat=$r->fetch()){
// do what you want to do ...
}
This trick changes the subselect to a materialized derived table where you can use LIMIT.
Note:
Of course you should test the sql statement in a sql client first to make sure you get the data you need.

Related

How do I count unique rows in php pdo?

Here's my usual way of counting rows...
$query = "SELECT * FROM users";
$stmt = $db->prepare($query);
$stmt->execute();
$count = $stmt->rowCount();
This will count all rows, even if I use a WHERE clause, it'll still count every row that meets that condition. However, let's say I have a table, we'll call it tokensEarned (that's my actual table name). I have the following data...
user_id = 1,2,4,5,8,8,2,4,3,7,6,2 (those are actual rows in my table - clearly, user 1 has 1 entry, 2 has three entries, etc.) In all, I have 12 entries. But I don't want my query to count 12. I want my query to count each user_id one time. In this example, my count should display 8.
Any help on this? I can further explain if you have any specific questions or clarification you need. I would appreciate it. Thank You.
The following query will yield the distinct user count:
$query = "SELECT COUNT(DISTINCT user_id) AS cnt FROM users";
$stmt = $db->prepare($query);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo "distinct user count: " . $row['cnt'];
It isn't possible to get all records and the distinct count in a single query.
Whether you use the query above or you return all the actual distinct rows really depends on whether you need the full records. If all you need are the counts, then it is wasteful to return the data in the records, and what I gave above is probably the best option. If you do need the data, then selecting all distinct rows might make more sense.
You can use distinct in mysql to select only unique fields in your table.
$query = "SELECT distinct user_id FROM users";
$stmt = $db->prepare($query);
$stmt->execute();
$count = $stmt->rowCount();
Change your query to the following, this way you only shows the unique user_id:
$query = "SELECT DISTINCT user_id FROM users";

Select random row from table with deleted records

If I have a table with 3 rows with IDs 1,3,5 because rows with ID 2 and 4 were deleted, how do I make sure I select a row that exists?
$stmt = $db->prepare("SELECT COUNT(*) FROM table");
$stmt->execute();
$stmt->bind_result($numRows);
$stmt->fetch();
$stmt->close();
$random = mt_rand(1,$numRows);
$stmt = $db->prepare("SELECT link FROM table WHERE id=$random");
This won't ever select row with id 5, and also will select one that doesn't exist (2).
If the number of rows are small (and you are sure that it will stay that way), you can use ORDER BY RAND()
(Please note that this will create performance problems with big tables).
Other way is first counting how many rows are there
SELECT COUNT(*) AS total FROM table;
then pick a random number
$rand = rand(1, $total);
and select that row with limit
SELECT * FROM table LIMIT $rand, 1;
U can use a SQLstatement with EXISTS
SELECT link
FROM table
WHERE EXISTS (SELECT link
FROM table
WHERE id = $random);
If you just want a random row and don't care about the id, then you could use:
SELECT link FROM table
ORDER BY RAND()
LIMIT 1
For large numbers of rows (10000+), then you may need to implement another solution, as this query can be slow. This site has a good explanation and alternative solutions
If you want to follow your approach then you have to do some changes in your query.
1.) Query one : select id from table. // It will give you array of existing id.
2.) You have to use array_rand(). and use your second query.
Example :
$stmt = $db->prepare("SELECT ID FROM table");
$result = $stmt->fetchAll();
$random = array_rand(array_flip($result), 1);
$stmt = $db->prepare("SELECT link FROM table WHERE id=$random");
You could select one, randomly order, like this:
SELECT link FROM table ORDER BY RAND() LIMIT 1
UPDATE:
You should benchmark the different solutions you have, but I'm thinking this one could be nice with large amount of rows:
$stmt = $db->prepare("SELECT COUNT(*) FROM table");
$stmt->execute();
$stmt->bind_result($numRows);
$stmt->fetch();
$stmt->close();
$random = mt_rand(1,$numRows);
$stmt = $db->prepare("SELECT link FROM table WHERE id>=$random LIMIT 1");
$stmt->execute();
$stmt->bind_result($link);
if(!$link){
$stmt = $db->prepare("SELECT link FROM table WHERE id<$random LIMIT 1");
$stmt->execute();
$stmt->bind_result($link);
}

MySQL PDO, selecting a single row from a result-set

If I run a query that returns multiple rows, is there a way I can select just one row out of that result?
So if I do something like
SELECT * FROM table WHERE number = 10
and it returns 33 results, is there a way I can go through those one at a time instead of returning the whole result set at once, or just return, for example, row 5 of the result set?
I have read about scrollable cursors but it seems they don't work on MySQL, although that seems to be what I am looking for....
I am using PDO with MySQL and PHP. I hope this makes sense, if not I will try and explain better.
Edit: This worked for what I wanted. Thanks.
$stmt = $dbh->prepare("SELECT * FROM $table WHERE user_points = '$target' ORDER BY tdate DESC LIMIT $count,1");
is there a way I can select just one row out of that result?
Yes there is, you can use LIMIT:
SELECT * FROM table WHERE number = 10 LIMIT 1;
$sql= "SELECT * FROM $table WHERE user_points = '$target' ORDER BY tdate";
$stmt= $pdo -> prepare($sql);
$stmt->execute();
$data = $stmt ->fetchAll();
//You asked about getting a specific row 5
//rows begin with 0. Now $data2 contains row 5
$data2 = $data[4];
echo $data2['A_column_in_your_table'];//row 5 data

What columns do I select if I only want a count

I want to count the results in my table... but I am usually confronted with a decision, what column do I select? Should I select the primary key? Wild Card? What has the most performance? Does it matter? Below is an example of how I call it
// Wild Card, I feel like this is the worst one for performance?
$query = "SELECT * FROM table WHERE status = ?";
// Only selecting one column? Is there a better way
$query = "SELECT id FROM table WHERE status = ?";
$stmt = $mysqli->prepare($query);
$stmt->bind_param('i',$status);
$stmt->execute();
$stmt->store_result();
$returned_amount = $stmt->num_rows;
$stmt->free_result();
$stmt->close();
Well, if you want MySQL to handle the count, you can just do the following
$query = "SELECT COUNT(*) as count FROM `table` WHERE `field` = ?";
The as count part means that you can access the count as if it were a column.
You should SELECT COUNT(`id`) FROM `table` WHERE `status`=?, much more efficient ;)
If you are willing to count all the results of your query (ie: for pagination), you can use FOUND_ROWS() option:
In your main query you need to add SQL_CALC_FOUND_ROWS option just after SELECT and in second query you need to use FOUND_ROWS() function to get total number of rows.
SELECT SQL_CALC_FOUND_ROWS id FROM table WHERE status = 'something' LIMIT 10;
SELECT FOUND_ROWS();
More info in dev.mysql.com

How to get the SQL_CALC_FOUND_ROWS value using prepared statements?

I'm currently scratching my head at how to implement SQL_CALC_FOUND_ROWS with prepared statements.
I'm writing a pagination class and obviously i want to add LIMIT to the query but also find what the total number of rows would be.
Here's an example from the class in question.
$query = "select SQL_CALC_FOUND_ROWS id,title,location,salary,employer from jobs where region=38 limit 0,3";
if($stmt = $connection->prepare($query)) {
$stmt->execute()or die($connection->error); //execute query
$stmt->bind_result($id,$title,$location,$salary,$employer,$image);
while($stmt->fetch()){
$jobs[$x]['id']=$id;
$jobs[$x]['title']=$title;
$jobs[$x]['location']=$location;
$jobs[$x]['salary']=$salary;
$jobs[$x]['employer']=$employer;
$jobs[$x]['image']=$image;
$x++;
}
$stmt->close();//close statement
}
I'm a bit stumped as to how to get the SQL_CALC_FOUND_ROWS actual value? I had thought adding in something like:
$stmt->store_result();
$count=$stmt->num_rows;
But that only gives a number based on the LIMIT, so in the above example its 3 rather than the full 6 that it should be.
Managed to figure it out, i will detail my answer below for anyone whos interested in future.
Original Code
$query="select SQL_CALC_FOUND_ROWS id,title,location,salary,employer from jobs where region=38 limit 0,3";
if($stmt = $connection->prepare($query)) {
$stmt->execute()or die($connection->error); //execute query
$stmt->bind_result($id,$title,$location,$salary,$employer,$image);
while($stmt->fetch()){
$jobs[$x]['id']=$id;
$jobs[$x]['title']=$title;
$jobs[$x]['location']=$location;
$jobs[$x]['salary']=$salary;
$jobs[$x]['employer']=$employer;
$jobs[$x]['image']=$image;
$x++;
}
$stmt->close();//close statement
}
Updated Code
$query="select SQL_CALC_FOUND_ROWS id,title,location,salary,employer from jobs where region=38 limit 0,3";
if($stmt = $connection->prepare($query)) {
$stmt->execute()or die($connection->error); //execute query
$stmt->bind_result($id,$title,$location,$salary,$employer,$image);
while($stmt->fetch()){
$jobs[$x]['id']=$id;
$jobs[$x]['title']=$title;
$jobs[$x]['location']=$location;
$jobs[$x]['salary']=$salary;
$jobs[$x]['employer']=$employer;
$jobs[$x]['image']=$image;
$x++;
}
//get total number of rows.
$query="SELECT FOUND_ROWS()";
$stmt = $connection->prepare($query);
$stmt->execute();
$stmt->bind_result($num);
while($stmt->fetch()){
$count=$num;
}
$stmt->close();//close statement
}
Probably could do it better another way but couldn't seem to find any good examples anywhere online and this works!
If you want to get the result of SQL_CALC_FOUND_ROWS you need to run query SELECT FOUND_ROWS() in MySQL. To do that you don't need prepared statement. You can just use query() method.
$connection->query('SELECT FOUND_ROWS()')->fetch_row()[0];
If you are using mysqlnd, which you should be using if you are on one of the supported PHP versions, you can make your code much simpler.
// Enable error reporting instead of using or die($connection->error)
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$query = "SELECT SQL_CALC_FOUND_ROWS id,title,location,salary,employer
FROM jobs
WHERE region=38
LIMIT 0,3";
$stmt = $connection->prepare($query);
$stmt->execute();
$jobs = $stmt->fetch_all(MYSQLI_ASSOC);
// There is no need for prepared statement when using FOUND_ROWS()
$connection->query('SELECT FOUND_ROWS()')->fetch_row()[0];
As you can see, it is much cleaner to use fetch_all() rather than while loop.
Warning
The SQL_CALC_FOUND_ROWS query modifier and accompanying FOUND_ROWS()
function are deprecated as of MySQL 8.0.17 and will be removed in a
future MySQL version. As a replacement, considering executing your
query with LIMIT, and then a second query with COUNT(*) and without
LIMIT to determine whether there are additional rows. For example,
instead of these queries:
SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name WHERE id > 100 LIMIT 10;
SELECT FOUND_ROWS();
Use these queries instead:
SELECT * FROM tbl_name WHERE id > 100 LIMIT 10;
SELECT COUNT(*) WHERE id > 100;

Categories