Limit number of results using column value as a count - php

I have two queries, one which will get a list of IDs from the main table, and another which will get all records in another table that relate to that ID.
I'd like to split it into pages of results, but my google searches are only coming up with people who want to have a certain number of results per column value, not who want to limit the overall count by it.
The main table has a column that contains the number of records, so actually reading the other table isn't needed. The limit would be used as a minimum value, so if there are still records left in the current group after the limit, it'll continue displaying them. That'd be somehow calculated as part of the offset so it can start in the correct place.
Here's an example of what I mean:
Table 1:
ID | Records
1 | 2
2 | 3
3 | 28
4 | 7
...
Table 2 (contents don't need to be known for this question):
ID | GroupID | Value
1 | 1 | x
2 | 1 | x
3 | 2 | x
4 | 2 | x
5 | 2 | x
6 | 3 | x
...
If the limit was given as 3 for example, both 1 and 2 should display on the first page, since just 1 by itself is under the limit. The next page will then start on 3, and that'll take up the entire page.
I could just manually count up using PHP until I reach the limit, though it might end up going slow if there were a lot of pages (I've no idea if mysql would be any better in that regard though). Here's a quick example of how that'd work to get the offset:
$page = 2;
$limit = 40;
$count = 0;
$current_page = 1;
$query = 'SELECT ID, Records FROM table1';
$stmt = $conn->prepare($query);
$stmt->execute();
while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
$count += $row['Records'];
if($count > $limit){
$current_page ++;
$count = 0;
if($current_page == $page){
$start_id = $row['ID'];
break;
}
}
}

Updated:
A "custom" Pagination is what you're looking for. So if you're planning to hop from page to page, you can't use hardcoded $page and $current_page values. Those values should be generated dynamically once you're on a particular page. In fact, you should have the ID column value in the query part of the URL so that the pagination links could satisfy your business logic.
Assuming the fact that your ID column value starts from 1, your code should be like this:
$id = isset($_GET['id']) && is_numeric($_GET['id']) ? $_GET['id'] : 1;
$limit = 40;
// Display query results based on particular ID number
$query = 'SELECT ID, Records FROM table1 WHERE ID >= ' . $id;
$stmt = $conn->prepare($query);
$stmt->execute();
$rowCount = $stmt->rowCount();
if($rowCount){
$total = 0;
while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
if($total <= $limit){
$total += $row['Records'];
// Display $row details here
}else{
// Get the next $id value
$id = $row['ID'];
break;
}
}
}
// Display relevant ID links
$query = 'SELECT * FROM table1';
$stmt = $conn->prepare($query);
$stmt->execute();
$idArr = array();
$total = 0;
while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
if($total == 0){
$idArr[] = $row['ID'];
}
if($total <= $limit){
$total += $row['Records'];
}else{
$total = 0;
}
}
foreach($idArr as $idValue){
if($idValue == $id){
echo '<a>'.$idValue.'</a> ';
}else{
echo ''.$idValue.' ';
}
}
Sidenote: If the ID column value of your table doesn't start from 1, then use a separate query to get the first ID value and update the following statement,
$id = isset($_GET['id']) && is_numeric($_GET['id']) ? $_GET['id'] : <FIRST_ID_VALUE>;

If you only need to create the "Next" button, you could try this way:
select t1.ID, t1.Records
from table1 t1
left join table1 t2
on t2.ID < t1.ID
and t2.ID > :last_selected_id_1
where t1.ID > :last_selected_id_2
group by t1.ID, t1.Records
having coalesce(sum(t2.Records), 0) < :limit
order by t1.ID
:last_selected_id_x is the last ID from the current page or 0 for the first page.
http://rextester.com/FJRUV28068
You can use MySQL session variables to create page links:
select page, min(ID) as min_id, max(ID) as max_id
from (
select ID
, #page := case when #sum = 0 then #page + 1 else #page end as page
, #sum := case when (#sum + Records) >= :limit
then 0
else #sum + Records
end as sum
from table1
cross join (select #sum := 0, #page := 0) initvars
order by ID
) sub
group by page
The result would look like:
page | min_id | max_id
1 | 1 | 2
2 | 3 | 3
3 | 4 | 4
http://rextester.com/LQVJWP18655
Note that it is officially (as of documentation) not recomended to use the session variables like that (read and write in one statement). Feature versions may break your code.

Here's the result I ended up with, I ended up basing it off Rajdeeps answer but tweaked so that it'll allow sorting.
$query = 'SELECT ID, Records FROM table1 ORDER BY something';
$stmt = $conn->prepare($query);
$stmt->execute();
$id_array = array(); // For storing each ID
$current_ids = array(); // For storing the current ID pages
$initial_ids = array(); // For storing the first page, in case the page is too high
$count = 0;
$current_page = 1;
while($row = $stmt->fetch(PDO::FETCH_ASSOC)){
$already_reset = false;
// Move the session to the next page if it's more over the limit than under
// For example, if a limit of 10 has 6 records already, and the next ID is 9 records, then start a new page
if($id_array and $count + $row['Records'] / 2 > $max_links){
array_push($id_array, $last_id);
$current_page ++;
$count = 0;
$already_reset = true; // Set a marker so that if this ID is larger than the limit then don't start a 2nd new page
}
// Backup the first results in case page is too high and needs to be reset to 1
if($current_page == 1){
array_push($initial_ids, $row['ID']);
}
$count += $row['Records'];
// The values that appear here are the results for the selected page
// They may not be in sequence so store the individual IDs
if($_GET['page'] == $current_page){
array_push($current_ids, $row['ID']);
}
// Start a new page if over the limit
if($count > $max_links and !$already_reset){
$current_page ++;
$count = 0;
array_push($id_array, $row['ID']);
}
$last_id = $row['ID'];
}
array_push($id_array, $last_id);
$total_pages = count($id_array);
// If page is invalid revert to default
if(!$current_ids){
$current_ids = $initial_ids;
$_GET['page'] = 1;
}
$_GET['page'] = max(1, min($total_pages, intval($_GET['page'])));
$current_ids is an array of the IDs for the current page, and $total_pages is pretty self explanatory.

Related

PhP/MySQL: how to dynamically change my (large and always changing) database

Scenario
I have a MySQL database with 10.000 rows. Setup of the database:
ID UniqueKey Name Url Score ItemValue
1 5Zvr3 Google google.com 13 X
2 46cfG Radio radio.com -20 X
3 2fg64 Yahoo yahoo.com 5 X
.... etc etc etc
As you can see, each item has a score. The score is constantly changing. Google may have a score of 13 now, but tomorrow it may be 80, or -50.
What I want:
I want to create a system that creates a hierarchy in my current database, based on the score of the items. Right now I'm thinking about percentile ranks, meaning that the highest scoring items will be close to 100%, and the lowest scoring items will be close to 0%. For this I created some code that will try to achieve what is shown here: http://www.psychstat.missouristate.edu/introbook/sbk14m.htm
This is my code:
$sql = "SELECT * FROM database order by Score";
$result = $conn->query($sql);
$count = 0;
while ($row = $result->fetch_assoc()) {
$woow = $row['Score'];
$sql = "SELECT * FROM database WHERE Score = $woow";
$resultnew = $conn->query($sql);
$somanythesame = $resultnew->num_rows;
$itemPercentile = ( ($count/$result->num_rows + 0.5*$somanythesame/$result->num_rows) * 100 );
$rowID = $row['ID'];
$sql2 = "UPDATE database SET itemValue = $itemPercentile WHERE ID = $rowID";
$conn->query($sql2);
$count++;
}
This works, but for one problem it does not: There are many items in my database, many with the same score. To illustrate my problem, here is a very simple 10-row database with only the Scores:
Scores
-10
0
0
0
10
20
20
30
40
50
The problem with my code is that it doesn't give the same percentile for the items with the same Score, because it takes in account all previous rows for the calculation, including the ones with the same Score.
So, for the 2nd, 3rd and 4th item with a Score of 0, it should be like this: (1/10 + 0.5*1/10) * 100. Problem is, that for the 3rd item it will do (2/10 + 0.5*1/10) * 100 and the 4th item it will do (3/10 + 0.5*1/10) * 100.
Then, for the 5th item with a score of 10, it should do (4/10 + 0.5*1/10) * 100. This is going well; only not for the items with te same score.
I'm not sure if I explained this well, I find it hard to put my problem in the right words. If you have any questions, let me know! Thank you for your time :)
You need to maintain an "identical count" ($icount) variable that tracks the number of items with an identical score and a "current score" ($score) that tracks the current score.
$icount = 0;
$score = null;
Increment $icount instead of $count when $woow == $score (identical value check). Otherwise, add it to your $count and increment, and then reset the $icount value to 0.
if ($woow == $score) {
$icount++;
} else {
$count += $icount + 1;
$icount = 0;
}
Finally, set your $score value to the latest $woow for testing in the next iteration of the loop:
$score = $woow;
This will allow items with the same Score to have the same $count value, while incrementing an additional $icount times when a new $score is found.
Your final code will look like this:
$sql = "SELECT * FROM database order by Score";
$result = $conn->query($sql);
$count = 0;
$icount = 0;
$score = null;
while ($row = $result->fetch_assoc()) {
$woow = $row['Score'];
$sql = "SELECT * FROM database WHERE Score = $woow";
$resultnew = $conn->query($sql);
$somanythesame = $resultnew->num_rows;
$itemPercentile = ( ($count/$result->num_rows + 0.5*$somanythesame/$result->num_rows) * 100 );
$rowID = $row['ID'];
$sql2 = "UPDATE database SET itemValue = $itemPercentile WHERE ID = $rowID";
$conn->query($sql2);
if ($woow == $score) {
$icount++;
} else {
$count += $icount + 1;
$icount = 0;
}
$score = $woow;
}
You can change $sql query:
$sql = "SELECT *,count(*) FROM database group by Score order by Score";
In this case, you fetch score with counts and no more select needed in the while loop.
Even you can select Percentile in MySQL query:
Select t2.* , #fb as N , ((t2.fb1 + 0.5 * t2.fw)/#fb*100) as percentile from (
Select t1.* , (#fb := #fb + t1.fw) as fb1 from (
Select score,count(*) as fw From tablename group by score order by score ASC
) as t1
) as t2
I think this query returns most of columns which you may needs to check results.

Mysql count column issue

I can't figure-it out how to count all these dancers columns and echo with total of all dancers
id | dancer1 | dancer2 | dancer3 | dancer4, and so on..
---------------------------------------
1 alex michael dalm name
2 clare rose test
I have this for the start but is not working:
$counter = mysql_query("SELECT COUNT(*) AS id FROM table");
$num = mysql_fetch_array($counter);
$dancers = $num["id"];
echo "Total dancers: $dancers";
Any help is appreciated. Thanks!
Try this:
$counter = mysql_query("SELECT * FROM table");
$dancers = 0;
while($rows = mysql_fetch_array($counter)){
for($i = 1; $i <= 24; $i++){
$dan_id = 'dancer'.$i;
if($rows[$dan_id] != "" || $rows[$dan_id] != null )
$dancers++;
}
}
echo "Total dancers:". $dancers;
Note: Never design your database table like this.
I would actually save your dancers in a different (easier) way... For examle:
ID NAME SURNAME PHONE ....
1 Anna Brickstone 0975 ...
2 Jef Damen 0754 ...
That way you could use the following code to count tables:
$dancersCount="0";
$sql = "SELECT * FROM dancers";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$dancersCount++;
}
} else {
echo "No dancers..";
}
echo"$dancersCount";
The counter will count +1 each time it finds a row (dancer)
If you really want to do it that way...
Then I don't really think there's an easy way to fix this... You will probably need to check how many columns you have in your database but that's not something i can help you with...
You need to change your table structure:
id | dancerNumber | name
1 1 alex
2 1 clare
3 2 michael
4 2 rose
5 3 dalm
6 3 test
7 4 name
8 4 dana
SELECT COUNT(*) AS id FROM table will return 8 dancers. If this is what you were looking for?
if you want to keep your structure then you need to do the following sql query
SELECT dancer1,
dancer2,
dancer3,
(CASE WHEN (dancer1 <> "" AND dancer1 IS NOT NULL) THEN 1 ELSE 0 END +
CASE WHEN (dancer2 <> "" AND dancer2 IS NOT NULL) THEN 1 ELSE 0 END +
CASE WHEN (dancer3 <> "" AND dancer3 IS NOT NULL) THEN 1 ELSE 0 END) AS COUNTER
FROM table
This will count all the non empty and non null columns and add a counter at the end of the table. this counter will then contain the number of dancers with your structure.
Full answer with your php code
$query = 'SELECT (CASE WHEN (dancer1 <> "" AND dancer1 IS NOT NULL) THEN 1 ELSE 0 END +
CASE WHEN (dancer2 <> "" AND dancer2 IS NOT NULL) THEN 1 ELSE 0 END +
CASE WHEN (dancer3 <> "" AND dancer3 IS NOT NULL) THEN 1 ELSE 0 END) AS COUNTER
FROM table'
$counter = mysql_query($query);
$num = mysql_fetch_array($counter);
$dancers = $num["COUNTER"];
echo "Total dancers: $dancers";

how to get a specific id within 5 rows in a paging query in Mysql

I have a function in php I use it for paging it is like this :
$query = "SELECT id,
FROM table
ORDER BY id ASC
LIMIT $offset,5";
this work fine but what I want is to get the page that contain id number let say 10 and with it the other 4 rows, I want it to return something like this:
7,8,9,10,11,12 -> if I give it id number 10.
25,26,27,28,29 -> if I give it id number 26 and so on.
like it would return the 5 rows but I want to know how to set the offset that will get me
the page that have the 5 rows with the specified id included.
what should I do like adding where clause or something to get what I want!
Notice that the IDs in your table won't be consecutive if you delete some rows. The code below should work in such conditions:
$result = mysql_query('select count(*) from table where id < ' . $someId);
$offset = mysql_result($result, 0, 0);
$result = mysql_query('select * from table order by id limit ' . max($offset - 2, 0) . ',5');
while ($row = mysql_fetch_assoc($result)) {
print_r($row);
}
Try something like this
//but for pagination to work $page should be $page*$limit, so new rows will come to your page
$limit = 5;
$start = ($page*limit) -2; // for normal pagination
$start = $page -2; // for your case, if you want ids around the $page value - in this case for id = 10 you will get 8 9 10 11 12
if ($start < 0) $start = 0; // for first page not to try and get negative values
$query = "SELECT id,
FROM rowa
ORDER BY id ASC
LIMIT $start,$limit";

Displaying 11 consecutive rows from a query where middle row contains a variable

On my web page, there is a variable called $submission. I would like to display exactly 11 rows from the query below: the row where $submission equals $row["title"], 5 rows above it, and 5 rows below it. All ranked by points descending.
How can I do this?
$sqlStr = "SELECT title, points, submissionid
FROM submission
ORDER BY points DESC";
$result = mysql_query($sqlStr);
$arr = array();
$count=1;
echo "<table class=\"samplesrec\">";
while ($row = mysql_fetch_array($result)) {
echo '<tr >';
echo '<td>'.$count++.'.</td>';
echo '<td class="sitename1">'.$row["title"].'</td>';
echo '<td class="sitename2"><div class="pointlink2">'.number_format($row["points"]).'</div></td>';
echo '</tr>';
}
echo "</table>";
This is a little tricky if several rows share the same value for "points":
points | title | submissionid
------ + ----- + ------------
...
50 | 'foo' | ABCD01234 <-- If (50, 'foo') is the "midpoint" record,
50 | 'bar' | EF7654321 <-- does (50, 'bar') come before or after?
...
In this case, we need to impose an order. For convenience we're going to order by "points" descending and then "title" descending.
Assuming your "midpoint record" has a points value of '#points' and a title '#title', we'll say the records that come "before" the midpoint are those records whose (points, title) > (#points, #title). Similarly, those records "after" the midpoint have their (points, title) < (#points, #title).
Putting it together, we have:
-- First, initialize user variables with the points and
-- title of our midpoint (including midpoint)
--
SELECT #title := title,
#points := points
FROM submission
WHERE title = ? -- bind your $submission variable bere
LIMIT 1;
-- Now, select six records greater than or equal to our
-- midpoint.
--
SELECT title, points, submissionid
FROM ( SELECT title, points, submissionid
FROM submission
WHERE (points, title) >= (#points, #title)
ORDER BY points ASC, title ASC
LIMIT 6) gte
-- and UNION those records with five records less than
-- our midpoint
--
UNION
SELECT title, points, submissionid
FROM ( SELECT title, points, submissionid
FROM submission
WHERE (points, title) < (#points, #title)
ORDER BY points DESC, title DESC
LIMIT 5) lt
-- Finally sort the result set
--
ORDER BY points DESC, title DESC
You need to use a UNION
(
SELECT title, points, submissionid
FROM submission
WHERE points < (SELECT points FROM submission WHERE title = <row_title> LIMIT 1)
ORDER BY points DESC LIMIT 5
)UNION(
SELECT title, points, submissionid
FROM submission
WHERE points > (SELECT points FROM submission WHERE title = <row_title> LIMIT 1)
ORDER BY points ASC LIMIT 5
) ORDER BY points DESC
I haven't tested it, but this is the gist of it. You will get 10 records (or less), you'll have to figure out in PHP which records should go above your $submission and which below, since if you get 9 records you wont know if 4 are higher, or 5.
Alternatively you could just do it with 2 queries of course >_>
SELECT
title, points, submissionid
FROM
submission
WHERE
ROWID >= (SELECT ROWID FROM submission WHERE ..... ORDER BY points DESC) - 5
AND
ROWID <= (SELECT ROWID FROM submission WHERE ..... ORDER BY points DESC) + 5
ORDER BY
points DESC
I think that the following might do what you are asking. It will run the query and start reading records from the result set up until it finds the first occurrence of a record with a title that equals the $submission variable that you have (if I understand it correctly the primary key of your table is the submissionid and the title is a simple field - i.e. you do not have a unique key on it, so there may be more than one records with the same title).
After it finds that first record it will read 5 more records and stop. Then it will provide you with the part of the array of records that you wish to print and finally it will print it.
$sqlStr = "SELECT title, points, submissionid FROM submission ORDER BY points DESC";
$result = mysql_query($sqlStr);
$count = 1;
$found = false;
$continue = true;
$records = array();
$row = mysql_fetch_array($result);
while ($row !== false && $continue === true)
{
if($found == false && $row['title'] == $submission)
{
$found = true;
}
elseif($found == true && $count < 6)
{
$count++;
}
elseif($found == true && $count >= 6)
{
$continue = false;
}
$records[] = $row;
$row = mysql_fetch_array($result);
}
if($found === true)
{
if(array_count($records) > 11)
$records = array_splice($records, -11);
}
else
{
$records = array();
}
echo "<table class=\"samplesrec\">";
for($i = 1; $i <= count($records); $i++)
{
echo '<tr >';
echo '<td>'.$i.'.</td>';
echo '<td class="sitename1">'.$records[$i]["title"].'</td>';
echo '<td class="sitename2"><div class="pointlink2">'.number_format($records[$i]["points"]).'</div></td>';
echo '</tr>';
}
echo "</table>";
If there are no duplicate "title" values and if there are no duplicate "points"
SELECT title, points, submissionid
FROM submission
WHERE points <=
( SELECT points
FROM submission
WHERE points >=
( SELECT points
FROM submission
WHERE title = #row_title
)
ORDER BY points ASC
LIMIT 1 OFFSET 5
)
ORDER BY points DESC
LIMIT 11 OFFSET 0
I think its simplest to just do 2 queries against the database, but you could do it in PHP:
$entryFound = false;
$counter = 0;
$priorEntries = array();
while ($row = mysql_fetch_array($result)) {
$rowHtml = '<tr >';
$rowHtml .= '<td>'.$count++.'.</td>';
$rowHtml .= '<td class="sitename1">'.$row["title"].'</td>';
$rowHtml .= '<td class="sitename2"><div class="pointlink2">'.number_format($row["points"]).'</div></td>';
$rowHtml .= '</tr>';
if ($entryFound) {
if ($counter < 5) {
$counter++;
echo $rowHtml;
}
} else {
array_unshift($priorEntries, $rowHtml);
if (strcmp($row["title"], $submission) == 0) {
echo implode(array_reverse($priorEntries));
$entryFound = true;
}
array_splice($priorEntries, 5);
}
}
If the intention is to use single query and table is small and performance doesn't matters then use
SELECT b.title, b.points FROM (
SELECT #rank1 := #rank1 + 1 as slno, temp1.* FROM (
SELECT s1.title, s1.points, COUNT(s2.title) rank
FROM submission s1
JOIN submission s2 ON s1.points <= s2.points
GROUP BY s1.title, s1.points
ORDER BY rank, s1.title ASC
) as temp1
JOIN( SELECT #rank1 := 0 ) AS init
ORDER BY slno
) a
LEFT JOIN (
SELECT #rank2 := #rank2 + 1 as slno, temp1.* FROM (
SELECT s1.title, s1.points, COUNT(s2.title) rank
FROM submission s1
JOIN submission s2 ON s1.points <= s2.points
GROUP BY s1.title, s1.points
ORDER BY rank, s1.title ASC
) as temp1
JOIN( SELECT #rank2 := 0 ) AS init
ORDER BY slno
) b ON a.slno BETWEEN b.slno - 5 AND b.slno + 5
WHERE a.title = <row_title>;
This will return the row selected by and upto 5 above(if present) and upto 5 below(if present).
However, it is highly suggested to use temporary table to store ranks and the use it to display ranks.
More details about above query #
http://www.artfulsoftware.com/infotree/queries.php?&bw=1280#460
More details about optimized method #
http://onlamp.com/pub/a/mysql/2007/03/01/optimize-mysql-rank-data.html

Mysql select list of items (repeat until certain amount)

I have the following structure:
Table: products
id, name, sort_order
Let's say I only have 5 products, but I want 20.
How would I loop through to get 20 results?
Additionally, I'll need to start at a specific sort_order. So let's say I have
1,2,3,4,5
and I want to get 20 results and start at 3. It should end up as:
3,4,5,1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,etc.
Any ideas? I'm totally lost.. thank you!
$how_many = 20;
$counter = 0;
$rows = array();
$result = mysql_query("Your query");
while($row == mysql_fetch_row($result)) {
$rows[] = $row;
}
$expanded = array();
for($i = 0; $counter < $how_many; $counter++) {
$expanded[] = $rows[$i];
$i++;
if($i == count($rows)) {
$i = 0;
}
}
And now $expanded is filled with those 5 rows for 4 times.
Edit: and don't forget to adjust $i to your start element. For your example that would mean $i = 2 (third element).
create a table named abc and use this procedure to populate the table
CREATE PROCEDURE `populator`(i INT)
BEGIN
DECLARE existing int;
DECLARE counting int;
DECLARE limitation int default i;
SELECT COUNT(*) INTO existing FROM asd;
theLoop: LOOP
SELECT COUNT(*) INTO counting FROM abc;
IF (existing + counting) > i
THEN SET limitation = i - counting;
END IF;
IF counting >= i
THEN LEAVE theLoop;
ELSE
SET #sql = CONCAT("INSERT INTO abc SELECT id from asd LIMIT ", limitation);
PREPARE s1 FROM #sql;
EXECUTE s1;
DEALLOCATE PREPARE s1;
END IF;
END LOOP theLoop;
END
remember to rename the tables. i used asd as source table and abc as destination table. if you do the concatenation thing right you can even write a procedure that works with temporary tables.
You can do this in MySQL by cheating:
SELECT
t1.id
FROM
products t1
CROSS JOIN products t2
LIMIT
2, 20
Like the others, I'm not 100% sure what you want. You want to show the same products over again until 20 times?

Categories