Selecting a random row that hasnt been selected much before? - php

Lets just put it at its simplest, a table with two fields: 'item_id' & 'times_seen'.
| item_id | times_seen |
----------+-------------
| 1001 | 48 |
| 1002 | 25 |
| 1003 | 1 |
| 1004 | 12 |
| 1005 | 96 |
| 1006 | 35 |
I'm trying to find a way to randomly select a row, but give preference to items that haven't been selected much before.
(obviously, a second query would be sent to increment the 'times-seen' field after it has been selected)
Although my current "project" is a php/mysql one, I'd like language agnostic solutions if possible. I'd much rather have a math based solution that could be adapted elsewhere. I'm not opposed to a php solution though. I'd just like to be able to understand how the code works rather than just copy and paste it.

How about a SQL solution:
select * from item order by times_seen + Rand()*100 limit 1;
How much you multiply random with (Its a value between 0 and 1) depends on how much randomness you want..
Edit: http://dev.mysql.com/doc/refman/5.0/en/mathematical-functions.html#function_rand

Fetch all the rows in the table
Determine the max value for times_seen
Assign each row a weight of max - times_seen
Pick from list based on weights
Step 4 is the tricky part, but you could do it all like this:
$max = 1;
$rows = array();
$result = mysql_query("SELECT * FROM table");
while ($row = mysql_fetch_array($result)){
$max = max($max, $row['times_seen']);
$rows[] = $row;
}
$pick_list = array();
foreach ($rows as $row){
$count = $max - $row['times_seen'];
for ($i=0; $i<$count; $i++) $pick_list[] = $row['item_id'];
}
shuffle($pick_list);
$item_id = array_pop($item_id);
To do it all in SQL:
SELECT *
FROM table
ORDER BY RAND( ) * ( MAX( times_seen ) - times_seen ) DESC
LIMIT 1
This selects a single row with weightings inversely proportional to the times_seen

Related

Fetch extra data for large number of IDs

I have a MySQL table containing information about objects. It looks like this:
+--------------+----+--------+-------+
| auto_incr_id | id | type | value |
+--------------+----+--------+-------+
| 1 | 1 | length | 105 |
| 2 | 1 | weight | 17 |
| 3 | 1 | price | 104 |
| 4 | 2 | length | 111 |
| 5 | 2 | weight | 18 |
| 6 | 2 | price | 87 |
+--------------+----+--------+-------+
I want to fetch the first x objects, sorted by length:
{
"id": 2,
"length": 111,
"weight": 18,
"price": 87
},
{
"id": 1,
"length": 105,
"weight": 17,
"price": 104
}
Here's what I do to achieve this:
First, I fetch the ids:
$type = "length";
$stmt = $mysqli->prepare("SELECT id FROM table WHERE type=? ORDER BY value DESC LIMIT ?");
$stmt->bind_param('si', $type, $limit);
$stmt->execute();
$result = $stmt->get_result();
$result = $result->fetch_all(MYSQLI_ASSOC);
Next, I get the other values for each object that was fetched:
$i = 0;
while ($i < count($result)) {
$stmt = $mysqli->prepare("SELECT type, value FROM table WHERE id=?");
$stmt->bind_param('i', $result[$i]['id']);
$stmt->execute();
$result_2 = $stmt->get_result();
$fetch = $result_2->fetch_all(MYSQLI_ASSOC);
$j = 0;
while ($j < count($fetch))
{
$result[$i][$fetch[$j]['type']] = $fetch[$j]['value'];
$j++;
}
}
This works great to fetch for example the top 5 ($limit = 5), but now I have a use case where I want to have this information for the top 10k or more. This take too much time, probably because it needs to go through the while loop and execute a statement for every found ID.
So I'm looking for a way to reduce the execution time getting this data. Any ideas?
Your issue appears to be a lack of understanding about table joins. There is a wrong and a right way to do this. The wrong way is very easy to understand, so I will show it so it makes sense...
select id, type, value from table where id in (select id from table where type='length');
That obviously gets the type/value combination from every id that has a value for type='length'. But, you aren't doing it ALL in the database. You still have to sort and group everything outside the database. Let's assume you only have type='length', 'weight' and 'price'. I can create three tables on the fly for that, creating attributes of length, weight, and price...
select l.id, length, weight, price from
(select id, value as length from table where type='length') l
left outer join
(select id, value as weight from table where type='weight') w
on l.id=w.id
left outer join
(select id, value as price from table where type='price') p
on l.id=p.id
order by length
Now, you will get each attribute in a row, one row per id. The id field is not guaranteed to be unique here. If you have the same ID with more than one length, it will show up more than once in the results. But, I hope you can see how I took the obvious subquery and turned it into a join to do all the work inside the database.
NOTE: I fixed the ambiguity error and added "left outer" to include rows where weight and price do not exist.
Well at least you can always use a subquery
SELECT id, type, value FROM table
WHERE id IN (SELECT id FROM table WHERE type=? ORDER BY value DESC LIMIT ?")
order by id

MySQL PHP select where "X,Y" is in X,Y,Z

How do I complete this code below? I am trying to select news from the database and the locations which can be 23,22,88,90 location codes. I know you can just do IN('23', '22', '88', '90') but the problem is, my locations are a string so it comes out like IN('23,22,88,90') I believe.
How do I expand on the string of locations and select all or any including the locations in the string? So in the database, newsLocations could be 22 23 22,90 23,80,90 90. If that makes sense? so if $locationstoGet has 22,88,90 only, it will get the newsLocation even if the result is just 88,90 without the 22.
$locationsToGet = '22,88';
$db->query("SELECT * FROM news WHERE newsLocation IN($locationstoGet)");
I hope I explained this alright.
I saw a response on another site here
So I will adapt the solution there to your scenario. Change locationsToGet into an array, and use the implode function to generate the right syntax for the IN Clause.
$locationsToGetArr = array('22','88');
$locations = "'".implode("','",$locationsToGetArr)."'"; //this should output '22','88'
$db->query("SELECT * FROM news WHERE newsLocation IN($locations)");
This solution is assuming your database structure is as such
+--------+--------------+
| news | newsLocation |
+--------+--------------+
| 1 | 88 |
| 1 | 22 |
| 2 | 22 |
| 2 | 88 |
+--------+--------------+
But if you are storing your data as the following instead
+--------+--------------+
| news | newsLocation |
+--------+--------------+
| 1 | 88,22 |
| 2 | 22,88 |
+--------+--------------+
You will not have much choice besides to select all from news table and have PHP filter the location. Not a very efficient method.
If your data is comma separated stored in databse column then you can use MYSQL FIND IN SET as per below example.
SELECT FIND_IN_SET('b','a,b,c,d');
OR you can try with regular expression in MYSQL but it will be too slow.
You can make an array of your locations and then populate your query string with the items from the array
$locations = '22,88';
$locationsToGetArray = explode(",", $locationToGet)
$query = "SELECT * FROM news WHERE newsLocation IN(";
for ($i = 0; $i < count($locationsToGetArray); $i++) {
$query .= $locationsToGetArray[$i];
if($i == (count($locationToGetArray) - 1)) $query.= ")";
else $query .= ",";
}
$db->query($query);

How can I get the sums of all the integers in a row of an SQL table with PHP?

I am currently making an attendance website. The data for attendance is stored like this...
+-----------------------------------------------+
| Name | 12/20/16 | 12/21/16 | 12/23/16 |
+-----------------------------------------------+
|Person1 | 1 | 0 | 1 |
|Person2 | 0 | 1 | 0 |
|Person3 | 1 | 1 | 1 |
+-----------------------------------------------+
If a person was there, then the date column for their row is marked as a "1". If they weren't there, then the date column for their row is marked as a "0".
I am trying to make a readout of how many days they were present.
How can I get a sum of all the values in the date columns for that specific person's row in PHP?
EDIT: I understand that it is a bad way of formatting the data. This is per the owners request. They have their mind set on it and won't listen to reason. They are thinking of SQL as an Excel file.
Since you can't refactor the database to work the only way to do this is
SELECT name, `12/20/16`+`12/21/16`+`12/23/16` as days_attended
FROM tablename
and yes every time you add a column you have to change your query.
You could make a dynamic query -- use the above as a template as to what that dynamic query would look like.
But you REALLY should refactor the database and make a view for your user to make them happy.
This is exactly why views exist.
Okay so with the help of some people in the comments, I have put together a working function to accomplish what I needed.
$ppl = mysqli_query($conn, "SELECT * FROM Attendance2016 WHERE name = '" . getSessionVal("Name") . "'");
$row = mysqli_fetch_array($ppl);
$loopMax = count($row);
$currentAtttendance = 0;
for($x = 0; $x < $loopMax; $x++){
if($row[$x] === "0"){
continue;
}else if($row[$x] === "1"){
$currentAtttendance = $currentAtttendance + 1;
}
}
return $currentAtttendance;

MySQL query with multiple random values but sum always within a range

I have a table of store items with their price.
I'm trying to write a mysql query that pulls a number of items (between 3 and 6) at RANDOM, with the TOTAL value of all items within $20 of a value chosen by the user.
Any idea's on how to accomplish this?
Thanks in advance!
EDIT*** so far here is what I have. The big issue is that sum(price) takes the sum of ALL items. The secondary issue is having the "LIMIT" be random, but I can eventually have php pick a random number between 3 and 6 prior to running the query.
SELECT item,price,sum(price)
FROM items
WHERE sum(price) BETWEEN ($value-10) AND ($value+10)
ORDER BY rand() LIMIT 6
I can't think of a good way to do this in an SQL query without joining your items table on itself multiple times, which would lead to combinatorial explosion as the number of items in the table grows.
I've worked up a solution in PHP that breaks your items into price groups. Consider the following table:
+----+--------------------+-------+
| id | item | price |
+----+--------------------+-------+
| 1 | apple | 10.5 |
| 2 | banana | 1.85 |
| 3 | carrot | 16.22 |
| 4 | donut | 13.33 |
| 5 | eclair | 18.85 |
| 6 | froyo | 26.99 |
| 7 | gingerbread | 12.15 |
| 8 | honecomb | 50.68 |
| 9 | ice-cream-sandwich | 2.44 |
| 10 | jellybean | 2.45 |
| 11 | kitkat | 2.46 |
| 12 | lollipop | 42.42 |
+----+--------------------+-------+
http://sqlfiddle.com/#!9/0d815
First, break the items into Price Groups based on the random number of items (between 3 and 6 in your case). The Price Group Increment will be determined by the variance in price ($20.00) divided by the number of items being selected. This ensures that you will not go outside of your variance. Here is an example for a group of 4 items:
PRICE_GROUP_INCREMENT = VARIANCE / NUMBER_ITEMS
PRICE_GROUP_INCREMENT = 20 / 4 = 5
SELECT Count(`id`) AS `item_count`,
Round(`price` / 5) `price_group`
FROM `items`
WHERE `price` <= 35
GROUP BY `price_group`
ORDER BY `price_group` ASC;
Result set:
+------------+-------------+
| item_count | price_group |
+------------+-------------+
| 4 | 0 |
| 2 | 2 |
| 2 | 3 |
| 1 | 4 |
+------------+-------------+
Next, we can search through the result set to find a combination of price groups that equal the target price group. The target price group is determined by your target price divided by the price group increment. Using our example above, let's try to find 4 items that add up to $35.00 with a $20.00 variance.
TARGET_PRICE_GROUP = round(TARGET_PRICE / PRICE_GROUP_INCREMENT)
TARGET_PRICE_GROUP = round($35.00 / $5.00) = 7
Searching through the result set, we have can get to a target price group of 7 with these groups of 4 items:
SELECT `items`.* FROM `items` WHERE ROUND(`price`/5) = 0 ORDER BY rand() ASC LIMIT 2;
SELECT `items`.* FROM `items` WHERE ROUND(`price`/5) = 4 ORDER BY rand() ASC LIMIT 1;
SELECT `items`.* FROM `items` WHERE ROUND(`price`/5) = 3 ORDER BY rand() ASC LIMIT 1;
or
SELECT `items`.* FROM `items` WHERE ROUND(`price`/5) = 0 ORDER BY rand() ASC LIMIT 1;
SELECT `items`.* FROM `items` WHERE ROUND(`price`/5) = 3 ORDER BY rand() ASC LIMIT 1;
SELECT `items`.* FROM `items` WHERE ROUND(`price`/5) = 2 ORDER BY rand() ASC LIMIT 2;
To speed up finding a random, suitable combination of queries, I wrote a recursive function that randomly weights each price group based on the number of items in it, then sorts it. This speeds things up because the function returns as soon as it finds the first solution. Here's the full PHP script:
<?php
function rand_weighted($weight, $total){
return (float)mt_rand()*(float)$weight/((float)mt_getrandmax()*(float)$total);
};
//you can change these
$targetPrice = 35.00;
$numProducts = rand(3,6);
$maxVariance = 20.00;
$priceGroupIncrement = $maxVariance / $numProducts;
$targetPriceGroupSum = (int)round($targetPrice/$priceGroupIncrement, 0);
$select = "SELECT COUNT(`id`) AS `item_count`, ROUND(`price`/{$priceGroupIncrement}) `price_group`";
$from = "FROM `items`";
$where = "WHERE `price` <= {$targetPrice}";
$groupBy = "GROUP BY `price_group`";
$orderBy = "ORDER BY `price_group` ASC"; //for readability of result set, not necessary
$sql = "{$select} {$from} {$where} {$groupBy} {$orderBy}";
echo "SQL for price groups:\n{$sql};\n\n";
//run your query here and get the result set
//here is a sample result set
//this assumes $targetPrice = 35.00, $numProducts=4, and $maxVariance=20.00
$numProducts = 4;
$priceGroupIncrement = 5;
$targetPriceGroupSum = 7;
$resultSet = array(
array('item_count'=>4, 'price_group'=>0),
array('item_count'=>2, 'price_group'=>2),
array('item_count'=>2, 'price_group'=>3),
array('item_count'=>1, 'price_group'=>4),
);
//end sample result set
$priceGroupItemCount = array();
$priceGroupWeight = array();
$total = 0;
//randomly weight price group based on how many items are in the group
foreach ($resultSet as $result){
$priceGroupItemCount[$result['price_group']] = $result['item_count'];
$total += $result['item_count'];
}
foreach ($resultSet as $result){
$priceGroupWeight[$result['price_group']] = rand_weighted($result['item_count'], $total);
}
//recursive anonymous function to find a match
$recurse = function($priceGroupWeight, $selection=array(), $priceGroupSum=0) use ($priceGroupItemCount, $total, $numProducts, $targetPriceGroupSum, &$recurse){
//sort by random weighted value
arsort($priceGroupWeight);
//iterate through each item in the $priceGroupWeight associative array
foreach ($priceGroupWeight as $priceGroup => $weight){
//copy variables so we can try a price group
$priceGroupWeightCopy = $priceGroupWeight;
$selectionCopy = $selection;
$priceGroupSumCopy = $priceGroupSum + $priceGroup;
//try to find a combination that adds up to the target price group
if (isset($selectionCopy[$priceGroup])){
$selectionCopy[$priceGroup]++;
} else {
$selectionCopy[$priceGroup] = 1;
}
$selectionCount = array_sum($selectionCopy);
if ($priceGroupSumCopy == $targetPriceGroupSum && $selectionCount == $numProducts) {
//we found a working solution!
return $selectionCopy;
} else if ($priceGroupSumCopy < $targetPriceGroupSum && $selectionCount < $numProducts) {
//remove the item from the price group
unset($priceGroupWeightCopy[$priceGroup]);
//if there is still remaining items in the group, add the adjusted weight back into the price group
$remainingInPriceGroup = $priceGroupItemCount[$priceGroup] - $selectionCopy[$priceGroup];
if ($remainingInPriceGroup > 0){
$remainingTotal = $total - count($selection);
$priceGroupWeightCopy[$priceGroup] = rand_weighted($remainingInPriceGroup, $remainingTotal);
}
//try to find the solution by recursing
$tryRecursion = $recurse($priceGroupWeightCopy, $selectionCopy, $priceGroupSumCopy);
if ($tryRecursion !== null){
return $tryRecursion;
}
}
}
return null;
};
$selection = $recurse($priceGroupWeight);
if ($selection===null){
echo "there are no possible solutions\n";
} else {
echo "SQL for items:\n";
foreach ($selection as $priceGroup => $numberFromPriceGroup){
$select = "SELECT `items`.*";
$from = "FROM `items`";
$where = "WHERE ROUND(`price`/{$priceGroupIncrement}) = {$priceGroup}";
$orderBy = "ORDER BY rand() ASC";
$limit = "LIMIT {$numberFromPriceGroup}";
$sql = "{$select} {$from} {$where} {$orderBy} {$limit}";
echo "$sql;\n";
}
}
This algorithmic approach should perform much better than a pure SQL Query-based solution, especially once your items table grows.
You'll need to use a HAVING clause -
SELECT item, price, sum(price) as total_price
FROM items
GROUP BY item
HAVING total_price BETWEEN ($value-10) AND ($value+10)
ORDER BY rand() LIMIT 6
Here is an example and here is another example
In this case the sum is always going to be the total of each item (using GROUP BY) which is great if you only have one of each item. If you have more than one of each the sum is going to total all of those items together in the GROUP BY. Based on your original description it is the second query that you're looking for where a customer will be able to see random products within a price range.
It would be best if you provided a table schema (perhaps using SQL Fiddle) and then showed us examples of what you want the results to be.

How to parse results from MySQL Table with PHP using row data?

Typically when I make calls into a mysql db, I reference a column for values I need. In this particular instance however, I need to retrieve a set of rows and parse with PHP accordingly. The example is this:
Table format
ID | Level
-----------------
1 | 1
2 | 2
3 | 2
4 | 3
5 | 4
6 | 4
I am ultimately trying to retrieve all possible levels and count the number of results by those levels. A simple GROUP BY, COUNT() will do the trick:
'Select Level, Count(*) as counter FROM table GROUP BY Levels ORDER BY Levels ASC'
Which will return:
table_new
Level | Count
--------------
1 | 1
2 | 2
3 | 1
4 | 2
The problem I face though is when retrieving these results with PHP, I am not quite sure how to set a variable, say 'level1' and set it to the value returned in the count column.
I assume the logic would follow:
while ($row = $stmt->fetch()) {
$count_level = $row['counter']
}
(but then I would need to create counts for each level type. Any suggestions?
$level = array();
while ($row = $stmt->fetch()) {
$level[$row['level']] = $row['counter']
}
then you have $level array and $level[1], $level[2] etc variables

Categories