MySQL generate random product list based on budget? - mysql

I'm tinkering with a function to enable users to specify a budget, and have them receive a list of randomly picked products that add up as best as possible to their specified budget.
As it stands now, I'm trying to do this with PHP but it tends to slow down when there is a huge product list.
Is this at all possible with just a MySQL query?
Edit:
My PHP "implementation":
$budget = 100;
$spent = 0;
$finished = false;
while(!$finished) {
$getRandomProduct = mysql_query('SELECT `id`, `price` FROM `products` WHERE `price` <= "'.($budget-$spent).'" ORDER BY RAND() LIMIT 1');
if(mysql_num_rows($getRandomProduct)) {
$randomProduct = mysql_fetch_assoc($getRandomProduct);
$productList[] = $randomProduct['id'];
$spent += $randomProduct['price'];
} else {
$finished = true;
}
}

The performance and the "randomness" of the result set can be further improved if you first define how many products to include.
$min = 10;
$max = 20;
$total = rand($min,$max);
The algorithm is based on the following:
In a collection of n positive numbers that sum up to S, at least one of them will be less than S divided by n (S/n)
Steps:
Select a product randomly where price < BUDGET /TOTAL. Get its price, lets say X.
Select a product randomly where price < (BUDGET - X)/(TOTAL - 1). Get its price, assume Y.
Select a product randomly where price < (BUDGET - X - Y)/(TOTAL - 2).
Repeat this and get (TOTAL - 1) products. For the last product, select one where price = remaining price. (or price <= remaining price and order by price desc and hopefully you could get close enough).
$budget = 100;
$spent = 0;
$finished = false;
for($i = 0; $i < $total - 1; $i++) {
$getRandomProduct = mysql_query('SELECT `id`, `price` FROM `products` WHERE `price` <= "'.(($budget-$spent)/($total - $i)).'" ORDER BY RAND() LIMIT 1');
if(mysql_num_rows($getRandomProduct)) {
$randomProduct = mysql_fetch_assoc($getRandomProduct);
$productList[] = $randomProduct['id'];
$spent += $randomProduct['price'];
} else {
break;
}
}
$getRandomProduct = mysql_query('SELECT `id`, `price` FROM `products` WHERE `price` <= "'.($budget-$spent).'" ORDER BY `price` DESC LIMIT 1');
$productList[] = $randomProduct['id'];
This improves:
The query performance, the condition is more strict
The result set is more random, as previous you could easily select the 1st product close to the budge and limit the budget of the others
References:
My answer using the same algorithm for another question

Related

MySQL SUM CASE returning incorrect calculated amount

Apologies for the confusing title
I have a table 'trainees' which lists booked places on a sailing trip
I then have a berth options table which lists among other things lists whether the berth is booked individually or as a cabin.
I am trying to get the total number of booked berths, however;
if the voption_pricedIndividually is 1 then the number of places added to the total should be 1.
If the voption_pricedIndividually is 0 then regardless of the number of 'trainees' booked the number of places added to the total should be the maximum number of people permitted in the cabin. voption_berthsAvailable. So in this case 3.
In the images above, there are:
2x trainees booked in berth 1 and
3x trainees booked in berth 5.
I need a total number of trainees booked, however if they are in a cabin, regardless of the number booked, it needs to return the maximum occupancy.
So in this case we have 3x individual bookings and then 3x more from the maximum occupancy as set in voption_berthsAvailable.
The expected outcome is 6.
After some great help by #Luuk I'm getting closer but the below is multiplying the sum by the number of rows (30 instead of 6)
SELECT voyage_name, voyage_startDate, voyage_id, session_name,
SUM(test.bookings) AS berthTotals
FROM voyages
LEFT JOIN voyagesessions ON voyagesessions.session_voyageID = voyages.voyage_id
LEFT JOIN trainees ON trainees.trainee_voyageID = voyages.voyage_id
LEFT JOIN vesseloptions ON vesseloptions.voption_id = trainees.trainee_berthID
LEFT JOIN
(SELECT SUM(bookings) as bookings, trainee_voyageID, trainee_sessionID FROM (
SELECT voption_pricedIndividually, trainee_voyageID, trainee_sessionID,
CASE WHEN voption_pricedIndividually = 0
THEN
MIN(CASE WHEN voption_pricedIndividually = 1
THEN 1
ELSE voption_berthsAvailable END)
ELSE
SUM(CASE WHEN voption_pricedIndividually = 1
THEN 1
ELSE voption_berthsAvailable END)
END as bookings
FROM trainees
LEFT JOIN vesseloptions ON vesseloptions.voption_id = trainees.trainee_berthID
LEFT JOIN voyagesessions ON voyagesessions.session_id = trainees.trainee_sessionID
GROUP BY voption_pricedIndividually, CASE WHEN voption_pricedIndividually <> 0 THEN voption_id END
) x ) as test ON test.trainee_voyageID = voyages.voyage_id
GROUP BY voyage_name, session_name, voyages.voyage_startDate, voyage_id
ORDER BY voyage_startDate, voyage_name
Getting close but now the math is not working.
The query is nearly there but it is returning the correct number but multiplying it by the number of records. Should be 6 not 30
Structures can be found in the DBFIDDLE https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=2d14e5048e2667551fc2d9c6d6010166
Ok, so I may have cheeted a little as this isn't in a complete query and I used some PHP but.....
**GetVoaygesBySession**
SELECT * FROM voyagesessions
LEFT JOIN voyages ON voyages.voyage_id = voyagesessions.session_voyageID
WHERE session_deleted != 1
**getTraineesByVoyageAndSession**
SELECT * FROM trainees
LEFT JOIN vesseloptions ON vesseloptions.voption_id = trainees.trainee_berthID
WHERE trainee_voyageID = :voyage
AND trainee_sessionID = :session
AND trainee_status != 6
Then I created multiple foreach loops to run through each result and build an array I can use totally the berth counts as it loops through.
I basically had it add to $i when it was a individual berth on each loop and just overwrite the same number $s with the total in the berth on each loop for the cabins.
$i = 0;
$c = 0;
$voyageD = array();
$sessionsandvoyages = $this->voyageModel->getVoyagesBySession();
foreach ($sessionsandvoyages as $trip) {
$trainees = $this->traineeModel->getTraineesByVoyageAndSession($trip->voyage_id, $trip->session_id);
foreach ($trainees as $trainee) {
if ($trainee->voption_pricedIndividually == 1) {
$i++;
} else {
$c = $trainee->voption_berthsAvailable;
}
}
$total = $i + $c;
$voyageD[] = array(
'voyage_name' => $trip->voyage_name,
'voyage_startDate' => $trip->voyage_startDate,
'voyage_id' => $trip->voyage_id,
'session_name' => $trip->session_name,
'berthBookings' => $total
);
$total = 0;
$i = 0;
$c = 0;
}
I realise this is probably not the most efficient way of doing it but at the moment it is the only way I can get it to work

Finding the id whose average sum is maximum compared to other id

I have a MySQL table named "Rating" which contains (id, rateable_id, rating, user_id) columns. rateable_id is the id of a record which is rated and rated score is stored in rating (star rating). All I want to do is to find particular rateable_id (grouped by) whose sum average is maximum among other rateable_id.
In the above sample table , i should get rateable_id = 26 because its rating is max ((3+3)/2) = 3 as compared to other rateable_id.
raw sql or eloquent any preferred.
Edit: Sorry to mention but i have done roughly in not standard way Anyway it returns the answer but i am looking using nested select answer. averageRating is willvincent package for counting avg sum of rating. $popular_post returns the id whose average rating sum maximum.
$posts = "App\Opportunity"::all(); //where(createdate < 1month)
$i=11;
$cr=0;
$pr=0;
$mi = 0; //max index
$maxR=0; //max rating value
for($i=0; $i< count($posts); $i++)
{
$cr = $posts[$i]->averageRating; //current rating
if($cr)<br>
{
if($cr > $maxR)
{
$maxR = $cr;
$mi = $i;
}
}
else
{
// echo "skip<br>";
}
}
$popular_post = ($posts[$mi]->id);
You are going to need 2 selects:
SELECT MAX(rating) rating, rateable_id FROM (
SELECT AVG(rating) rating, rateable_id FROM table GROUP BY reateble_id
) GROUP BY rateable_id ORDER BY rating LIMIT 1
I think this will help you little bit.
$ratings = Rating::all()->groupBy('rateable_id');
$topList = [];
foreach($ratings as $key => $value){
$topList[$key] = ($value->sum('rating')) / count($value);
}
arsort($topList); //sort the array by top values
$max_rateable_id = key($topList); //key of first item of the array
$max_rating = reset($topList); //value of first item of the array
$both = [$max_rateable_id => $max_rating]; //key and value of the first item of the array

optimize sql query inside foreach

I need help optimizing the below querys for a recurrent calendar i've built.
if user fail to accomplish all task where date
This is the query i use inside a forech which fetched all dates that the current activity is active.
This is my current setup, which works, but is very slow.
Other string explained:
$today=date("Y-m-d");
$parts = explode($sepparator, $datespan);
$dayForDate2 = date("l", mktime(0, 0, 0, $parts[1], $parts[2], $parts[0]));
$week2 = strtotime($datespan);
$week2 = date("W", $week2);
if($week2&1) { $weektype2 = "3"; } # Odd week 1, 3, 5 ...
else { $weektype2 = "2"; } # Even week 2, 4, 6 ...
Query1:
$query1 = "SELECT date_from, date_to, bok_id, kommentar
FROM bokningar
WHERE bokningar.typ='2'
and date_from<'".$today."'";
function that makes the foreach move ahead one day at the time...
function date_range($first, $last, $step = '+1 day', $output_format = 'Y-m-d' )
{
$dates = array();
$current = strtotime($first);
$last = strtotime($last);
while( $current <= $last ) {
$dates[] = date($output_format, $current);
$current = strtotime($step, $current);
}
return $dates;
}
foreach:
foreach (date_range($row['date_from'], $row['date_to'], "+1 day", "Y-m-d")
as $datespan)
if ($datespan < $today)
Query 2:
$query2 = "
SELECT bok_id, kommentar
FROM bokningar b
WHERE b.typ='2'
AND b.bok_id='".$row['bok_id']."'
AND b.weektype = '1'
AND b.".$dayForDate2." = '1'
AND NOT EXISTS
(SELECT t.tilldelad, t.bok_id
FROM tilldelade t
WHERE t.tilldelad = '".$datespan."'
AND t.bok_id='".$row['bok_id']."')
OR b.typ='2'
AND b.bok_id='".$row['bok_id']."'
AND b.weektype = '".$weektype2."'
AND b.".$dayForDate2." = '1'
AND NOT EXISTS
(SELECT t.tilldelad, t.bok_id
FROM tilldelade t
WHERE t.tilldelad = '".$datespan."'
AND t.bok_id='".$row['bok_id']."')";
b.weektype is either 1,2 or 3 (every week, every even week, every uneven week)
bokningar needs INDEX(typ, date_from)
Instead of computing $today, you can do
and date_from < CURDATE()
Are you running $query2 for each date? How many days is that? You may be able to build a table of dates, then JOIN it to bokningar to do all the SELECTs in a single SELECT.
When doing x AND y OR x AND z, first add parenthes to make it clear which comes first AND or OR: (x AND y) OR (x AND z). Then use a simple rule in Boolean arithmetic to transform it into a more efficient expression: x AND (y OR z) (where the parens are necessary).
The usual pattern for EXISTS is EXISTS ( SELECT 1 FROM ... ); there is no need to list columns.
If I am reading it correctly, the only difference is in testing b.weektype. So the WHERE can be simply
WHERE b.weektype IN ('".$weektype2."', '1')
AND ...
There is no need for OR, since it is effectively in IN().
tilldelade needs INDEX(tilldelad, bok_id), in either order. This should make the EXISTS(...) run faster.
Finally, bokningar needs INDEX(typ, bok_id, weektype) in any order.
That is a lot to change and test. See if you can get those things done. If it still does not run fast enough, start a new Question with the new code. Please include SHOW CREATE TABLE for both tables.

display 10 percent of all items mysql

I'm just a beginner at mysql so in school we got task to do. It goes like this. Display / print 10% of all books from books in falling order. So i tried to use limit, but it doesn't work. What can i do? My code i've tried to use:
select title, price from book
order by price desc
limit (select count(*)*0.1 from book);
thank you for your answers!
limit values have to be hard-coded constants. You can't use variables on them, e.g. select ... limit #somevar is a syntax error. You also can't use sub-queries or other dynamic values either. So you're stuck with either fetching the row count ahead of time and stuff it into the query string as a "hard-coded" value:
$ten_percent = get_from_database('select count(*) / 10 from book');
$sql = "SELECT .... LIMIT $ten_percent";
Or you simply fetch everything and then abort your loop once you've reached 10%:
$sql = "SELECT ....";
$result = mysql_query($sql) or die(mysql_error());
$total_rows = mysql_num_rows($result);
$fetched = 0;
while($row = mysql_fetch_assoc()) {
$fetched++;
if ($fetched >= ($total_rows / 10)) {
break; // abort the loop at 10%
}
... do stuff with $row
}

How to include zero when counting with a MySQL GROUP BY expression

I'd like to count the number events that occur on each day over the last month, but also include a count of zero when no events are found. Is that possible?
Here's what I'm starting from...
SELECT COUNT(*) AS count,
DATE(usage_time_local) AS d
FROM usages
WHERE user_id=136
AND DATE(usage_time_local) >= DATE('2011-04-24')
AND DATE(usage_time_local) <= DATE('2011-05-24')
GROUP BY DATE(usage_time_local);
UPDATE: Given the answer, I implemented a code solution by initializing a loop and then filling in the details.
$dailyCount = array();
for( $i=1; $i<=30; $i++ ) {
$day = date('Y-m-d',(time()-($i*24*60*60)));
$dailyCount[$day] = 0;
}
foreach( $statement as $row ) {
$dailyCount[$row['d']] = $row['count'];
}
You can't do this with standard SQL queries - you'd be trying to group on a date(s) that doesn't exist in the table.
Standard workaround is to make a temporary table that contains the date range in sequential order with no gaps, join that against your table and do the count/aggregate as usual.