Ponderate average MYSQL - mysql

We have a little simulator of a tour-operator DB (MYSQL) and we are asked to get a Query that gives us the weighted avg of duration of the tours that we have.
https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
Using subquery I got to this point where I have the days that each tour lasts and the weight of each tour from the total of tours, but I am stuck and don't know how to get the weighted avg from here. I know I have to use another select from the result I already got but I would appreciate some help.
SQLfiddle down here:
http://sqlfiddle.com/#!9/53d80/2
Tables and data
CREATE TABLE STAGE
(
ID INT AUTO_INCREMENT NOT NULL,
TOUR INT NOT NULL,
TYPE INT NOT NULL,
CITY INT NOT NULL,
DAYS INT NOT NULL,
PRIMARY KEY (ID)
);
CREATE TABLE TOUR
(
ID INT AUTO_INCREMENT NOT NULL,
DESCRIPTION VARCHAR(255) CHARACTER SET UTF8 COLLATE UTF8_UNICODE_CI
NOT NULL,
STARTED_ON DATE NOT NULL,
TYPE INT NOT NULL,
PRIMARY KEY (ID)
);
INSERT INTO TOUR (DESCRIPTION, STARTED_ON, TYPE) VALUES
('Mediterranian Cruise','2018-01-01',3),
('Trip to Nepal','2017-12-01',1),
('Tour in Nova York','2015-04-24',5),
('A week at the Amazones','2014-09-11',2),
('Visiting the Machu Picchu','2013-02-19',4);
INSERT INTO STAGE (TOUR, TYPE, CITY, DAYS) VALUES
(1, 1, 38254, 1),
(1, 2, 22460, 3),
(1, 2, 47940, 3),
(1, 2, 42600, 4),
(1, 3, 38254, 1),
(2, 1, 13097, 1),
(2, 2, 29785, 5),
(2, 3, 13097, 1),
(3, 1, 788, 2); ,
(3, 2, 48019, 6),
(3, 3, 788, 1),
(4, 1, 38254, 2),
(4, 2, 8703, 3);,
(4, 3, 38254, 4),
(5, 1, 10453, 1),
(5, 2, 32045, 5),
(5, 3, 10453, 2);
Query:
SELECT
AVG(TD.TOUR_DAYS) AS AVERAGE_DAYS,
COUNT(TD.TOUR_ID) AS WEIGHT
FROM
(
SELECT
TOUR.ID AS TOUR_ID,
SUM(DAYS) AS TOUR_DAYS,
COUNT(STAGE.ID) AS STAGE_DAYS
FROM
TOUR
INNER JOIN
STAGE
ON
TOUR.ID = STAGE.TOUR
GROUP BY
TOUR.ID
) AS TD
GROUP BY
TD.TOUR_DAYS
weigthed avg would be:
(1×7+1×8+2×9+1×12) / (1+1+2+1) = 9

Wheighted AVG can be calculated with SUM(value * wheight) / SUM(wheight). In your case:
SELECT SUM(AVERAGE_DAYS * WEIGHT) / SUM(WEIGHT)
FROM (
SELECT
AVG(TD.TOUR_DAYS) AS AVERAGE_DAYS,
COUNT(TD.TOUR_ID) AS WEIGHT
FROM
(
SELECT
TOUR.ID AS TOUR_ID,
SUM(DAYS) AS TOUR_DAYS,
COUNT(STAGE.ID) AS STAGE_DAYS
FROM
TOUR
INNER JOIN
STAGE
ON
TOUR.ID = STAGE.TOUR
GROUP BY
TOUR.ID
) AS TD
GROUP BY
TD.TOUR_DAYS
) sub
http://sqlfiddle.com/#!9/53d80/4
I'm not 100% sure, but it looks like the following query is doing exactly the same:
SELECT AVG(TOUR_DAYS)
FROM (
SELECT TOUR, SUM(DAYS) AS TOUR_DAYS
FROM STAGE
GROUP BY TOUR
) sub;
Or even without any subqueries:
SELECT SUM(DAYS) / COUNT(DISTINCT TOUR)
FROM STAGE;
That would mean, the requirement should be simplified to "Get average number of days per tour".

Related

SQL including count and multiple where clauses

I have 2 tables.
Table text_to_annotate:
CREATE TABLE text_to_annotate (
ID varchar(3),
text varchar(100));
INSERT INTO text_to_annotate (ID, text)
VALUES
(1, test1),
(2, test2),
(3, test3);
Table annotation_data:
CREATE TABLE annotation_data (
text_ID varchar(3),
annotation_ID varchar(3)
IP varchar(15));
INSERT INTO annotation_data (text_ID, annotation_ID, IP)
VALUES
(1, 0, IP_1),
(2, 1, IP_1),
(3, 2, IP_1),
(1, 1, IP_2),
(2, 2, IP_2),
(3, 3, IP_2),
(3, 0, IP_3),
(3, 0, IP_4),
(3, 2, IP_5);
I want to display an unseen text to an annotator which hasn't been annotated more than 5 times. For example, a new annotator with IP = IP_6 cannot annotate text_ID = 3, only text_ID = 1 and text_ID = 2. An annotator can only annotate unique text_IDs once.
Here's my code, but something isn't quite correct:
SELECT text_to_annotate.ID, text_to_annotate.text
FROM text_to_annotate
WHERE text_to_annotate.ID NOT IN (
SELECT text_ID, count(*)
FROM annotation_data
WHERE IP = '{$ip}'
AND GROUP BY text_ID
HAVING count(*) > 1;
)
ORDER BY RAND()
Here's my answer:
SELECT text_to_annotate.ID, text_to_annotate.text
FROM text_to_annotate
WHERE text_to_annotate.ID NOT IN (
SELECT text_ID, count(*)
FROM annotation_data
WHERE IP = '{$ip}'
)
AND text_to_annotate.ID IN (
SELECT text_ID FROM impact_annotation
GROUP BY text_ID
HAVING COUNT(*) < 5
)
ORDER BY RAND()

MYSQL JOIN with lower-than / greater-than conditions (minimum_quantity, valid_from)

I have
order table with columns
id
date
supplier_id
order_lineitem table with columns
id
order_id
article_id
order_quantity
order_price
a prices table with columns
id
article_id
supplier_id
valid_until
minimum_order_quantity
list_price
The prices table doesn't necessarily have to have a matching / valid entry, so this one would have to be joined via an outer join.
I'd like to compare order_prices against list_prices.
Therefore I need to somehow join
SELECT
o.id,
o.date,
ol.article_id,
ol.order_quantity,
ol.order_price,
p.list_price
FROM
`order` o JOIN order_lineitem ol on ol.order_id = o.id
LEFT OUTER JOIN prices p on
p.article_id = ol.article_id
AND p.supplier_id = o.supplier_id
AND p.minimum_order_quantity <= ol.order_quantity
AND IFNULL(p.valid_until, DATE('2099-12-31')) >= o.date
/* here comes the fun part that doesn't work (reliably) */
ORDER BY
IFNULL(p.valid_until, DATE('2099-12-31')) asc,
p.minimum_order_quantity desc
GROUP BY o.id, ol.id, p.article_id
/* ... trying to get only THAT price from the prices table that applies for the
(a) the given article
(b) from the given supplier
(c) that was valid at the time of purchase (i.e. has the smallest "valid_until" date that is greater than the purchase date)
(d) when ordering the given quantity (prices can also increase with higher quantities, so it has to be the price with the largest minimum_order_quantity that is smaller than the ordered quantity)
*/
I particularly don't want to fall into the trap (which I dug for myself here) of using group by to limit the results to 1 record from the prices table based on a previous sorting, since
(i) as per MySQL documentation it is non-deterministic which record will actually get returned (although it may in effect often work and this is a frequently suggested route to go) - also see this excellent explanation on the issue: https://stackoverflow.com/a/14770936/9818188 and
(ii) this concept wouldn't work on other SQL implementations like SQL Server, Maria DB & Co.
The question is not around putting in a nested query in order to be able to ORDER first and then GROUP subsequently. It's more about how to really properly get the correct row--ideally also working on other SQL implementations like SQL Server, Maria DB or Google BigQuery.
And since I can't really rely on prices being cheaper the more I buy I also can't simply get the min(list_price).
How can this can be achieved?
Since the output of this query is required for downstream processing, I can't slice & dice the task but need a full list of all orders with respective list prices.
EDIT
Here is a SQL fiddle - the desired prices are shown in column order_price, the prices incorrectly determined by the JOIN (excluding the order byclause - as this would cause non-deterministic results) are shown in column list_price:
http://sqlfiddle.com/#!9/f03a4f/2
CREATE TABLE `order`
(`id` int, `date` datetime, `supplier_id` int)
;
INSERT INTO `order`
(`id`, `date`, `supplier_id`)
VALUES
(1, '2022-01-15 00:00:00', 1),
(2, '2022-02-15 00:00:00', 1),
(3, '2022-03-15 00:00:00', 1),
(4, '2022-01-15 00:00:00', 2),
(5, '2022-02-15 00:00:00', 2),
(6, '2022-03-15 00:00:00', 2)
;
CREATE TABLE order_lineitem
(`id` int, `order_id` int, `article_id` int, `order_quantity` int, `order_price` int)
;
INSERT INTO order_lineitem
(`id`, `order_id`, `article_id`, `order_quantity`, `order_price`)
VALUES
(1, 1, 1, 1, 11),
(2, 1, 1, 10, 8),
(3, 1, 1, 100, 9),
(4, 2, 1, 1, 15),
(5, 2, 1, 10, 12),
(6, 2, 1, 100, 13),
(7, 3, 1, 1, 17),
(8, 3, 1, 10, 14),
(9, 3, 1, 100, 16),
(10, 4, 1, 1, 10),
(11, 4, 1, 10, 80),
(12, 4, 1, 100, 80),
(13, 5, 1, 1, 10),
(14, 5, 1, 10, 80),
(15, 5, 1, 100, 80),
(16, 6, 1, 1, 10),
(17, 6, 1, 10, 10),
(18, 6, 1, 100, 10)
;
CREATE TABLE prices
(`id` int, `article_id` int, `supplier_id` int, `valid_until` varchar(10), `minimum_order_quantity` int, `list_price` int)
;
INSERT INTO prices
(`id`, `article_id`, `supplier_id`, `valid_until`, `minimum_order_quantity`, `list_price`)
VALUES
(1, 1, 1, '2022-01-31', 1, 11),
(2, 1, 1, '2022-01-31', 10, 8),
(3, 1, 1, '2022-01-31', 100, 9),
(4, 1, 2, NULL, 1, 10),
(5, 1, 1, '2022-02-31', 1, 15),
(6, 1, 1, '2022-02-31', 10, 12),
(7, 1, 1, '2022-02-31', 100, 13),
(8, 1, 1, NULL, 1, 17),
(9, 1, 1, NULL, 10, 14),
(10, 1, 1, NULL, 100, 16),
(11, 2, 1, NULL, 1, 99),
(12, 1, 2, '2022-02-31', 10, 80)
;
SELECT
o.id,
o.supplier_id,
o.date,
ol.article_id,
ol.order_quantity,
ol.order_price,
p.list_price
FROM
`order` o JOIN order_lineitem ol on ol.order_id = o.id
LEFT OUTER JOIN prices p on
p.article_id = ol.article_id
AND p.supplier_id = o.supplier_id
AND p.minimum_order_quantity <= ol.order_quantity
AND IFNULL(p.valid_until, DATE('2099-12-31')) >= o.date
/* here comes the fun part that doesn't work (reliably) */
/* NOTE: I am purposesly commenting out the ORDER BY clause here, because
(a) it would have to go after GROUP BY - requiring a nested table which I would like to prevent AND, more importantly,
(b) limiting the numer of rows returned to 1 by GROUPing with an incomplete set of columns on a sorted table may return non-deterministic results as per the MySQL documentation.
see also https://stackoverflow.com/a/14770936/9818188 explaining the issue with GROUP BY in this context
#
# ORDER BY
# IFNULL(p.valid_until, DATE('2099-12-31')) asc,
# p.minimum_order_quantity desc
*/
GROUP BY o.id, ol.id, p.article_id
/* ... trying to get only THAT price from the prices table that applies for the
(a) the given article
(b) from the given supplier
(c) that was valid at the time of purchase (i.e. has the smallest "valid_until" date that is greater than the purchase date)
(d) when ordering the given quantity (prices can also increase with higher quantities, so it has to be the price with the largest minimum_order_quantity that is smaller than the ordered quantity)
*/
If you are interrestd in the highest listprice, you would do it like the.
If you need also other columns from theprices table, you need to SQL select only rows with max value on a column
as you have to join the sub querys for all articles
SELECT
o.id,
o.date,
ol.article_id,
ol.order_quantity,
ol.order_price,
(SELECT `list_price` FROM prices p WHERE
p.article_id = ol.article_id
AND p.supplier_id = o.supplier_id
AND p.minimum_order_quantity <= ol.order_quantity
AND IFNULL(p.valid_until, DATE('2099-12-31')) >= o.date
ORDER BY `list_price` DESC
LIMIT 1
) list_price
FROM
`order` o JOIN order_lineitem ol on ol.order_id = o.id

How do i get a limited number of rows for each value in a field? (db is mysql but could be changed if easier)

I found this difficult to search for this question. I have a table of sports fixtures (tbl_fixture) and a table of sports participants (tbl_participant) which have a many-to-many relationship via a linking table (tbl_fixture_participant)
I need to return the most recent 3 fixtures (ie latest tbl_fixture.start_datetime) of multiple participants and whether they won each of the fixtures, (eg more recent 3 fixtures of participant 1 and most recent 3 fixtures of participant 2, and most recent 3 fixtures of participant 3, with each record returning the fixture_id, participant_id, start_datetime and is_winner fields).
The number of participants that i need to get the data for could be between 1 and 100.
If there's a better way to structure my data, or a better database for this type of query (graph db?) then i'm happy to look into those.
Here's a sample schema:
CREATE TABLE tbl_fixture (
fixture_id INT AUTO_INCREMENT PRIMARY KEY,
start_datetime DATETIME NOT NULL
);
CREATE TABLE tbl_participant (
participant_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE tbl_fixture_participant (
fixture_id INT NOT NULL,
participant_id INT NOT NULL,
is_winner TINYINT NOT NULL,
FOREIGN KEY (fixture_id)
REFERENCES tbl_fixture (fixture_id)
ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (participant_id)
REFERENCES tbl_participant (participant_id)
ON UPDATE RESTRICT ON DELETE CASCADE
);
INSERT INTO tbl_fixture (fixture_id, start_datetime)
VALUES (1, "2021-01-14 15:00:00"),
(2, "2021-01-13 16:00:00"),
(3, "2021-01-12 17:00:00"),
(4, "2021-01-11 15:00:00"),
(5, "2021-01-19 16:00:00"),
(6, "2021-01-18 17:00:00"),
(7, "2021-01-05 15:00:00"),
(8, "2021-01-03 16:00:00"),
(9, "2021-01-03 17:00:00"),
(10, "2021-01-11 15:00:00"),
(11, "2021-01-12 16:00:00"),
(12, "2021-01-13 17:00:00"),
(13, "2021-01-14 15:00:00"),
(14, "2021-01-19 16:00:00");
INSERT INTO tbl_participant (participant_id, name) VALUES
( 1,"Team 1"),
( 2,"Team 2"),
( 3,"Team 3");
INSERT INTO tbl_fixture_participant (fixture_id, participant_id, is_winner)
VALUES (1, 1, 0)
,(2, 1, 1)
,(2, 2, 0)
,(3, 1, 1)
,(12, 2, 0)
,(4, 3, 1)
,(4, 2, 0)
,(6, 3, 1)
,(1, 2, 1)
,(10, 1, 1)
,(5, 2, 0)
,(6, 1, 0)
,(11, 1, 1)
,(14, 1, 0)
,(7, 2, 0)
,(7, 3, 1)
,(3, 3, 0)
,(8, 1, 0)
,(5, 3, 1)
,(13, 2, 0)
,(8, 3, 1)
,(13, 3, 1)
,(9, 1, 0)
,(9, 2, 1)
,(10, 2, 0)
,(11, 3, 0)
,(12, 3, 1)
,(14, 3, 1);
And SQL Fiddle of same.
I would like the data to come back like:
fixture_id
start_datetime
participant_id
is_winner
14
2021-01-19T16:00:00Z
1
0
6
2021-01-18T17:00:00Z
1
0
1
2021-01-14T15:00:00Z
1
0
5
2021-01-19T16:00:00Z
2
0
13
2021-01-14T15:00:00Z
2
0
1
2021-01-14T15:00:00Z
2
1
EDITED TO REFLECT FACT THAT DATES ARE NOT NECESSARILY SEQUENTIAL...
E.g. (for older versions of MySQL)...
SELECT x.*
, fx.start_datetime
FROM fixture_participant x
JOIN fixture fx
ON fx.fixture_id = x.fixture_id
JOIN fixture_participant y
ON y.participant_id = x.participant_id
JOIN fixture fy
ON fy.fixture_id = y.fixture_id
AND fy.start_datetime > fx.start_datetime
GROUP
BY x.fixture_id
, x.participant_id
, x.is_winner
, fx.start_datetime
HAVING COUNT(x.fixture_id) <=3
ORDER
BY participant_id,fixture_id;
...or something like that.

Complex SQL query issue (MYSQL Workbench 6.3)

I am having trouble figuring out how to write this query.
Let me explain the situation.
So, the question,
I need to display all the player names who have scored a score greater than 99, who have played matches in all the same grounds where a certain player (e.g. pid = 1) has played and has scored a score greater than 99.
(They could have played in other grounds besides the one pid = 1 has played, but the minimum requirement being they must have played in all the same grounds as him).
I have a database, which consist of 3 tables; player, ground, matches. And following data.
create database test1;
use test1;
CREATE TABLE `player` (
`pid` int(11) NOT NULL,
`pname` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `ground` (
`gid` int(11) NOT NULL,
`gname` varchar(20) DEFAULT NULL,
`country` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `matches` (
`pid` int(11) DEFAULT NULL,
`gid` int(11) DEFAULT NULL,
`score` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `player` ADD PRIMARY KEY (`pid`);
ALTER TABLE `ground` ADD PRIMARY KEY (`gid`);
ALTER TABLE `matches`
ADD KEY `gid` (`gid`),
ADD KEY `pid` (`pid`);
INSERT INTO `player` (`pid`, `pname`) VALUES
(1, 'afridi'),
(2, 'kohli'),
(3, 'imam'),
(4, 'fawad'),
(5, 'baven'),
(6, 'awais');
INSERT INTO `ground` (`gid`, `gname`, `country`) VALUES
(1, 'Qaddafi', 'PK'),
(2, 'National', 'PK'),
(3, 'Eden Garden', 'IND'),
(4, 'Lords', 'ENG'),
(5, 'MCG', 'AUS'),
(6, 'Arbab Nayyaz', 'PK');
INSERT INTO `matches` (`pid`, `gid`, `score`) VALUES
(1, 2, 23),
(1, 1, 111),
(2, 3, 107),
(2, 5, 103),
(1, 3, 117),
(1, 4, 55),
(1, 5, 101),
(1, 6, 44),
(2, 6, 103),
(2, 4, 103),
(2, 2, 117),
(2, 1, 103),
(4, 1, 77),
(3, 1, 13),
(5, 2, 22),
(3, 2, 101),
(3, 3, 101),
(5, 1, 101),
(5, 4, 101),
(5, 5, 101),
(6, 1, 101),
(6, 2, 101),
(6, 3, 101),
(6, 4, 101),
(6, 5, 101),
(6, 4, 101);
Relatively a simple database.
I've written the following query which displays the names of 4 players. It is displaying all the players who have played in the same grounds as pid = 1. How to display only those players which have played in all the same grounds as pid = 1.
select p.pname
from player p
join matches mn on mn.pid = p.pid
where (p.pid != 1) and (mn.score > 99) and exists (select m.gid from matches m where (m.pid = 1) and (mn.gid = m.gid))
group by pname;
According to the data provided in the tables,
Afridi (pid = 1) has scored century in the following grounds; 1, 3, and 5.
Respectively, players (pid) 2, 3, 5 ,6 have scored century in grounds = 1, 3, and 5.
These players have made centuries in other grounds as well but this query displays all players who have played in any of the 3 grounds.
The players could've played in other grounds as well, but the minimum requirement being that the players have to play in all the grounds; 1, 3, 5.
So, what I need is, only all those players, which have played in all of the same grounds, as in grounds; 1, 3, 5.
From observing the data in table matches we can see the players that have played in all the same grounds are only 2, being pid = 2, 6.
Any idea how to go about this?
I think this query should do what you want. It creates a table of grounds where the first player has played and made a century (g1), and joins that to the players who have also played at those grounds. If the number of different grounds that the other player has played at is the same as the number of different grounds that the first player has played at, they must have both played at the same set of grounds. Note there are a couple of places (in both subqueries) where you need to set the player id for comparison.
SELECT p.pname
FROM (SELECT gid, pid FROM matches WHERE pid=1 AND score >= 100) g1
LEFT JOIN matches m
ON m.gid = g1.gid AND m.pid != g1.pid
JOIN player p
ON p.pid = m.pid
GROUP BY m.pid
HAVING COUNT(DISTINCT m.gid) = (SELECT COUNT(DISTINCT gid) FROM matches WHERE pid=1 AND score >= 100)
ORDER BY m.pid
SQLFiddle Demo

MySQL sort column by values

I want to sort my database result based on a column value. I thought I needed to do this:
SELECT * FROM products
ORDER BY FIELD(brand_id, 4, 1, 6)
But this doesn't sort my result at all. Or at least in a way I don't understand.
What I want to achieve is that I get all products, ordered by brand_id. First all with brand_id 4, then 1 then 6 and then the rest.
Here is my example on jsfiddle.
If not working (jsfiddle not working fine here), see my code below:
Schema:
CREATE TABLE `products` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`brand_id` int(10) unsigned NOT NULL,
`price` decimal(8,2) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
INSERT INTO `products` (`id`, `brand_id`, `price`)
VALUES
(1, 1, 10.32),
(2, 4, 15.32),
(3, 2, 23.32),
(4, 6, 56.32),
(5, 4, 23.32),
(6, 4, 23.32),
(7, 1, 25.32),
(8, 5, 15.32),
(9, 3, 55.32),
(10, 5, 23.32);
The problem is here:
FIELD(brand_id, 4, 1, 6)
will return 0 if brand_id is not present in the list, so brands not present will be at the top. You can use this:
ORDER BY FIELD(brand_id, 6, 1, 4) DESC
with all of the values in reverse order, and then you have to order in DESC order.
Normally, when you use field() for sorting you also have in:
SELECT p.*
FROM products p
WHERE brand_id IN (4, 1, 6)
ORDER BY FIELD(brand_id, 4, 1, 6);
Then it works as advertised.
Instead of reversing values, you can also take a more explicit approach:
SELECT p.*
FROM products p
ORDER BY brand_id IN (4, 1, 6) DESC,
FIELD(brand_id, 4, 1, 6);
This is longer, but the intention is clear.