SELECT Statement for cocktail db - sql

This is probably pretty simple and dumb to ask but Im just not getting there right now. I have a db for cocktails and want to check which cocktails I can make with the available ingredients:
Get the names of all cocktails where every ingredient is in stock
These are my tables:
create table cocktails
(
name TEXT PRIMARY KEY
)
create table ingredients
(
name TEXT PRIMARY KEY
)
create table cocktail_ingredients
(
cocktail_name TEXT ,
ingredient_name TEXT ,
amount INTEGER ,
FOREIGN KEY ( cocktail_name ) REFERENCES cocktails( name ) ,
FOREIGN KEY ( ingredient_name ) REFERENCES ingredients( name )
)
create table ingredients_in_stock
(
ingredient_name TEXT ,
FOREIGN KEY ( ingredient_name ) REFERENCES ingredients ( name )
)
And this is my code so far:
SELECT ci.cocktail_name
FROM cocktail_ingredients ci
WHERE ci.ingredient_name IN ( SELECT iis.ingredient_name
FROM ingredients_in_stock iis
)
GROUP BY ci.cocktail_name
HAVING COUNT(*) = ( SELECT COUNT(*)
FROM ingredients_in_stock
)
;

You can use a LEFT JOIN and a IN clause for this. Something like this:
SELECT name FROM cocktails WHERE Name NOT IN(
SELECT DISTINCT ci.cocktail_name FROM cocktail_ingredients ci LEFT JOIN ingredients_in_stock istk
ON ci.ingredient_name=istk.ingredient_name WHERE istk.ingredient_name IS NULL)
This query inverts the logic: List the cocktails where none of it's ingredients are missing on the ingredients_in_stock table. Hope the idea helps you

A correlated subquery should work:
select cocktail_name as all_ingredients_in_stock
from cocktail_ingredients ci
inner join ingredients_in_stock iis
on ci.ingredient_name = iis.ingredient_name
group by cocktail_name
having count(*) =
(select count(*)
from cocktail_ingredients
where cocktail_name = ci.cocktail_name
)
Sample SQL Fiddle

You could just say something like this:
select ci.name
from cocktail_ingredients ci
left join ingredients_in_stock iis on iis.ingredient_name = ci.ingredient_name
group by ci.name
having count(ci.ingredient_name) = sum( case
when iis.ingredient_name is not null
then 1
else 0
end
)
In the having clause,
The count(ci.ingredient_name) gives you the total number of ingredients required for the cocktail
The sum() expression gives you the count of in-stock ingredients used by the cocktail.

Related

Optimise many-to-many join

I have three tables: groups and people and groups_people which forms a many-to-many relationship between groups and people.
Schema:
CREATE TABLE groups (
id SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE people (
id SERIAL PRIMARY KEY,
name TEXT,
join_date TIMESTAMP
);
CREATE TABLE groups_people (
group_id INT REFERENCES groups(id),
person_id INT REFERENCES people(id)
);
When I want to query for the latest 10 people who recenlty joined the group which has id = 1:
WITH person_ids AS (SELECT person_id FROM groups_people WHERE group_id = 1)
SELECT * FROM people WHERE id = ANY(SELECT person_id FROM person_ids)
ORDER BY join_date DESC LIMIT 10;
The query needs to scan all of the joined people then ordering them before selecting. That would be slow if the group containing too many people.
Is there anyway to work around it?
Schema (re-)design to allow same person joining multiple group
Since you mentioned that the relationship between groups and people
is many-to-many, I think you may want to move join_date to groups_people
(from people) because the same person can join different groups and each
such event has its own join_date
So I would change the schema to
CREATE TABLE people (
id SERIAL PRIMARY KEY,
name TEXT --, -- change
-- join_date TIMESTAMP -- delete
);
CREATE TABLE groups_people (
group_id INT REFERENCES groups(id),
person_id INT REFERENCES people(id), -- change
join_date TIMESTAMP -- add
);
Query
select
p.id
, p.name
, gp.join_date
from
people as p
, groups_people as gp
where
p.id = gp.person_id
and gp.group_id=1
order by gp.join_date desc
limit 10
Disclaimer: The above query is in MySQL syntax (the question was originally tagged with MySQL)
This seems much easier to write as a simple join with order by and limit:
select p.*
from people p join
groups_people gp
on p.id = gp.person_id
where gp.group_id = 1
order by gp.join_date desc
limit 10; -- or fetch first 10 rows only
Try rewriting using EXISTS
SELECT *
FROM people p
WHERE EXISTS (SELECT 1
FROM groups_people ps
WHERE p.id = ps.person_id and group_id = 1)
ORDER BY join_date DESC
LIMIT 10;

PostgreSQL select query doesn't work

I have 3 tables:
CREATE table materials
(id serial primary key not null,
name varchar(50) not null,
unit varchar(10) not null default 'шт',
price decimal(12, 2) not null check (price>0));
CREATE table warehouses
(id serial primary key not null,
lastname varchar(25) not null);
CREATE table materials_in_warehouses
(id_warehouses integer references warehouses(id) on update cascade on delete cascade,
id_materials integer references materials(id),
unit varchar(15) default 'шт',
count integer not null CHECK (count>0),
lastdate date not null,
primary key (id_warehouses, id_materials);
And i need to select for each material : name; count of warehouses, where the material is present in an amount of > 100.
I tried to do something like this:
select materials.name, count(select * from materials_in_warehouses where
materials.id = materials_in_warehouses.id_materials AND material_in_warehouses.count > 100) as sount from materials;
But of course, this nonsense can't work.
Thank you in advance.
Pretty straight forward.
SELECT m.name, count(miw.id_warehouses)
FROM materials m
LEFT JOIN materials_in_warehouses miw ON m.id=miw.id_materials AND miw."count">100
GROUP BY m.id, m.name
Your mistake might have just been the fact that you're using count as a column name, when it's an aggregate function. Use double quotes in PostgreSql for that:
Escaping keyword-like column names in Postgres
Try like this
select materials.name, count(
select id_warehouses from materials_in_warehouses join materials
on materials.id = materials_in_warehouses.id_materials
where material_in_warehouses.count > 100
) as sount from materials;
SELECT m.name, COUNT(w.id) AS locs
FROM materials m, warehouses w, materials_in_warehouses mw
WHERE m.id = mw.id_materials
AND w.id = mw.id_warehouses
AND mw.count > 100
GROUP BY m.name;
If that works I'll come back and explain how I came up with it.

Proper use of NOT EXISTS

I have two tables -
member_master
-----------------------------
member_id, ( PK )
branch_id, ( PK )
name member_id,
member_photo
-----------------------------
member_id, ( FK )
branch_id, ( FK )
photo_index,
photo_file
Each entry in member_master has zero or more corresponding entries in member_photo table.
I have two requirements :
Get all the entries from member_master which have at least one entry in member_photo table. I am getting the correct result using the following SQL command
SELECT DISTINCT member_master.member_id,member_master.branch_id,name
FROM member_master, member_photo
WHERE member_master.branch_id=1
AND EXISTS
(
SELECT member_photo.member_id
WHERE member_master.member_id = member_photo.member_id
AND member_master.branch_id = member_photo.branch_id
)
;
Get all the entries from member_master which DO NOT have any entry in member_photo table. I am using the following SQL command
SELECT DISTINCT member_master.member_id,member_master.branch_id,name FROM member_master, member_photo
WHERE member_master.branch_id=1
AND NOT EXISTS
(
SELECT member_photo.member_id
WHERE member_master.member_id = member_photo.member_id
AND member_master.branch_id = member_photo.branch_id
)
;
The only difference is I have added a NOT before EXISTS command.
But unfortunately it does not give me the correct result. It simply returns all the rows in the table.
Please note that I am using SQL Server Express 2005.
You should do it without joining the tables, just select from the master table and add check for the photo table, like this:
SELECT
m.member_id,
m.branch_id,
m.name
FROM
member_master m
WHERE
m.branch_id=1 AND
EXISTS (SELECT 1 from member_photo p where
m.member_id = p.member_id AND m.branch_id = p.branch_id)
And similarly the other case:
SELECT
m.member_id,
m.branch_id,
m.name
FROM
member_master m
WHERE
m.branch_id=1 AND
not EXISTS (SELECT 1 from member_photo p where
m.member_id = p.member_id AND m.branch_id = p.branch_id)
Required SQL query is:
SELECT
member_id,
branch_id,
name
FROM member_master
WHERE member_master.branch_id=1
AND NOT EXISTS(SELECT member_photo.member_id
WHERE member_master.member_id = member_photo.member_id
AND member_master.branch_id = member_photo.branch_id)

SQL help for beginner

I need a query which returns the number of sportsman in each team.
It should only display teams with a name longer than the average length of names and surnames of sportsman.
I have two tables teams and sportmans.
Here's what I tried:
SELECT TIMOVI,SPORTISTI COUNT(IME_PREZIME)
FROM TIMOVI
JOIN SPORTISTI
ON SIFRA=TIM,
GROUP BY TIMOVI
WHERE LENGTH(naziv)> ((AVG(LENGHT(ime_preizime)))/2)
How about a query like this:
SELECT t.name, COUNT(s.id)
FROM team t
INNER JOIN sportsman s ON s.team = t.id
WHERE LENGTH(t.name) > (
SELECT AVG(LENGTH(s2.name)) FROM sportsman s2
)
GROUP BY t.id
For schema like this:
CREATE TABLE sportsman (
id INTEGER,
team INTEGER,
name VARCHAR(50)
);
CREATE TABLE team (
id INTEGER,
name VARCHAR(50)
);
SQL Fiddle

how can i rewrite a select query in this situation

Here are two table in parent/child relationship.
What i need to do is to select students with there average mark:
CREATE TABLE dbo.Students(
Id int NOT NULL,
Name varchar(15) NOT NULL,
CONSTRAINT PK_Students PRIMARY KEY CLUSTERED
(
CREATE TABLE [dbo].[Results](
Id int NOT NULL,
Subject varchar(15) NOT NULL,
Mark int NOT NULL
)
ALTER TABLE [dbo].[Results] WITH CHECK ADD CONSTRAINT [FK_Results_Students] FOREIGN KEY([Id])
REFERENCES [dbo].[Students] ([Id])
I wrote a query like this :
SELECT name , coalesce(avg(r.[mark]),0) as Avmark
FROM students s
LEFT JOIN results r ON s.[id]=r.[id]
GROUP BY s.[name]
ORDER BY ISNULL(AVG(r.[mark]),0) DESC;
But the result is that all of students with there avg mark in desc order.What i need is to restrict result set with students that have the highest average mark agaist other,i.e.if the are two students with avg mark 50 and 1 with 25 i need to display only those students with 50.If there are only one student with highest avg mark- only he must appear in result set.How can i do this in best way?
SQL Server 2005+, using CTEs:
WITH grade_average AS (
SELECT r.id,
AVG(r.mark) 'avg_mark'
FROM RESULTS r
GROUP BY r.id),
highest_average AS (
SELECT MAX(ga.avg_mark) 'highest_avg_mark'
FROM grade_average ga)
SELECT DISTINCT
s.name,
ga.avg_mark
FROM STUDENTS s
JOIN grade_average ga ON ha.id = s.id
JOIN highest_average ha ON ha.highest_avg_mark = ga.avg_mark
Non-CTE equivalent:
SELECT DISTINCT
s.name,
ga.avg_mark
FROM STUDENTS s
JOIN (SELECT r.id,
AVG(r.mark) 'avg_mark'
FROM RESULTS r
GROUP BY r.id) ga ON ha.id = s.id
JOIN SELECT MAX(ga.avg_mark) 'highest_avg_mark'
FROM (SELECT r.id,
AVG(r.mark) 'avg_mark'
FROM RESULTS r
GROUP BY r.id) ga) ha ON ha.highest_avg_mark = ga.avg_mark
If you're using a relatively new version of MS SQL server, you can use WITH to make this simple to write:
WITH T AS (
SELECT
name,
coalesce(avg(r.[mark]),0) as mark
FROM students s
LEFT JOIN results r ON s.[id]=r.[id]
GROUP BY s.[name])
SELECT name as 'ФИО', mark as 'Средний бал'
FROM T
WHERE T.mark = (SELECT MAX(mark) from T)
Is it as simple as this? For all versions of SQL Server 2000+
SELECT TOP 1 WITH TIES
name, ISNULL(avg(r.[mark]),0) as AvMark
FROM
students s
LEFT JOIN
results r ON s.[id]=r.[id]
GROUP BY
s.[name]
ORDER BY
ISNULL(avg(r.[mark]),0) DESC;
SELECT name as 'ФИО',
coalesce(avg(r.[mark]),0) as 'Средний бал'
FROM students s
LEFT JOIN results r
ON s.[id]=r.[id]
GROUP BY s.[name]
HAVING AVG(r.[mark]) >= 50
ORDER BY ISNULL(AVG(r.[mark]),0) DESC
about HAVING clause