Computed Column as Average from Other Data in Another Table - mysql

I am creating a simple movie website that allows users to browse through movie titles, and rate movies with a 5 point rating system. I am using XAMPP, and phpAdmin to store my database through the SQL language. I have the following table below that stores ratings:
CREATE TABLE `movie_ratings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`movie_id` int(11) DEFAULT NULL,
`rating` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
I have the following table below that stores movies:
CREATE TABLE `movies` (
`movie_title` varchar(255) NOT NULL,
`movie_id` int(100) NOT NULL,
`genre` text NOT NULL,
`release_date` text NOT NULL,
`price` int(100) NOT NULL,
`year` year(4) NOT NULL,
`description` text NOT NULL,
`movie_image` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
I want to add a column, average_rating to my movies table that basically takes all rating values from movie_ratings from a specific movie_id and averages them and then stores the value into this new average_rating column within the movies table.
I believe I need a computed column, but I only know how to do this via columns from the same table, here I am using another table. I read that a view could be possible too... Would prefer to store the average rating in movies though as a new column.
So I created a VIEW but it's only displaying one movie and averaging.
CREATE VIEW movie_ratings_view AS
SELECT b.movie_id, b.movie_title, b.movie_image, b.price, AVG(br.rating)
AS avgRating FROM movies b INNER JOIN movie_ratings br ON
b.movie_id = br.movie_id;

You need a group by clause
CREATE VIEW movie_ratings_view
AS
SELECT
b.movie_id
, b.movie_title
, b.movie_image
, b.price
, AVG(br.rating) AS avgRating
FROM movies b
INNER JOIN movie_ratings br ON b.movie_id = br.movie_id
GROUP BY
b.movie_id
, b.movie_title
, b.movie_image
, b.price
;
An alternative (if/when subqueries are allowed in the from clause of a view by MySQL):
CREATE VIEW movie_ratings_view
AS
SELECT
m.movie_id
, m.movie_title
, m.movie_image
, m.price
, COALESCE(mr.numRatings, 0) AS numRatings
, COALESCE(mr.avgRating, 0) AS avgRating
FROM movies m
LEFT JOIN (
SELECT
movie_id
, COUNT(rating) AS numRatings
, AVG(rating) AS avgRating
FROM movie_ratings
GROUP BY movie_id) mr ON m.movie_id = mr.movie_id
;

Probably the most efficient code for a view is a correlated subquery:
CREATE VIEW movie_ratings_view AS
SELECT m.movie_id, m.movie_title, m.movie_image, m.price
(SELECT AVG(mr.rating)
FROM movie_ratings mr
WHERE mr.movie_id = m.movie_id
) as avgRating
FROM movies m;
This can make use of an index on movie_ratings(movie_id, rating). And it doesn't have the outer GROUP BY which can be expensive.
It is slightly different because this will return movies with no ratings -- but that might even be desirable.

Related

Mysql find the book with the highest rating for each country. If there is a tie "print" the book with the highest number of ratings

I have the following tables:
CREATE TABLE `country` (
`name` VARCHAR(60) NOT NULL,
`code` VARCHAR(3) UNIQUE NOT NULL,
PRIMARY KEY (`code`)
);
CREATE TABLE `user` (
`userId` INT UNIQUE NOT NULL AUTO_INCREMENT,
`country` VARCHAR(3) NOT NULL,
`age` INT NOT NULL,
PRIMARY KEY (`userId`),
CONSTRAINT `fk_user_country` FOREIGN KEY (`country`) REFERENCES `country`(`code`)
);
CREATE TABLE `bookRating` (
`userId` INT NOT NULL,
`isbn` VARCHAR(13) NOT NULL,
`rate` INT NOT NULL,
`date` DATE NOT NULL,
CONSTRAINT `fk_bookRating_user` FOREIGN KEY (`userId`) REFERENCES `user`(`userId`),
CONSTRAINT `fk_bookRating_book` FOREIGN KEY (`isbn`) REFERENCES `book`(`isbn`)
);
CREATE TABLE `book` (
`isbn` varchar(13) UNIQUE NOT NULL,
`bookTitle` VARCHAR(280),
`bookAuthor` VARCHAR(150),
`yearPublication` int(4),
-- `yearPublication` must be an integer because we have value less that 1901 in dataset
`publisher` VARCHAR(135),
PRIMARY KEY (`isbn`),
CONSTRAINT `publication_yea_chk` check ((`yearPublication` > -1) && (`yearPublication` < 2101))
);
As I am saying on the title I want to find the book with the highest average rating, For each country
I have tried this query:
select T1.name, T1.BookTitle, Rate
from
(
select C.Code, AVG(BR.rate) MAXRating
from `bookRating` BR
inner join `book` B on BR.isbn = B.isbn
INNER JOIN `USER` U ON BR.UserID = U.USERId
INNER JOIN `COUNTRY` C ON U.country = C.Code
group by C.Code
) T
inner join
(
select C.Code, C.name, B.BookTitle, BR.ISBN, BR.rate
from `bookRating` BR
inner join `book` B on BR.isbn = B.isbn
INNER JOIN `USER` U ON BR.UserID = U.USERId
INNER JOIN `COUNTRY` C ON U.country = C.Code
) T1 ON T.Code = T1.Code AND T.MAXRATING = T1.RATE;
I am pretty sure this works. But I want to make it like If 2 or more books have the same average rating I want the one with the highest number of ratings.
I figured that I could use an If() statement, but how could I If(... , a condition)
How could I do it?
UPDATE
I have made the database and inserted some info in db fidle:
https://www.db-fiddle.com/f/s6wKhKhxXMX1W2x9VZn9da/1
You can join the tables, aggregate by country and book to get all average ratings and use window functions MAX() and FIRST_VALUE() on the results of the aggregation to get the book with the highest average for each country:
SELECT DISTINCT c.name,
FIRST_VALUE(b.bookTitle) OVER (
PARTITION BY c.Code
ORDER BY AVG(r.rate) DESC, COUNT(*) DESC
) bookTitle,
MAX(AVG(r.rate)) OVER () AverageRating
FROM country c
INNER JOIN users u ON u.country = c.Code
INNER JOIN bookRating r ON r.UserID = u.UserID
INNER JOIN book b ON b.isbn = r.isbn
GROUP BY c.Code, b.isbn;
See the demo.

Mysql Inner join - how to use value of first table column in join clause

In my "bookings" table, each booking has a number of persons and an "event_time" , which is one of three time slots which is bookable.
In my query I am trying to return how many free seats there are left for each restaurant and time slot (event_time number)
I select restaurants and do an INNER JOIN to include the bookings table, but I would need access to the "number_of_seats_max" column from the restaurants table inside the inner join, which does not seem possible.
Here is fiddle.
Tables:
CREATE TABLE `restaurants` (
`id` int(10) UNSIGNED NOT NULL,
`title` text COLLATE utf8mb4_unicode_ci,
`number_of_seats_max` int(11) DEFAULT NULL
);
CREATE TABLE `bookings` (
`id` int(10) UNSIGNED NOT NULL,
`event_date` timestamp NULL DEFAULT NULL,
`event_time` int(11) NOT NULL,
`number_of_persons` int(11) NOT NULL,
`restaurant_id` int(11) NOT NULL
);
The below query works, but in this case I have hard coded "80" instead of the max seats column ( r.number_of_seats_max ). Thats the column I need to use. If you put r.number_of_seats_max instead, you get the error "unknown column".
SELECT r.title, r.number_of_seats_max, innerquery.free_seats_left,
innerquery.num_persons_booked
FROM restaurants r
INNER JOIN(
select
restaurant_id,
SUM(number_of_persons) as num_persons_booked,
(80 - SUM(number_of_persons)) AS free_seats_left // <-- 80 is hard coded
from bookings
WHERE event_date = '2019-07-18'
group by event_time,restaurant_id
ORDER BY free_seats_left DESC
) as innerquery
ON innerquery.restaurant_id = r.id;
How can I solve it?
Do the subtraction in the main query, not the subquery.
SELECT r.title, innerquery.event_time, r.number_of_seats_max,
r.number_of_seats_max - innerquery.num_persons_booked AS free_seats_left,
innerquery.num_persons_booked
FROM restaurants r
INNER JOIN(
select
restaurant_id,
event_time,
SUM(number_of_persons) as num_persons_booked
from bookings
WHERE event_date = '2019-07-18'
group by event_time,restaurant_id
) as innerquery
ON innerquery.restaurant_id = r.id
ORDER BY free_seats_left DESC
I added event_time to the SELECT list of both the subquery and the main query, so you can show the available seats for each time slot.

MySQL left join doesnt give me what i expect

I'd like some help with an left join statement thats not doing what i, probably incorrectly, think it should do.
there are two tables:
cd:
CREATE TABLE `cd` (
`itemID` int(11) NOT NULL AUTO_INCREMENT,
`title` text NOT NULL,
`artist` text NOT NULL,
`genre` text NOT NULL,
`tracks` int(11) NOT NULL,
PRIMARY KEY (`itemID`)
)
loans
CREATE TABLE `loans` (
`itemID` int(11) NOT NULL,
`itemType` varchar(20) NOT NULL,
`userID` int(11) NOT NULL,
`dueDate` date NOT NULL,
PRIMARY KEY (`itemID`,`itemType`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
and i want to select all cd's thats not in loans using a left join and then an where dueDate is null
select
t.itemID,
t.artist as first,
t. title as second,
(select AVG(rating) from ac9039.ratings where itemType = 'cd' and itemId = t.itemID) as `rating avarage`,
(select COUNT(rating) from ac9039.ratings where itemType = 'cd' and itemId = t.itemID) as `number of ratings`
from
cd t left join loans l
on t.itemID = l.itemID
where l.itemType = 'cd' and l.dueDate is null;
this one however returns an empty table even though there are plenty rows in cd with itemIDs thats not in loans
now i was under the understanding that the left join should preserv the righthandside and fill the columns from the lefthandside with null values
but this does not seem to be the case, can anbyone enlighten me?
Your WHERE condition causes the error. The L.ItemType = 'cd' will always return false if the L.DueDate IS NULL is true. (All of your fields are NOT NULL, so the DueDate can only be NULL if there is no matching records, but in this case the ItemType field will be NULL too).
Another point is that your query is semantically incorrect. You are trying to get the record from the cd table where the loans table do not contains any rows with dueDates.
The second table acts as a condition, so it should go to the WHERE conditions.
Consider to use the EXISTS statement to achieve your goal:
SELECT
t.itemID,
t.artist as first,
t. title as second,
(select AVG(rating) from ac9039.ratings where itemType = 'cd' and itemId = t.itemID) as `rating avarage`,
(select COUNT(rating) from ac9039.ratings where itemType = 'cd' and itemId = t.itemID) as `number of ratings`
FROM
cd t
WHERE
NOT EXISTS (SELECT 1 FROM loans l WHERE t.itemID = l.itemID AND L.itemType = 'cd')
Based on your data model you have to add another condition to the subquery to filter out those records which are out-of-date now (dueDate is earlier than the current time)
This is the case, when you do not delete outdated loan records.
NOT EXISTS (SELECT 1 FROM loans l WHERE t.itemID = l.itemID AND AND L.itemType = 'cd' l.dueDate > NOW())

MYSQL query, joining 2 table's issue

I have this query -
SELECT interest_desc, categoryID, MAX(num_in_cat) AS num_in_cat
FROM
(
SELECT interest_desc, categoryID, COUNT(categoryID) AS num_in_cat
FROM interests
GROUP BY interest_desc, categoryID
) subsel
GROUP BY interest_desc, categoryID
I want to change it so that I can eventually display the category name from a separate table called categories. All I can display is the categoryID from interests with this sql
Both table structures are
#interests
CREATE TABLE `interests` (
`interestID` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`categoryID` int(11) NOT NULL,
`sessionID` int(11) NOT NULL,
`interest_desc` varchar(30) NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`interestID`)
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
categories table structure
# categories
CREATE TABLE `categories` (
`categoryID` int(11) NOT NULL AUTO_INCREMENT,
`category_desc` varchar(100) NOT NULL,
PRIMARY KEY (`categoryID`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
I know a join of some sort is needed but I have looked at examples and are struggling to get the exact syntax.
I have this in a php script - the echo statement is this
"{$result['interest_desc']} was the most popular in category {$result['categoryID']} with {$result['num_in_cat']} occurrences\n";
and its output is this -
"Adidas was the most popular in category 5 with 1 occurrences"
I want the output to be "Adidas was the most popular in Sport with 1 occurrences"
However my sql query does not feature category_desc.
This is more quick performance wise
SELECT subsel.interest_desc, subsel.categoryID, cat.category_desc, MAX(num_in_cat) AS num_in_cat
FROM
(
SELECT interest_desc, categoryID, COUNT(categoryID) AS num_in_cat
FROM interests
GROUP BY interest_desc, categoryID
) subsel
inner join categories as cat on subsel.categoryID = cat.categoryID
GROUP BY interest_desc, subsel.categoryID
Kindly check this , It will give you the required result.
SELECT subsel.interest_desc, cat.category_desc, MAX(num_in_cat) AS num_in_cat
FROM
(
SELECT interest_desc, categoryID, COUNT(categoryID) AS num_in_cat
FROM interests
GROUP BY interest_desc, categoryID
) subsel
inner join categories as cat on subsel.categoryID = cat.categoryID
GROUP BY interest_desc, subsel.categoryID
SELECT * FROM interests i LEFT JOIN categories c ON i.categoryID = c.categoryID
I haven't tested it. There might be syntax errors.
I do not know in what realistic scenarios the two similar queries as you posted make sense. I would say you can go with this straightaway:
SELECT i.interest_desc, c.category_desc, COUNT(i.categoryID) AS num_in_cat
FROM interests AS i
INNER JOIN categories AS c USING (categoryID)
GROUP BY i.interest_desc, i.categoryID

Best way to search two queries and eliminate rows without a relationship

I am working on a property website and have record sets for property and for unit, unit has a one-to-many relationship with property. What I'm trying to figure out is how to best create a search function which will output results based on criteria from both. So if I search for a property with the location Manchester and a unit with a freehold tenure I'd like to eliminate all properties which don't have a unit with the tenure of freehold.
A potential solution I've considered is to create a record set for properties which match the property criteria and then create a unit record set for units which match the unit criteria and then finally loop through the property record set in server-side code and eliminate any properties which aren't related to any of the units in the unit record set. Really not sure if this is the best way to do things though so would be keen to hear any suggestions?
Thanks
EDIT (Added table structure and MySQL):
--
-- Table structure for table `property`
--
CREATE TABLE IF NOT EXISTS `property` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` text NOT NULL,
`street` text NOT NULL,
`town` text NOT NULL,
`postcode` text NOT NULL,
`description` longtext NOT NULL,
`team_member` varchar(255) NOT NULL DEFAULT '',
`pdf` text NOT NULL,
`default_image_id` int(11) DEFAULT NULL,
`virtual_tour_link` text NOT NULL,
`date` date NOT NULL DEFAULT '0000-00-00',
`archive` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='' AUTO_INCREMENT=13 ;
--
-- Table structure for table `unit`
--
CREATE TABLE IF NOT EXISTS `unit` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` text NOT NULL,
`description` text NOT NULL,
`size_sq_ft` int(11) DEFAULT NULL,
`size_acres` float DEFAULT NULL,
`price` float DEFAULT NULL,
`rental_price` float DEFAULT NULL,
`on_application` tinyint(1) DEFAULT NULL,
UNIQUE KEY `id` (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Stores data for property units' AUTO_INCREMENT=5;
--
-- Table structure for table `property_to_unit`
--
CREATE TABLE IF NOT EXISTS `property_to_unit` (
`property_id` int(11) NOT NULL,
`unit_id` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
--
-- MySQL which produces list of properties
--
SELECT
P.id AS id,
P.name AS name,
P.street AS street,
P.town AS town,
P.postcode AS postcode,
P.description AS description,
P.team_member AS team_member,
P.pdf AS pdf,
P.virtual_tour_link AS virtual_tour_link,
P.date AS date,
P.archive AS archive,
PI.name as image,
P2.image_ids as image_ids,
L2.location_ids as location_ids,
U2.unit_ids as unit_ids
FROM property P
-- Get default image and join using property id
LEFT JOIN property_image PI ON PI.id = P.default_image_id
-- Create a list of image_ids from property_image and
-- property_to_property_image tables then join using property_id
LEFT JOIN (
SELECT
property_id,
GROUP_CONCAT(CAST(id AS CHAR)) as image_ids
FROM property_to_property_image PTPI
LEFT JOIN property_image PI ON PI.id = PTPI.property_image_id
GROUP BY property_id
) P2 ON P2.property_id = P.id
-- Create a list of locations from property_location table
-- and join using property_id
LEFT JOIN (
SELECT
property_id,
property_location_id,
GROUP_CONCAT(CAST(property_location.id AS CHAR)) AS location_ids
FROM property_to_property_location
INNER JOIN property_location ON property_location.id = property_to_property_location.property_location_id
GROUP BY property_id
) L2 ON L2.property_id = P.id
-- Create a list of units from unit table
-- and join using property_id
LEFT JOIN (
SELECT
property_id,
unit_id,
GROUP_CONCAT(CAST(unit_id AS CHAR)) AS unit_ids
FROM property_to_unit
INNER JOIN unit ON unit.id = property_to_unit.unit_id
GROUP BY property_id
) U2 ON U2.property_id = P.id
--
-- MySQL which produces list of units
--
SELECT
id,
name,
description,
size_sq_ft,
size_acres,
price,
rental_price,
on_application,
tenure_ids,
tenure_names,
type_ids,
type_names
FROM unit AS U
-- join tenure ids and names
LEFT JOIN (
SELECT
unit_id,
GROUP_CONCAT( CAST(UT.id AS CHAR) ) AS tenure_ids,
GROUP_CONCAT(UT.name) AS tenure_names
FROM unit_to_unit_tenure UTUT
INNER JOIN unit_tenure UT ON UT.id = UTUT.unit_tenure_id
GROUP BY unit_id
) UT ON UT.unit_id = U.id
-- join type ids and names
LEFT JOIN (
SELECT
unit_id,
GROUP_CONCAT( CAST(UTYPE.id AS CHAR) ) AS type_ids,
GROUP_CONCAT(UTYPE.name) AS type_names
FROM unit_to_unit_type UTUT
INNER JOIN unit_type UTYPE ON UTYPE.id = UTUT.unit_type_id
GROUP BY unit_id
) UTYPE ON UTYPE.unit_id = U.id
WHERE 0=0
I'm currently using a dynamically created WHERE statement appended to each MySQL query to filter the property and unit results.
You're making it a bit more complicated than it is. If I understand correctly, you can easily do this in a single query. This would search properties that have units with a particlar unit tenure id:
select *
from property p
where p.id in (
select pu.property_id
from property_to_unit pu
inner join unit u ON pu.unit_id = u.id
inner join unit_to_unit_tenure uut ON u.id = uut.unit_id
where uut.id = <cfqueryparam value="#uutid#">
)
Using two queries and then looping through to cross-check sounds like it could be dog slow.
Your situation requires a posted foreign key in the property table. Store the unit_id in the property table and use a join in your query such as:
select * from property p, unit u
where p.unit_id = u.id
and p.town = ....
EDIT: So I just noticed the rest of your SQL. If you require to keep the many-to-many relationship table for the unit -> property relationship then you will need to join unit and property off of that table.