How do I model product ratings in the database? - mysql

What is the best approach to storing product ratings in a database? I have in mind the following two (simplified, and assuming a MySQL db) scenarios:
Create two columns in the products table to store the number and the sum of all votes respectively. Use the columns to get an average at run time or using a query.
This approach means I only need to access one table, simplifying things.
Normalize the data by creating an additional table to store the ratings.
This isolates the ratings data into a separate table, leaving the products table to furnish data on available products. Although it would require a join or a separate query for ratings.
Which approach is best, normalised or denormalised?

A different table for ratings is highly recommended to keep things dynamic. Don't worry about hundreds (or thousands or tens of thousands) of entries, that's all peanuts for databases.
Suggestion:
table products
id
name
etc
table products_ratings
id
productId
rating
date (if needed)
ip (if needed, e.g. to prevent double rating)
etc
Retrieve all ratings for product 1234:
SELECT pr.rating
FROM products_ratings pr
INNER JOIN products p
ON pr.productId = p.id
AND p.id = 1234
Average rating for product 1234:
SELECT AVG(pr.rating) AS rating_average -- or ROUND(AVG(pr.rating))
FROM products_ratings pr
INNER JOIN products p
ON pr.productId = p.id
AND p.id = 1234;
And it's just as easy to get a list of products along with their average rating:
SELECT
p.id, p.name, p.etc,
AVG(pr.rating) AS rating_average
FROM products p
INNER JOIN products_ratings pr
ON pr.productId = p.id
WHERE p.id > 10 AND p.id < 20 -- or whatever
GROUP BY p.id, p.name, p.etc;

I know that my answer is not what you actually ask for, but you might want to have a chance of facilitating that new products with your system can almost never beat the old products. Say that you would get a product with 99% rating. It would be very difficult for new products to get high if you sort by products with the highest rating.

Do not store a record of each rating unless you absolutely need them specifically. An example of such a case could be a psychological experiment that tends to analyze specific properties of the raters themselves. So, yeah! You'd have to be just as crazy to store each rate in a separate record.
Now, coming to the solution, add two more columns to your product table: AverageRating and RateCount.
What would you store in them? Well, suppose you have an already-calculated average of the two numbers: 2 and 3, which is 2.5; having a new rate of 10, you'll multiply the average (2.5) by the rate count (2 in this case). Now, you have 5. Add this result to the new rate value (10) and divide the result by 3.
Let's cover all the above in a simple formula,
(AverageRating * RateCount + NewRateValue) / (RateCount + 1)
So (2.5 * 2 + 10) / (2 + 1) = 5.
Calculate the average on the server-side (not in your database) and store the average in the AverageRating column and the rate count in the RateCount column.
Simple, right?!
Edit
This solution doesn't require storing each rating separately as long as no review, edit or delete operations are involved. Yet, for such cases; let's assume that you've got a review with a rating of 3 that the owning user would like to modify to 4. Then, the formula to recalculate the average rating would be like this,
(AverageRating * RateCount - OldRateValue + NewRateValue) / RateCount
References
https://math.stackexchange.com/a/106314

Related

MySQL - When shouldn't I Join tables? Combinatorial Explosion of values

I am working on a database called classicmodels, which I found at: https://www.mysqltutorial.org/mysql-sample-database.aspx/
I realized that when I executed an Inner Join between 'payments' and 'orders' tables, a 'cartesian explosion' occurred. I understand that these two tables are not meant to be joined. However, I would like to know if it is possible to identify this just by looking at the relational schema or if I should check the tables one by one.
For instance, the customer number '141' appears 26 times in the 'orders table', which I found by using the following code:
SELECT
customerNumber,
COUNT(customerNumber)
FROM
orders
WHERE customerNumber=141
GROUP BY customerNumber;
And the same customer number (141) appears 13 times in the payments table:
SELECT
customerNumber,
COUNT(customerNumber)
FROM
payments
WHERE customerNumber=141
GROUP BY customerNumber;
Finally, I executed an Inner Join between 'payments' and 'orders' tables, and selected only the rows with customer number '141'. MySQL returned 338 rows, which is the result of 26*13. So, my query is multiplying the number of times this 'customer n°' appears in 'orders' table by the number of times it appears in 'payments'.
SELECT
o.customernumber,
py.amount
FROM
customers c
JOIN
orders o ON c.customerNumber=o.customerNumber
JOIN
payments py ON c.customerNumber=py.customerNumber
WHERE o.customernumber=141;
My questions is the following:
1 ) Is there a way to look at the relational schema and identify if a Join can be executed (without generating a combinatorial explosion)? Or should I check table by table to understand how the relationship between them is?
Important Note: I realized that there are two asterisks in the payments table's representation in the relational schema below. Maybe this means that this table has a composite primary key (customerNumber+checkNumber). The problem is that 'checkNumber' does not appear in any other table.
This is the database's relational schema provided by the 'MySQL Tutorial' website:
Thank you for your attention!
This is called "combinatorial explosion" and it happens when rows in one table each join to multiple rows in other tables.
(It's not "overestimation" or any sort of estimation. It's counting data items multiple times when it should only count them once.)
It's a notorious pitfall of summarizing data in one-to-many relationships. In your example each customer may have no orders, one order, or more than one. Independently, they may have no payments, one, or many.
The trick is this: Use subqueries so your toplevel query with GROUP BY avoids joining one-to-many relationships serially. In the query you showed us, that's happening.
You can this subquery to get a resultset with just one row per customer. (try it.)
SELECT customernumber,
SUM(amount) amount
FROM payments
GROUP BY customernumber
Likewise you can get the value of all orders for each customer with this
SELECT c.customernumber,
SUM(od.qytOrdered * od.priceEach) amount
FROM orders o
JOIN orderdetails od ON o.orderNumber = od.orderNumber
GROUP BY c.customernumber
This JOIN won't explode in your face because customer can have multiple orders, and each order can have multiple details. So it's a strict hierarchical rollup.
Now, we can use these subqueries in the main query.
SELECT c.customernumber, p.payments, o.orders
FROM customers c
LEFT JOIN (
SELECT c.customernumber,
SUM(od.qytOrdered * od.priceEach) orders
FROM orders o
JOIN orderdetails od ON o.orderNumber = od.orderNumber
GROUP BY c.customernumber
) o ON c.customernumber = o.customernumber
LEFT JOIN (
SELECT customernumber,
SUM() payment
FROM payments
GROUP BY customernumber
) p on c.customernumber = p.customernumber
Takehome tricks:
A subquery IS a table (a virtual table) that can be used whereever you might mention a table or a view.
The GROUP BY stuff in this query happens separately in two subqueries, so no combinatorial explosions.
All three participants in the toplevel JOIN have either one or zero rows per customernumber.
The LEFT JOINs are there so we can still see customers with (importantly for a business) no orders or no payments. With the ordinary inner JOIN, rows have to match both sides of the ON conditions or they're omitted from the resultset.
Pro tip Format your SQL queries fanatically carefully: They are really verbose. Adm. Grace Hopper would be proud. That means they get quite long and nested, putting the Structured in Structured Query Language. If you, or anybody, is going to reason about them in future, we must be able to grasp the structure easily.
Pro tip 2 The data engineer who designed this database did a really good job thinking it through and documenting it. Aspire to this level of quality. (Rarely reached in the real world.)
In this particular case, your behavior should depend on the accounting style being supported by the database, and this does not appear to be "open item" style accounting ie when an order is raised for 1000 there does not need to be a payment against it for 1000.. This is perhaps unusual in most consumer experience because you will be quite familiar with open item style ordering from Amazon - you buy a 500 dollar tv and a 500 dollar games console, the order is a thousand dollars and you pay for it, the payment going against the order. However, you're also familiar with "balance forward" accounting if you paid for that order using your credit card because you make similar purchases every day for a month and hen you get a statement from your bank saying you owe 31000 and you pay a lump of money, doesn't even have to be 31k. You aren't expected to make 31 payments of 1000 to your bank at the end of the month. Your bank allocate it to the oldest items on the account (if they're nice, or the newest items if they're not) and may eventually charge you interest on unpaid transactions
1 ) Is there a way to look at the relational schema and identify if a Join can be executed
Yes, you can tell looking at the schema- customer has many orders, customer makes many payments, but there is no relation between the order and payment tables at all so we can see there is no attempt to directly attach a payment to an order. You can see that customer is a parent table of payment and order, and therefore enjoys a relationship with each of them but they do not relate to each other. If you had Person, Car and Address tables, a person has many addresses during their life, and many cars but it doesn't mean there is a relationship between cars and addresses
In such a case it simply doesn't make sense to join payments to customers to orders because they do not relate that way. If you want to make such a join and not suffer a Cartesian explosion then you absolutely have to sum one side or the other (or both) to ensure that your joins are 1:1 and 1:M (or 1:1 and 1:1). You cannot arrange a join that is a pair of 1:M.
Going back to the car/person/address example to make any meaningful joins, you have to build more information into the question and arrange the join to create the answer. Perhaps the question is "what cars did they own while they lived at" - this flattens the Person:Address relationship to 1:1 but leaves Person:Car as 1:M so they might have owned many cars during their time in that house. "What was the newest car they owned while living at..." might be 1:1 on both sides if there is a clear winner for "newest" (though if they bought two cars manufactured at identical times...)
Which side you sum in your orders case will depend on what you want to know, but in this case I'd say you usually want to know "which orders haven't been paid for" and that's summing all payments and rolling summing all orders then looking at what point the rolling sum exceeds the sum of payments.. those are the unpaid orders
Take a look again at your database graph (the one that was present in the first iteration of your question). See the lines between tables have 3 angled legs on one end - that's the many end. You can start at any table in the graph and join to other tables by walking along the relationship. If you're going from the many end to the one end, and assuming you've picked out a single row in the start table (a single order) you can always walk to any other table in the many->one direction and not increase your row count. If you walk the other way you potentially increase your row count. If you split and walk two ways that both increase row count you get a Cartesian explosion. Of course, also you don't have to only join on relation lines, but that's out of scope for the question
ps: this is easier to see on the db diagram than the ERD in the question because the database purely concerns itself with the columns that are foreign keyed. The ERD is saying a customer has zero or one payments with a particular check number but the database will only be concerned with "the customer ID appears once in the customer table and multiple times in the payment table" because only part of the compound primary key of payment is keyed to the customer table. In other words, the ERD is concerned with business logic relations too, but the db diagram is purely how tables relate and they aren't necessarily aligned. For this reason the db diagrams are probably easier to read when walking round for join strategies
After seeing the answers of Caius Jard and O.Jones (please, check their replies), which kindly helped me to clarify this doubt, I decided to create a table to identify which customers paid for all orders they made and which ones did not. This creates a pertinent reason to join 'orders', 'orderdetails', 'payments' and 'customers' tables, because some orders may have been cancelled or still may be 'On Hold', as we can see in their corresponding 'status' in the 'orders' table. Also, this enables us to execute this join without generating a 'combinatorial explosion'.
I did this by using the CASE statement, which registers when py.amount and amount_in_orders match, don't match or when they are NULL (customers which did not make orders or payments):
SELECT
c.customerNumber,
py.amount,
amount_in_orders,
CASE
WHEN py.amount=amount_in_orders THEN 'Match'
WHEN py.amount IS NULL AND amount_in_orders IS NULL THEN 'NULL'
ELSE 'Don''t Match'
END AS Match
FROM
customers c
LEFT JOIN(
SELECT
o.customerNumber, SUM(od.quantityOrdered*od.priceEach) AS amount_in_orders
FROM
orders o
JOIN orderdetails od ON o.orderNumber=od.orderNumber
GROUP BY o.customerNumber
) o ON c.customerNumber=o.customerNumber
LEFT JOIN(
SELECT customernumber, SUM(amount) AS amount
FROM payments
GROUP BY customerNumber
) py ON c.customerNumber=py.customerNumber
ORDER BY py.amount DESC;
The query returned 122 rows. The images below are fractions of the generated output, so you can visualize what happened:
For instance, we can see that the customers identified by the numbers '141', '124', '119' and '496' did not pay for all the orders they made. Maybe some of them where cancelled or maybe they simply did not pay for them yet.
And this image shows some of the columns (not all of them) that are NULL:

Which relationship model to use for storing a value that depends on other values?

The task that I have got:
Implement Product Discount functionality, where the amount of Discount depends on different conditions, for example:
Amount of Discount depends on Product Category (Phone, TV, PC);
Amount of Discount depends on Product Manufacturer (Apple, Samsung, Sony);
Amount of Discount depends on Customer Type (Registered/Guest);
etc.
Assume, that we need to cover only first condition, where the Discount amount per Manufacturer - I think this is a pure example of 1 to 1 relationship, where for each Manufacturer defined their own discount.
From the DB schema point of view, we can easily implement this part in 2 ways:
One Table MANUFACTURER_DISCOUNT with 2 Columns: MANUFACTURER_NAME (type ENUM) and DISCOUNT_AMOUNT (type: LONG);
Or with 2 Tables MANUFACTURER (MANUFACTURER_NAME, ID) and DISCOUNT (DISCOUNT_AMOUNT, MANUFACTURER_ID);
But what I have to do in the case when I have more than 1 criteria (discount per manufacturer) only?
How can I properly build the structure of my tables?
Do I need just to extend the table that I described as option 1 with additional columns like PRODUCT_CATECORY, CUSTOMER_TYPE or possible more elegant and correct solution exists?
Each discount should be its own table.
Then the amount of discount query becomes something like:
SELECT IFNULL(cd.discount, 0) + IFNULL(md.discount, 0) + IFNULL(custd.discount, 0) as discount
FROM product p
LEFT JOIN category_discount cd
ON p.category = cd.category
LEFT JOIN manufacturer_discount md
ON p.manufacturer = md.manufacturer
LEFT JOIN customer_discount custd
ON p.customer_type = custd.customer_type
Note the JOINs are more important here than the exact calculation on the discount, just remember NULL is a not found value and need to be accounted for.
Keep tables for a single purpose. Putting multiple discount types could just get messy as you'd need multiple rows. Multiple tables with one to one relationships also end up being an unnecessary complication.
When in doubt about table structures, start to write out the queries you need on those tables. It usually becomes obvious which table structure results in simple queries at that point.

Looking for the earliest history entry of a product should I join history/product tables or store value in product table?

I have a MySQL database with a table for products and a table with the buying/selling history of these products. The buying and selling history of each product is basically tracked in this history table.
I am looking for the most efficient way of creating a list of these products with the earliest transaction data from the history table joined.
At the moment my SQL query selects the products with the earliest history entry like this:
SELECT p.*
, h.transdate
, h.sale_price
FROM products p
LEFT
JOIN
( SELECT MIN(transdate) transdate
, product_id
FROM history
GROUP
BY product_id
) hist_min
ON hist_min.product_id = p.id
LEFT
JOIN history h
ON h.product_id = hist_min.product_id
AND h.transdate = hist_min.transdate
Since this query is used very frequently and potentially with many products I am considering storing the first sale_price directly in the 'products' table. This way I wouldn't need the 2 additional JOINS at all. But this would mean I store redundant data.
For me the most important question is, which of these possibilities is the most efficient one.
I am not sure if I am allowed to ask this additionally, but if there is an even better way I would like to know about it.
EDIT: To clarify 'efficient', I am talking about tens of thousands of products with maybe 10 history records each, where I only pick pagewise 20 with a LIMIT statement. To save the original price with the product would be pulling the data straight with the record, while the scanning of dates in the history table for the earliest time and another scan to join the actual row of data would require certainly more resources, even if only for the second table involved. The use of a primary key ID oder an index over product_id and transdate would certainly speed up the second join though.
What you're describing is called 'normalization'. The level of normalization is not a black and white area so I don't think this site is the place to get your answer as it's primarily opinion based.
Check out these links to get started:
Database Normalization Explained in Simple English
Wikipedia (check out the 'See also' section, it describes level of normalization)

Get stats table from a many to many relationship

I have a pivot table for a Many to Many relationship between users and collected_guitars. As you can see a "collected_guitar" is an item that references some data in foreign tables (guitar_models, finish).
My users also have some foreign data in foreign tables (hand_types and genders)
I want to get a derived table that lists data if I look for a particular model_id in "collected_guitar_user"
Let's say "Fender Stratocaster" is model id = 200, where the make is Fender (id = 1 of makes table).
The same guitar could come in a variety of finish hence the use of another table collected_guitars.
One user could have this item in his collection
Now what I want to find by looking at model_id (in this case 200) in the pivot table "collected_guitar_user" is the number of Fender Stratocasters that are collected by users that share the same genders.sex and hand_types.type as the logged in user and to see what finish they divide in (some percent of finish A and B etc...).
So a user could see that is interested in what others are buying could see some statistics for the model.
What query can derive this kind of table??
You can do aggregate counts by using the GROUP BY syntax, and CROSS JOIN to compute a percentage of the total:
SELECT make.make, models.model_name as model, finish.finish,
COUNT(1) AS number_of_users,
(COUNT(1) / u.total * 100) AS percent_owned
FROM owned_guitar, owned_guitar_users, users, models, make, finish
CROSS JOIN (SELECT COUNT(1) AS total FROM users) u
WHERE users.id = owned_guitar_users.user_id
AND owned_guitar_user.owned_guitar_id = owned_guitar.id
AND owned_guitar.model_id = models.id
AND owned_guitar.make_id = make.id
AND owned_guitar.finish_id = finish.id
GROUP BY owned_guitar.id
Please note though, that in cases where a user owns more than one guitar, the percentages will no longer necessarily sum to unity (for example, Jack and John could both own all five guitars, so each of them owns "100%" of the guitars).
I'm also a little confused by your database design. Why do you have a finish_id and make_id associated directly in the owned_guitar table as well as in the models table?

MySQL Query - Lowest Values with Multiple Tables / Joins

i am requesting some help for a query to be used on a custom golf website.
what i need is to find the lowest score per player per course. my club has 3 nine hole loops, 27 holes in total, but i want to find the lowest per 9 holes (i.e. course as i am describing it).
i have the following database structure (note, i haven’t put in all rows, only those that are pertinent to the query i am stuggling with).
Golf DB ERP Diagram
a query to get the full set of data would be (note some field names are different - the diagram was trying to better descriptive…):
select * from round r, round_hole rh, player p, course_nine c, course_hole ch
where r.r_id = rh.rh_rid
and p.id = r.r_pid
and c.cn_nine = r.r_nine
and ch.ch_nine = c.cn_nine
and rh.rh_hid = ch.ch_no
a snapshot of the results are:
Full query ouput
however, i then need to filter it as above, into "per player, per course”
i am presuming this is some subquery, join, temp table or “in” type statement, but struggling, particularly as it spans multiple tables.
any help is appreciated
This can be accomplished using some simple aggregation. As long as you are able to properly join all of your tables, you can do this:
SELECT player, course, MIN(score) AS lowestScore
FROM myTables
GROUP BY player, course;