I'm using PHP and MySQL.
I have a table named quantity. Inside there are records of product name, product price and product quantity. Besides these, there are a few others that helps me select the last records based on date and position, as well as a GROUP BY the field named price because there are different quantities with different prices for the same product. So, I currently select my product specific price and quantity like this:
SELECT `price`,`quantity` FROM (SELECT `price`,`quantity` FROM `quantity` WHERE `product_name` = 'DELL' ORDER BY `date` DESC, `position`) AS `Actions` GROUP BY `price`
This query is a workaround because I need to get data like this:
product_name | price | quantity
DELL | 100 | 30
DELL | 120 | 10
DELL | 130 | 2
Assuming that I have multiple records like these and I need to get the latest of them. Anyway, from this query I need to do the following: I need to select the records whose quantity summed with another product's quantity equals 35. So, by using my query I know that it should stop at line 2 because I can take the 30 products that came with the price of $100 and another 5 products from the line 2 that has price of 120. And then I would need to enter my updates. So, the new data would look like:
product_name | price | quantity
DELL | 100 | 0
DELL | 120 | 5
DELL | 130 | 2
How am I going to achieve this?
Option 1: Use program logic instead of a query:
There is nothing wrong with using the programming layer to do more advanced database interactions. SQL is not an answer to everything... (Also consider a stored procedure).
enough = 35
running_total = 0
START TRANSACTION
while running_total < enough:
select one record order by price limit 1 FOR UPDATE
add to running_total
UPDATE records...
COMMIT
Option 2: Use a query with a running total:
In this option, you obtain a running total using a derived query, and then filter that down to specific records in the outer query. If you intend on updating them, you should wrap this in a transaction with the right isolation level.
SET #running_total = 0;
SELECT
row_id,
product_name,
price,
quantity
FROM
(
SELECT
row_id,
product_name,
price,
quantity,
#running_total := #running_total + quantity AS running_total
FROM
sometable
WHERE
quantity > 0
ORDER BY
quantity
LIMIT
35 /* for performance reasons :) */
) as T1
WHERE
running_total < 35
I would tend to prefer option 1 because it is more "obvious", but perhaps this will give you some food for thought.
Related
I have a MariaDB table with an auto-incremented index, but also a "sortorder" field that controls the, well, sort order, when data is queried and displayed.
E.g.
id title sortorder
1 this 10
2 that 30
3 other 20
4 something 25
So far, so good. I'd like to create a function to automatically re-order these though - well, not re-order, but redo the values of the sortorder column per the existing order. The desired outcome from the above after running the function would be this:
id title sortorder
1 this 10
2 that 40
3 other 20
4 something 30
Is this something that can be done with an SQL statement in MariaDB (I have not found anything for that yet), or do I need to do it in my (php) application?
The logic for the new sort order values is based on the ordering by the sortorder column.
The reason for renumbering is that the sort order values are going to be manually maintained in the application, but it may be occasionally helpful to start with a clean slate. Users will be trained to "leave some room" in the values to allow for future edits.
On day one, "sortorder" will get (manually) populated with, say, 10, 20, 30, etc. Or possibly 100, 200, 300, etc. So that if they need to reorder things in the future, this will allow changing one item's sortorder value to say 25, to put it between the items with 20 and 30. Make sense?
But eventually, it's possible that the users could paint themselves into a corner, or at any rate make things confusing for themselves. It would be nice to build them a button that simply goes through the rows, and re-sets all the sortorder values, to preserve the existing row order but to make the values of sortorder be spaced evenly by intervals anew.
This would require some subqueries to be written inside. Steps of what I did are as follows:
Table name I used is tt. You need to change it according to your table name.
First is to get all rows in sorted order of sortorder column.
Second, declare a variable, say #serial_no and keep incrementing it by 1 on every selected row. This is an old school technique but I find it more readable.
Assign new sortorder values in this new parent select query. For now, I have just multiplied it's serial number(as in rank) by 10. You can adjust accordingly.
In your update query, inner join current copy of the table being updated with this select query and update the new sortorder column values correctly by matching them on id column.
Snippet:
update tt A
inner join (
select id, title, (#serial_no := #serial_no + 1) as serial_no,#serial_no * 10 as `sortorder`
from (
select *
from tt
order by sortorder asc
) temp_derived,(SELECT #serial_no := 0) as sn
) B
on A.id = B.id
set A.sortorder = B.sortorder
Update:
I just realised the control is completely shifted from user to DB. If you wish to update multiple rows with their new sortorder values, I wish to propose a workaround technique since I have never seen updating multiple rows with new values submitted from user in bulk(happy to learn if there exists one).
You need to map old values with new values, say in an associative array in PHP.
Start a DB transaction in MySQL.
Insert all new rows in bulk.
Delete all previous old rows in one go with IDs sent from PHP (in a prepared statement preferably with the previously mapped assoc array)
Commit the transaction.
Rollback ofcourse if something goes wrong.
Update: This solution works in MySQL 8.0, but not in MariaDB, because MariaDB's support for CTE doesn't support UPDATE statements. I'll leave this solution here for readers who use MySQL, but it doesn't work for MariaDB.
mysql> select * from NoOneEverNamesTheirTableInSqlQuestions;
+----+-----------+-----------+
| id | title | sortorder |
+----+-----------+-----------+
| 1 | this | 10 |
| 2 | that | 30 |
| 3 | other | 20 |
| 4 | something | 25 |
+----+-----------+-----------+
mysql> with cte as (
select id, row_number() over (order by sortorder) * 10 as new_sortorder
from NoOneEverNamesTheirTableInSqlQuestions
)
update NoOneEverNamesTheirTableInSqlQuestions join cte using (id)
set sortorder = new_sortorder;
Query OK, 2 rows affected (0.01 sec)
Rows matched: 4 Changed: 2 Warnings: 0
mysql> select * from NoOneEverNamesTheirTableInSqlQuestions;
+----+-----------+-----------+
| id | title | sortorder |
+----+-----------+-----------+
| 1 | this | 10 |
| 2 | that | 40 |
| 3 | other | 20 |
| 4 | something | 30 |
+----+-----------+-----------+
In MariaDB you can do:
update t
join (
select id, 10 * row_number() over (order by sortorder) as rn10
from t
) x on x.id = t.id
set t.sortorder = x.rn10;
Result:
id title sortorder
--- ---------- ---------
1 this 10
2 that 40
3 other 20
4 something 30
See running example at db<>fiddle.
I'm working on a track and field ranking database in MySQL/PHP5 whereby I'm struggling to find the best way to query results per unique athlete by highest value.
just
SELECT distinct name, event
FROM results
sample database
name | event | result
--------------------------
athlete 1 | 40 | 7.43
athlete 2 | 40 | 7.66
athlete 1 | 40 | 7.33
athlete 1 | 60 | 9.99
athlete 2 | 60 | 10.55
so let's say that in this case I'd like to rank the athletes on the 40m dash event by best performance I tried
SELECT distinct name, event
FROM results
WHERE event = 40
ORDER by result DESC
but the distinct only leaves the first performance (7.43) of the athlete which isn't the best (7.33). Is there an easy way other than creating a temp table first whereby the results are ordered first and performing a select on the temp table afterwards?
You might be interested in group by:
SELECT name, min(result) as result
FROM results
WHERE event = 40
GROUP BY name
This gives you the best result per athlete.
As suggested by spencer, you can also order the list by appending this:
ORDER BY min(result) ASC
The problem is that the columns used in the ORDER BY aren't specified in the DISTINCT. To do this, you need to use an aggregate function to sort on, and use a GROUP BY to make the DISTINCT work.
SELECT distinct name, event
FROM results
WHERE event = 40
GROUP BY name
ORDER by result DESC
let me explain the whole thing with an example:
| id | product | rating |
1 23 54
2 23 54
3 23 53
4 24 33
5 26 22
6 24 11
Lets say we have multiple ratings for each product and want to display the three top products. This would mean we can can user Inner-/left-/right- Join to get the products name from another table, order it by desc and set a limit of 3. But this would show us the same product three times with a rating of 54, 54 and 53.
Is it possible to avoid products with the same id in the result just with SQL?
So the dream output from one SQL query would be:
| id | product | rating |
1 23 54
4 24 33
5 26 22
In words: the top three unique products by rating (and of course only the row of the item with the highest rating -> id 1 or 2 for product 23 and not id 3).
Further more if there is only one product or two products with multiple ratings it should only transfer 1 or 2 results.
You can do this by taking the maximum rating for each product and choosing the top three:
select product, max(rating) as maxrating
from table t
group by product
order by maxrating desc
limit 3;
If you want the id for this rating, you can use the substring_index()/group_concat() trick:
select product, max(rating) as maxrating,
substring_index(group_concat(id order by rating desc), ',', 1) as id
from table t
group by product
order by maxrating desc
limit 3;
Alternatively, you can eschew the group by:
select t.*
from table t
where not exists (select 1
from table t2
where t2.product = t.product and
(t2.rating > t.rating or
t2.rating = t.rating and t2.id > t.id
)
)
order by t.rating desc
limit 3;
The complicated where clause is because multiple ratings can be the same.
EDIT:
The not exists version is getting the highest rating on the highest id for each row. The logic is simply saying: "Get me all rows from the products table where the product in the row has no other row with a higher rating/id combination". This is an awkward way for people to understand "Get the row with the maximum rating". But it turns out to be easier for the database to process. It is typically the most efficient method in MySQL and often the most efficient method in other databases as well, particularly with the right indexes defined.
Use a SELECT DISTINCT query. Check out details here: http://dev.mysql.com/doc/refman/5.6/en/select.html
I have a list of films that users can rank in order of which they like best using jQuery UI Sortable (all works well). The lower the order number the better the film (1) and the higher (26) the worse it is. The list of films could be endless but is fixed in the database (users can't add more), so the user can only select from x list of films.
Films do not have to be in the users list, if they haven't seen film 5 then it won't get included (this may be compounding the problem).
Currently this is stored in the table:
film_id | user_id | order
4 2 3
5 3 3
6 2 1
7 2 2
7 3 1
8 3 2
What I want, and don't know where to start is an overall 'Top 10' style list. i.e. film 7 is the most popular because it appears higher up peoples lists and is in more lists. Film 6 could be the most popular but it's only in one list?!
I am stuck on both the logic and the Mysql queries to do it!
I am thinking I might need to weight the order somehow? Or have a separate table with the score per film and just update it after every edit. The following query seems like the right idea if it was just based on the count of items in the table but not when I want to add position in to the equation.
SELECT ff.film_id, COUNT(ff.film_id) AS cnt, SUM(ff.order) AS rank FROM
`favourite_film` AS ff GROUP BY ff.film_id ORDER BY cnt DESC, rank ASC
I guess I need the count of all the films in the table and the sum of the order (but reversed?), my theory then goes flat!
Any help or links would be greatly appreciated. Thanks.
Depending your "business rules", I think you should find some sort of calculation to both take into account the position and the number of "votes".
Just a random guess, but why not sorting by COUNT(votes)/AVG(pos) ? For maintainability reason, you might want to factor out the ranking function:
CREATE FUNCTION ranking(average_pos REAL, vote_count INT)
RETURNS REAL
DETERMINISTIC
RETURN vote_count/average_pos;
The query is now simply:
SELECT film_id,
AVG(pos) as a, COUNT(*) as c, ranking(AVG(pos),COUNT(*)) AS rank
FROM vote GROUP BY film_id
ORDER BY ranking(AVG(pos), COUNT(*)) DESC;
Producing with your example:
+----------+------+----+----------------+
| FILM_ID | A | C | RANK |
+----------+------+----+----------------+
| 7 | 1.5 | 2 | 1.333333333333 |
| 6 | 1 | 1 | 1 |
| 8 | 2 | 1 | 0.5 |
| 5 | 3 | 1 | 0.333333333333 |
| 4 | 3 | 1 | 0.333333333333 |
+----------+------+----+----------------+
See http://sqlfiddle.com/#!2/3b1d9/1
you should have reverted the list before saving it. this way you could leave the unselected movies out of the rating.
a workaround might be:
Count the amount of lists SELECT COUNT(DISTINCT(user_id) save this as $AMOUNT_OF_LISTS
now count the points using
SELECT film_id, (SUM(order)+($AMOUNT_OF_LISTS-COUNT(DISTINCT(user_id)))*POINTS_FOR_NOT_IN_LIST) as points FROM table GROUP BY film_id
logic: sum up all points and add POINTS_FOR_NOT_IN_LIST points for every time not in a list (total amount of lists - amount of times movie is in the list)
insert a value POINTS_FOR_NOT_IN_LIST to your liking. (might be 26 or 27 or even lower)
you probably want to add ORDER BY points DESC LIMIT 10 to the query to get 10 highest points
SELECT MIN( `order` ) , COUNT( * ) AS cnt, `film_id`
FROM `favourite_film`
GROUP BY `film_id`
ORDER BY cnt DESC , `order`
I would do this, I would assign a higher value to the movies with the higher ranking. Then I would sum the values per movie and order by the total descending to get the overall ranking. This way you are giving weight to both the popularity and rankings of each movie.
So if you wanted to do it by the top 3 ranked movies per user you could do this:
SELECT film_id, SUM(3 -- The max number of ranked movies per user
- order -- the ranking
+ 1) total_score
FROM TABLE_NAME
GROUP BY film_id
ORDER BY total_score DESC;
Obviously you could remove the comments
This way the top rated movie would get the higher score, the next highest, the next highest score, etc. If you were counting the top 10 movies per user, just change the 3 to 10.
I am building a EC system for a client, this client is selling second hand products. My DB scheme is roughly like this
+-------------+ +-------------+ +-------------+
| Category | | Product | | Stock |
+-------------+ +-------------+ +-------------+
| category_id | | category_id | | stock_id |
| path | | product_id | | product_id |
+-------------+ +-------------+ +-------------+
a product is inside a category, a stock is an item of one product, the stock also contain specific information for that: stock serial, state (good condition, a bit broken, junk).
Now I need to make a page to show product in stock in each category for exemple something like this:
TV (big category)
LCD (small category)
SONY super LCD 22'' <- product
Stock 1
Stock 2
Stock 3
Sony super LCD 24''
.... etc ...
PLASMA (category)
Hitachi bla bla plasma
Stock 1
Stock 2
Stock 3
Since the user can browse first level of category I need to add some kind of paging system or the page would get too long and the server would be loaded for nothing.
My initial plan was to pull all the products in the category and children category, those product would have stock and do a limit on that query. When I got those product I would do n number of query to get the actual stock rows.
Since a good SQL query is better then long explanation
SELECT * FROM category c JOIN product p ON c.category_id = p.category_id
WHERE c.deleted = 0
AND p.deleted = 0
AND (c.category_id = 37 or c.path LIKE '/1/37/%')
AND (SELECT count(*) FROM stock s WHERE s.product_id = p.product_id AND s.sell= 1) != 0
LIMIT 10
After I would do 10 queries like this
SELECT * FROM stock s WHERE s.sell = 1 AND s.product_id = pulledid
so here come my question, is there a better way to do it ? Can you some flows in this approach(don't be indulgent I know that's really a naive approach but I cannot come with something else)
Would it be more intelligent to limit on stock ? knowing that the page would have some dynamic html (maybe ajax) to group some that have same price.
Is it better to show:
- one product and all stocks (in most case it won't be more then 5 items)
- page by stock which means the Product A could be on page 1 and page 2.
If someone have experience building this kind of system and can give me some feedback I would be grateful.
Why are you getting the product IDs and then using them individually to query the stock table? It seems like you could just join the stock table.
Using your design you will get more than 10 rows if there is more than one item in stock for a given product ID.
It is not clear how you are going to get the next 10 for the following page. You need a way to select starting at the next 10.
The normal way I would structure a problem like this this would be to create a query that lists all the results. You could do this by joining the stock table into your initial query. To make the paging work you devise a column or set of columns in the output that corresponds to the ordering that you want in your output, then add an order by clause for that. This set should be unique. In your case it could be something like product ID, stock ID, and a unique stock identifier like an autoincremented column or a timestamp. You can then limit your first page query to the page size. For the next page, select only those records where the unique values are greater than the last one on the last page. In your case, you would be using multiple columns so you might not be able to query this way. An alternative would be to select 2 pages worth for page 2 but throw away the first page and display the second page.
The idea is that for each page you are selecting the same overall set of records in the same order but displaying a different subset.