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

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.

Related

SQL Query get row rank or position on the main select query

I'm trying to get the Rank of a specific row from my SELECT query.
Here's my table :
| PID | Age |
|------ |----- |
| 5295 | 27 |
| 4217 | 26 |
| 2935 | 25 |
| 8706 | 24 |
My query and code: I'm using SQL Server
$x = 0;
$query = odbc_exec($connect, "SELECT * FROM mytable ORDER BY Age DESC"); // Ordering by "Age" Descending
while($row = odbc_fetch_array($query)) {
$x++;
if($row['PID'] == 2935) { break; // Stop }
}
echo $x; // output "3"
This method works, but the question is can this be done with simple standalone SQL Query instead of looping through all results then incrementing $x variable to get the specified row rank. because this table contains thousands of entries which will be a little laggy while looping till it find the desired row then stop looping.
The logic of this ranking since I order with Age descending, so for example it will start with 27 with rank 1 along to 24 with rank 4.
How can I achieve this?
You can use row_number() in a subquery to assign a rank to each record, then filter out in the outer query on the row you are interested in:
select *
from (
select t.*, row_number() over(order by age desc) rn
from mytable t
) t
where pid = 2935
In MySQL, window functions are supported in version 8.0 only. In SQL Server, they have been available for a long time.

Select, Sum and compare with PHP and MySQL

I have two tables in my MySQL database:
1 named "stock"
id | product | qty
----------------------
1 | 1 | 15
----------------------
2 | 1 | 20
And the second one named "orders"
id | product | qty | stock_id
----------------------------
1 | 1 | 7 | 1
-----------------------------
2 | 1 | 8 | 1
So, before register a new "order", I need to verify which "stock.id" has free product to sell, with SUM all existent orders.qty and subtract it to stock.qty, for this example I'm going to insert a new order of 10 pieces of product with id '1' ($new_order = '10' (pieces):
for each stock.id { SUM(orders.qty) as total | then verify if 'total' >= $new_order | if(total >= $new_order){select that stock.id} if don't { continue looking for an stock.id with free product for sale } }
Hoping to make myself known, I need your help to structure MySql query from PHP for that function.
UPDATE
I've solved with this double query:
<?
$queryL = "SELECT id,unidades,producto FROM stock WHERE `producto` = '$producto'";
$resultL = $mysqli->query($queryL);
/* array asociativo */
while($rowL = mysqli_fetch_array($resultL, MYSQLI_ASSOC))
{
$id_lote = $rowL['id'];
$unidades = $rowL['unidades'];
$queryD = "SELECT SUM(cantidad) as total FROM `pedidos` WHERE `lote` = $id_lote";
$resultD = $mysqli->query($queryD);
/* array asociativo */
if($rowD = mysqli_fetch_array($resultD, MYSQLI_ASSOC)){
$ventas = $rowD['total'];
$disponible = $unidades - $ventas;
if($disponible >= $cantidad){ $lote = $id_lote; }
}
}
?>
Can someone help me simplifying this?

SQL UPDATE in a loop ( mysql , PHP )

I have two tables :
Order :
date | Product | Quantity
01/03| P1 | 2
01/03| P2 | 2
02/03| P1 | 3
02/03| P2 | 1
02/03| P3 | 5
Stock :
Purchase number | product | Quantity
01/03 | P1 | 4
01/03 | P2 | 1
02/03 | P2 | 2
02/03 | P3 | 5
02/03 | P1 | 1
02/03 | P1 | 1
The first table is my order (what I sold), with this table I want to update the second table (which is my stock) to know exactly what i have in stock.
But i want to do that following the date, I first update the Quantity.Stock 01/03 before 02/03 .
Right now I have a loop , the problem is I don't want to have any " -1 " and i don't want to update all the rows with the same data .
In this example I have 5 P1 in order so in stock the first line of P1 must be 0 , the next P1 line must be 0 too but the last line must stay at 1 .
Any ideas ? or direction ?
Bruno
I suppose date and purchase_number are some sort of datetime values.
Get orders, which you haven't used yet. I suppose you have some used/processed column or table, where you saved them as marked.
SELECT id, product, quantity
FROM orders
WHERE used = 0 AND quantity > 0
ORDER BY date ASC
Get all relevant purchases you can use in order you want to use them, so from the oldest.
SELECT id, product, quantity
FROM stock
WHERE quantity > 0
ORDER BY purchase_number ASC
You can then iteratively mark orders and update purchases. I assume you saved your results to $orders and $purchases respectively.
foreach ($orders as $order) {
$remaining = $order['quantity'];
foreach ($purchases as &$purchase)
if ($order['product'] !== $purchase['product']
|| $purchase['quantity'] === 0) {
continue;
}
$remaining = $purchase['quantity'] - $remaining;
$purchase['quantity'] = max($remaining, 0);
// update purchase, where
// :quantity is $purchase['quantity']
// :id is $purchase['id']
// UPDATE stock SET quantity = :quantity WHERE id = :id
if ($remaining >= 0) {
break;
}
$remaining = -$remaining;
}
unset($purchase);
if ($remaining > 0) {
// we have problem, we sold more, then we have in stock
}
// mark order as used, where :id is $order['id']
// UPDATE orders SET used = 1 WHERE id = :id
}
Example has time complexity O(M*N), where M is orders and N is purchases. You can change it to O(M+N), when you group data by products and do M SQL queries for purchases or some preprocessing. It has memory complexity O(M+N), which is a tradeoff for less queries.

select terms (categories) van mysql table and output them in a select box

this is my database structure for my terms table (categories):
id | name | parent_id | level | position
--------------------------------------------------------------
1 | term 1 | NULL | 0 | 1
2 | term 2 | 1 | 1 | 1
3 | term 3 | 1 | 1 | 2
4 | term 4 | NULL | 0 | 2
5 | term 5 | 4 | 1 | 1
so terms 2 and 3 are 1st level children of 1 and 5 is a first lst level child of 4
this is my query: (and it's not correct, this should be fixed)
SELECT
`id`,
`name`
FROM
`terms`
ORDER BY
`position` ASC,
`level` ASC
this is my php:
$terms = array();
// query part
if(!$this->_db->resultRows > 0)
return $terms;
while($d = $this->_db->fetch($r))
{
$terms[$d->id] = new Term($d->id);
}
return $terms;
current result:
term 1
term 2
term 5
term 4
term 3
but the result should be:
term 1
term 2
term 3
term 4
term 5
I don't know how to alter the query to get the correct result
the goal is to output this in a (multiple) select box
I know how to do it with nested lists, but you can't nest a select
Okay, so there are many ways to deal with this type of problem. All the ways that I will present do not require there to be a level column in the table. In my opinion that is redundant data since it can be deduced from the information found in the other columns.
If you know the maximum "level" will only be 1 (maximum depth is 2) you can use a query like this:
SELECT
t.`id`,
t.`name`,
IF(p.`position` IS NULL, t.`position`*{$row_count}, p.`position`*{$row_count}+t.`position`) AS display_order
FROM
`terms` t
LEFT JOIN `terms` p ON p.`id` = t.`parent_id`
ORDER BY
display_order
where $row_count is calculated:
SELECT COUNT(*) FROM `terms`
There are ways to modify this SQL to make it work with more levels (depth), but the query needs to get bigger with each maximum level/depth it will support.
If you are uncertain about the number of levels you will have, then you should probably just do something like this:
$dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$sth = $dbh->prepare('SELECT id,name FROM terms WHERE parent_id IS NULL ORDER BY position');
$sth->execute(array(NULL));
$terms = $sth->fetchAll();
$sql = 'SELECT id,name FROM terms WHERE parent_id = ? ORDER BY position';
$terms_to_check = $terms;
$terms = array();
while(count($terms_to_check))
{
$k = array_shift($terms_to_check);
$terms[] = $k;
$sth = $dbh->prepare($sql);
$sth->execute(array($k['id']));
$results = $sth->fetchAll();
$terms_to_check = array_merge($results, $terms_to_check);
}
(By the way I recommend using PDO.)

Selecting a random row that hasnt been selected much before?

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

Categories