SQL UPDATE in a loop ( mysql , PHP ) - 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.

Related

MySQL - Is it possible to merge select queries, so it doesn't have to be executed in while loop?

So i have a query which will get all information about matched rows where items are not visible.
Now inside while loop i had to use select count query to count how many not visible items every user has and it takes too much time if there are so many results in a loop.
Is there a way to query it without running select query inside while loop?
For example:
SELECT categories.name,
quality.name,
users.regdate,
categories.name,
items.name,
items.visible,
items.owner,
users.username
FROM items
LEFT JOIN users ON items.owner = users.id
LEFT JOIN categories ON items.category = categories.id
LEFT JOIN quality on items.category_quality = quality.id
WHERE items.visible = 'no'
Now in while loop to get number of items, i had to query
while($row = mysqli_fetch_assoc($sql))
{
$all_items_by_user = "SELECT COUNT(items.name)
FROM items
WHERE items.owner = ".$row['owner']."";
$visible_items_by_user = "SELECT COUNT(items.name)
FROM items
WHERE items.owner = ".$row['owner']." AND items.visible = 'yes'";
// rest of code here.
// when there are 200 items in loop executing time goes crazy
// (6+ secs, where with 1 item it's around 0.05)
}
Edit with example:
Some people asked to see what final code would be, I'll try to give the best i can.
Ignore left joins as this is not relavent, I just added those from the whole query.
Id | item name | owner | Visible
1 | Item 1 | 2 | no
2 | Item 2 | 2 | yes
3 | Item 3 | 2 | no
4 | Item 4 | 2 | no
5 | Item 5 | 2 | no
6 | Item 6 | 3 | no
7 | Item 7 | 3 | no
8 | Item 8 | 3 | no
9 | Item 9 | 4 | no
10 | Item 10 | 4 | no
In while loop for every user it should count how many rows user owns.
For example:
user 2 owns 5 rows total and 4 hidden, ($all_items_by_user will count everything, while $visible_items_by_user will count only 1)
user 3 owns 3 rows,
user 4 owns 2 rows.
How to count per user items because if i do it outside of while loop, it will count only for first user, not for every single user.
The final result should be
while($row = mysqli_fetch_assoc($sql))
{
$all_items_by_user = "SELECT COUNT(items.name)
FROM items
WHERE items.owner = ".$row['owner']."";
$visible_items_by_user = "SELECT COUNT(items.name)
FROM items
WHERE items.owner = ".$row['owner']." AND items.visible = 'yes'";
echo $row['owner'] . "(Visible items: number | All items: number";
// Output: 2 (Visible items: 1 | All items: 5)
}
This assumes that you only care about the counts per user and that you do not care bout any items specific information
SELECT items.owner,
COUNT(items.name) AS itemCount,
SUM( CASE WHEN items.visible = 'yes' AND item.name IS NOT NULL
THEN 1 ELSE 0 END) AS visibleCount
FROM items
GROUP BY items.owner
Then you just loop through each row, which will give you the user, total count for that user, and visible count for that user.
DEMO
You could avoid queries in loop using a join for aggreated result subqquery
SELECT categories.name,
quality.name,
users.regdate,
categories.name,
items.name,
items.visible,
items.owner,
users.username,
ifnull(t.count_name,0),
ifnull(t.count_visible_name,0)
FROM items
LEFT JOIN users ON items.owner = users.id
LEFT JOIN categories ON items.category = categories.id
LEFT JOIN quality on items.category_quality = quality.id
LEFT JOIN (
select items.owner
, COUNT(items.name) count_name
, sum( case when items.visible = 'yes' AND item.name is not null
then 1 else 0 end) count_visible_name
from items
group by items.owner
) t on t.owner = items.owner
be sure you have proper composite index
on table items column(owner, category, category_quality)

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.

Select multiple fields on different rows with ONE query

With one MYSQL query only, how can I check if a buyer and a seller have both bought from each other ?
Table : purchase
purchase_id | seller | buyer
12 | 2 | 5
29 | 5 | 2
If the query returns a result (which would be the case here), then a specific div would appear on the page.
SELECT (COUNT(*) >= 2) AS Result
FROM purchase a
INNER JOIN purchase b
ON a.seller = b.buyer AND
a.buyer = b.seller
WHERE (a.seller = 2 AND a.buyer = 5) OR
(a.seller = 5 AND a.buyer = 2)
SQLFiddle Demo
just replace 2 and 5 with your variables.

MySQL work out lowest of multiple columns

I have a table with the following:
+----+-------+-------+-----------+-----------+--------------+--------------+
| id | stock | price | usedStock | usedPrice | specialStock | specialPrice |
+----+-------+-------+-----------+-----------+--------------+--------------+
| #1 | 1 | 10.00 | 0 | 0 | 0 | 0 |
| #2 | 0 | 0 | 1 | 15.00 | 1 | 20.00 |
| #3 | 0 | 0 | 0 | 11.00 | 1 | 14.00 |
+----+-------+-------+-----------+-----------+--------------+--------------+
I would like to create a query that orders by lowest price first if the type of item is in stock.
So the query would result in the following order:
#1 - 10.00 (because 10.00 is the lowest result)
#3 - 14.00 (because although 11.00 is less, it's not in stock)
#2 - 15.00 (because 15.00 is lower than 20.00)
I've added the PHP tag to this question in case there's a quicker way of calculating using PHP after selecting the table.
I would order by the least price, in case item is present in stock:
ORDER BY
LEAST(CASE WHEN stock THEN price ELSE GREATEST(price, usedPrice, specialPrice) END,
CASE WHEN usedStock THEN usedPrice ELSE GREATEST(price, usedPrice, specialPrice) END,
CASE WHEN specialStock THEN specialPrice ELSE GREATEST(price, usedPrice, specialPrice) END)
If the condition is true (>=1), each CASE WHEN will return the price, otherwise it will return the greatest price. Fiddle here.
I would create a view first
CREATE VIEW stock_min_price AS
SELECT id, LEAST(IF(stock > 0, price, 1000000000),
IF (usedStock > 0, usedPrice, 10000000000),
IF (specialStock > 0, specialPrice, 10000000000)) AS actualPrice
FROM tablename;
then select and order by actual price of all items
SELECT id, IF(actualPrice = 1000000000, NULL, actualPrice) AS price
FROM stock_min_price
ORDER BY actualPrice
//edit: comparing to > 0 when stock can be greater than 1
Frankly, you should rethink your Data Modeling for exactly this kind of operation. There is no direct connection between the three types of stock, so you really should normalize them into another Table. For this i am going to assume your old table is named oldTable
CREATE TABLE stock(item_id integer, amount integer, price double, type enum('normal', 'used', 'special'));
INSERT INTO stock SELECT o.id, o.stock, o.price,'normal' FROM _oldTable as o
WHERE o.stock >0 OR o.amount >0 ;
INSERT INTO stock SELECT o.id, o.usedStock, o.usedPrice, 'used' FROM _oldTable as o
WHERE o.stock >0 OR o.amount >0 ;
INSERT INTO stock SELECT o.id, o.specialStock, o.specialPrice,'special' FROM _oldTable as o
WHERE o.stock >0 OR o.amount >0 ;
And there you have it. As you can see you can now basically get your information with a simple join on the stock table.
SELECT o.id, s.kind, min(s.price) FROM _oldTable_ AS o
JOIN stock as s ON s.item_id = o.id AND s.amount > 0
GROUP BY s.item_id
If you want to produce an output like in your original Table you use:
SELECT o.id, n.amount as amount, n.price as price, u.amount as usedAmount, u.price as usedPrice, s.amount as specialAmount, s.price as specialPrice
FROM _oldTable_ AS o
LEFT JOIN stock as n ON n.item_id = o.id AND n.type = 'normal'
LEFT JOIN stock as u ON u.item_id = o.id AND n.type = 'used'
LEFT JOIN stock as s ON s.item_id = o.id AND n.type = 'special';
It is now also more easy to a) get the total amount for each in stock (sum on the amount column with a GROUP BY Statement for the item_id) b) get the kind of stock that is used.
p.s.: you should probably set an index on the item_id column if you have lots of items. Also note that this solution potentially saves you disc space if not all items have all kinds of stock

How to make a order list of raw database data

I have a big database which is already filled and contains order data. I have a table named order which contains all the orders.
I need to make a list of all the ordered products which are the same. It's something like this:
My data in the database
id = 1 | orderid = 1 | product = A
id = 2 | orderid = 1 | product = A
id = 3 | orderid = 1 | product = B
id = 4 | orderid = 1 | product = B
id = 5 | orderid = 1 | product = B
id = 6 | orderid = 2 | product = A
In short:
order 1 has 2 product A and 3 product B
order 2 has 1 product A and 2 product B and 4 product C
order 3 has 3 product A and 3 product C
I would like to make a summary:
product A ordered 6 times
product B ordered 5 times
product C ordered 7 times
I can't find a way to iterate through the products which are ordered one by one. How can I count this or group them. I can't add an extra table because the database is not under my supervision.
You can do this with one query to your DB. In MySql:
SELECT product, COUNT(*) as count FROM test GROUP BY product
It returns two columns: first - product name, second - count.

Categories