Making Queries for relaional model - sql

I have some troubles with making queries for this task. anyone please help me.
Cars(License, Brand, Year);
Employees(EID, Firstname, Lastname, Wage);
Customers(CID, Firstname, Lastname, Phone);
OfficeStaff(EID, OfficeNumber);
Own(CID, License);
Mechanic(EID, HourlyPrice);
Repairs(License, EID, PartCost, Hours);
Create SQL statements performing the following tasks:
(a) Create the OfficeStaff-table while taking into account that the OfficeNumber may not be NULL and must be in
the range [1..10].
(b) Find the name(s), i.e. firstname(s) and lastname(s), of the owner(s) of the car(s), which have been repaired for
the most times.
(c) Find the average number of hours spent (i.e. Repairs.Hours) repairing cars of brand “Opel”.
(d) Update the HourlyPrice to be 20 EUR for all mechanics with a wage of 100 EUR or more.
My tries:
(a) Create the OfficeStaff-table while taking into account that the OfficeNumber may not be NULL and must be in the range [1..10].
CREATE TABLE OfficeStaff (
EID INT PRIMARY KEY,
Firstname TEXT,
Lastname TEXT,
Wage REAL,
OfficeNumber INT NOT NULL,
CONSTRAINT CK_OfficeNumber CHECK (OfficeNumber BETWEEN 1 AND 10)
b) no idea?!
(c) Find the average number of hours spent (i.e. Repairs.Hours) repairing cars of brand “Opel”.
SELECT AVG(R.Hours)
FROM Repairs R, Cars C
WHERE R.License = C.License AND C.Brand = “Opel”
(d) Update the HourlyPrice to be 20 EUR for all mechanics with a wage of 100 EUR or more.
UPDATE Mechanic
SET HourlyPrice = 20
WHERE Wage >= 100

(a) how to create can look here
CREATE TABLE OfficeStaff (
EID INT PRIMARY KEY,
Firstname varchar(100),
Lastname varchar(100),
Wage decimal(15,2),
OfficeNumber INT NOT NULL,
CONSTRAINT CK_OfficeNumber CHECK (OfficeNumber BETWEEN 1 AND 10)
)
(b) Not sure about this one, but you have to use rank to get not only 1 value among same values. For this you can look here
WITH cte AS (
SELECT a.Firstname, a.Lastname, rank() OVER (ORDER BY count(c.Hours)) AS rnk
from Customers as a
join Own as b
on a.CID=b.CID
join Repairs as c
on b.License = c.License
group by a.Firstname, a.Lastname
)
SELECT *
FROM cte
WHERE rnk <= 1;
(c) join usage here
SELECT AVG(R.Hours)
FROM Repairs R
join Cars C
on R.license=C.license
WHERE C.Brand = 'Opel'
(d) update usage here and here
UPDATE Mechanic
SET HourlyPrice = 20
from Employees
WHERE Mechanic.EID = Employees.EID
AND Employees.Wage >= 100

Related

SQL Server Stored Procedure for Menu Performance Report

I have four tables in my SQL database i.e MenuItems, Categories, Invoices and InvoiceDetails. Now what I want is to show the menu performance report for a certain date i.e total Qty and total Amount for
each menu item for a specific date. It shows the desired result without the date in the where clause but excludes menu items with null values.
Here is my stored procedure:
CREATE PROCEDURE spGetMenuPerformanceByDay
#Date date,
#Terminal int
AS
BEGIN
SELECT
M.Name,
ISNULL(SUM(D.Amount), 0) AS Amount,
ISNULL(SUM(D.Qty), 0) AS Qty
FROM
MenuItems AS M
JOIN
Categories AS C ON C.Id = M.CategoryId
LEFT JOIN
InvoiceDetails AS D ON M.Id = D.ItemId
LEFT JOIN
Invoices I ON I.Id = d.InvoiceId
WHERE
#Terminal IN (I.TerminalId, C.TerminalId)
AND CONVERT(date, I.Time) = #Date
OR NULL IN (Amount, Qty)
GROUP BY
M.Name, M.Id, D.ItemId
ORDER BY
(Qty) DESC
END
The result this stored procedure returns on adding Date in where clause:
Item
Amount
Qty
KOFTA ANDA
1950
3
HOT N SOUR SOUP
550
1
CHICKEN CHOWMEIN
250
1
CHICKEN KORMA
850
1
And the result I want is but don't get it on adding Date in where clause :
Item
Amount
Qty
KOFTA ANDA
1950
3
HOT N SOUR SOUP
550
1
CHICKEN CHOWMEIN
250
1
CHICKEN KORMA
850
1
CRISPY CHICKEN
0
0
MEXICAN BURGER
0
0
What if you don't put criteria for Invoices in the WHERE clause?
Sample data
create table Categories (
Id int primary key,
Name varchar(30) not null,
TerminalId int not null
);
create table MenuItems (
Id int identity(21,1) primary key,
Name varchar(30) not null,
CategoryId int not null,
foreign key (CategoryId) references Categories(Id)
);
create table Invoices (
Id int identity(31,1) primary key,
TerminalId int not null,
ItemId int not null,
Time datetime,
foreign key (ItemId) references MenuItems(Id)
);
create table InvoiceDetails (
InvoiceDetailId int identity(41,1) primary key,
InvoiceId int,
Amount decimal(10,2),
Qty int,
foreign key (InvoiceId) references Invoices(Id)
);
insert into Categories (Id, Name, TerminalId) values
(1,'KOFTA', 1),
(2,'SOUP', 1),
(3,'CHICKEN', 1),
(4,'BURGER', 1);
insert into MenuItems (CategoryId, Name) values
(1,'KOFTA ANDA'),
(2,'HOT N SOUR SOUP'),
(3,'CHICKEN CHOWMEIN'),
(3,'CHICKEN KORMA'),
(3,'CRISPY CHICKEN'),
(4,'MEXICAN BURGER');
insert into Invoices (ItemId, TerminalId, Time)
select itm.Id, cat.TerminalId, GetDate() as Time
from MenuItems itm
join Categories cat on cat.Id = itm.CategoryId
where itm.Name in (
'KOFTA ANDA',
'HOT N SOUR SOUP',
'CHICKEN CHOWMEIN',
'CHICKEN KORMA'
);
insert into InvoiceDetails (InvoiceId, Amount, Qty) values
(31, 1950, 3),
(32, 550, 1),
(33, 250, 1),
(34, 850, 1);
Query
DECLARE #TerminalId INT = 1;
DECLARE #Date DATE = GetDate();
SELECT
V.[Date],
C.Name AS Category,
M.Name AS MenuItemName,
ISNULL(SUM(D.Amount), 0) AS Amount,
ISNULL(SUM(D.Qty), 0) AS Qty
FROM Categories AS C
CROSS JOIN (SELECT #Date AS [Date], #TerminalId AS TerminalId) V
JOIN MenuItems AS M
ON M.CategoryId = C.Id
LEFT JOIN Invoices I
ON I.ItemId = M.Id
AND I.TerminalId = V.TerminalId
AND CAST(I.Time AS DATE) = V.[Date]
LEFT JOIN InvoiceDetails AS D
ON D.InvoiceId = I.Id
WHERE C.TerminalId = V.TerminalId
GROUP BY V.[Date], C.Id, M.Id, C.Name, M.Name
ORDER BY SUM(D.Qty) DESC
Date
Category
MenuItemName
Amount
Qty
2021-12-18
KOFTA
KOFTA ANDA
1950.00
3
2021-12-18
SOUP
HOT N SOUR SOUP
550.00
1
2021-12-18
CHICKEN
CHICKEN CHOWMEIN
250.00
1
2021-12-18
CHICKEN
CHICKEN KORMA
850.00
1
2021-12-18
CHICKEN
CRISPY CHICKEN
0.00
0
2021-12-18
BURGER
MEXICAN BURGER
0.00
0
Demo on db<>fiddle here
Here's my crack at your goal. Notice the changes. I found the reference to TerminalId in Category table highly suspicious - so much that I suspect it is a model flaw. Along those lines I note that TerminalId should likely have a foreign key to a missing table for Terminals. So I ignore that.
With that out, references to Category are now irrelevant. So that was removed as well. I also changed the procedure name since I find the reference to "day" misleading. It is highly likely "menu performance" would be evaluated on a "day" basis since retail (especially food service) sales vary by day of week consistently. So let's not mislead anyone thinking that is what this procedure does.
For simplicity and clarity, I removed the ISNULL usage. Add it back if desired but such things are usually better handled by the consumer of the resultset. I left the ORDER BY clause as a stub for you to re-evaluate (and you need to).
So how does this work? Simply calculate the sums directly in the CTE and then outer join from the menu items to the CTE sums to get all menu items along with the relevant performance information for the date specified.
CREATE PROCEDURE dbo.GetMenuPerformanceByDate
#Date date,
#Terminal int
AS
BEGIN
with sales as (
select det.ItemId, SUM(det.Amount) as amt, SUM(det.Qty) as qty
from dbo.Invoices as inv
inner join dbo.InvoiceDetails as det
on inv.Id = det.InvoiceId
where cast(inv.Time as date) = #Date
and inv.TerminalId = #Terminal
group by det.ItemId
)
select menu.name, sales.amt, sales.qty
from dbo.MenuItems as menu
left join sales
on menu.Id = sles.ItemId
order by ...
;
END;
One last note. This filter:
cast(inv.Time as date) = #Date
is generally not a good method of filtering a datetime column. Far better to use inclusive lower and exclusive upper boundaries like:
inv.Time >= #date and inv.Time < dateadd(day, 1, #date)
for this reason.
My last note - there is a potential flaw regarding MenuItems. Presumably "name" is unique. It is highly unlikely that multiple rows would have the same name, but "unlikely" is not a restriction. If you generate rows based on name and name turns out to NOT be unique, your results are difficult to interpret.

Select rows in one table, based on column values in another table?

I need to select one column for each matching row in another table. Sounds simple, but there's a twist.
I have one table that has Company IDs, and defines Companies.
I have another table that defines Departments in those companies.
I have a third table that contains the various Department StatusCodes.
The code that I have below works just fine, and is exactly what I want it to do. But I have to hardcode into the query which departments to look for. I'd like it to sub-select based on department codes it finds in the Departments table, and not require me to hard code each possible department that might exist at that company.
I need to select the matching department status from DepartmentStatus based upon what Departments exist for a company as defined in DepartmentsStatus table.
I suspect it's a pivot table, but they are a bit above my level.
(Company Table)
Company_ID Company_Name
-------------------------
1 Home Office
2 Stanton Office
3 NYC Office
(Departments Table)
CompanyID Department_Code
----------------------------
1 Sales
1 Inventory
1 Retail
1 Maint
2 OtherDept
2 ThatDept
2 BobsDept
(DepartmentStatus Table)
Company_ID Department StatusCode
-----------------------------------------
1 Sales InReview
1 Inventory InReview
1 Retail Ready
1 Maint Done
2 OtherDept InReview
2 ThatDept Research
2 BobsDept InReview
Note: I use "TOP 1", even though there is a unique index on Company_ID+Department, so there will never be more than one matching row.
So, for Company_ID=1:
select Company_ID,
(select top 1 StatusCode from DepartmentStatus ds where ds.Company_ID = cm.Company_ID and ds.Department='Sales') as SalesStatus
(select top 1 StatusCode from DepartmentStatus ds where ds.Company_ID = cm.Company_ID and ds.Department='Inventory') as InvStatus
(select top 1 StatusCode from DepartmentStatus ds where ds.Company_ID = cm.Company_ID and ds.Department='Retail') as RetailStatus
(select top 1 StatusCode from DepartmentStatus ds where ds.Company_ID = cm.Company_ID and ds.Department='Main') as MaintStatus
from Company cm
Where cm.CompanyID=1
Results:
Company_ID SalesStatus InvStatus RetailStatus MaintStatus
--------------- --------------- ---------- ------------- ------------
1 InReview InReview Ready Done
Or, for CompanyID=2:
select Company_ID,
(select top 1 StatusCode from DepartmentStatus ds where ds.CompanyID = cm.Company_ID and ds.Department='OtherDept') as OtherDeptStatus
(select top 1 StatusCode from DepartmentStatus ds where ds.CompanyID = cm.Company_ID and ds.Department='ThatDept') as ThatDeptStatus
(select top 1 StatusCode from DepartmentStatus ds where ds.CompanyID = cm.Company_ID and ds.Department='BobsDept') as BobsDeptStatus
from Company cm
Where cm.CompanyID=2
Results:
Company_ID OtherDeptStatus ThatDeptStatus BobsDeptStatus
---------- ---------------- -------------- --------------
2 InReview Research InReview
Thus, For Company 1, I need to go get the status for Departments Sales, Inventory, Retail, and Maint.
But for Company 2, I need to get the status for Departments OtherDept, ThatDept, and BobsDept.
The steps that I think describe what I am trying to do is:
Get list of Departments for Company 1 from Departments Table.
For each of these Departments in Step 1, query DepartmentStatus table for Department equals that department.
For Company 1, query get StatusCode for Departments "Sales, Inventory, Retail, and Maint"
For Company 2, query get StatusCode for Departments "OtherDept, thatDept, and BobsDept."
etc etc
The problem is, I do not know ahead of time (at query time) which departments exist for each Company, so I need to sub-select based upon what departments actually exist for each company.
There are a few other S.O. answers already that are close what I'm asking for, suggesting the use of a JOIN to accomplish this, however, they all assume that you know the values you want to query ahead of time when writing the join statement.
I'm looking for a solution to this problem, not just a correction on my current attempt. If you have a better idea entirely on how to accomplish this, I'd love to see it.
You seem to be looking for conditional aggregation. Using this technique, your query can be simplified as follows, to avoid the need for multiple inline subqueries:
select Company_ID,
max(case when ds.Department='Sales' then ds.StatusCode end) as SalesStatus,
max(case when ds.Department='Inventory' then ds.StatusCode end) as InvStatus,
max(case when ds.Department='Retail' then ds.StatusCode end) as RetailStatus,
max(case when ds.Department='Main' then ds.StatusCode end) as MaintStatus
from
Company cm
inner join DepartmentStatus ds on ds.Company_ID = cm.Company_ID
Where cm.CompanyID=1
group by cm.CompanyID
this would be my (untested) caveman way of solving your problem.
the only thing you need to know in advance is how many different departments you have in the company with the most departments.
DROP TABLE IF EXISTS Iddepartmentstatus;
DECLARE #Result TABLE(
Companyid INT,
Department1 NVARCHAR(MAX),
Department2 NVARCHAR(MAX),
Department3 NVARCHAR(MAX),
Department4 NVARCHAR(MAX),
Department5 NVARCHAR(MAX));
CREATE TABLE Iddepartmentstatus(
Num INT IDENTITY(1, 1),
Department NVARCHAR(MAX),
Statuscode NVARCHAR(MAX));
DECLARE #IDCompany TABLE(
Num INT IDENTITY(1, 1),
Companyid INT,
Companyname NVARCHAR(MAX));
INSERT INTO #IDCompany (Companyid,
Companyname)
SELECT Company_Id,
Company_Name
FROM Company;
DECLARE #Departmentcount INT = 0,
#Departmentstatuscount INT = 0,
#Companycount INT = 0;
WHILE #Companycount <=
(
SELECT MAX(Num)
FROM #IDCompany)
BEGIN
INSERT INTO Iddepartmentstatus (Department,
Statuscode)
SELECT Department,
Statuscode
FROM Departmentstatus
WHERE Company_Id = #Companycount;
INSERT INTO #Result (Companyid)
SELECT #Companycount AS T;
INSERT INTO #Result (Department1)
SELECT IIF(1 <=
(
SELECT MAX(Num)
FROM #IDCompany), Department + ': ' + Departmentstatus, NULL)
FROM Iddepartmentstatus
WHERE Num = 1;
INSERT INTO #Result (Department2)
SELECT IIF(2 <=
(
SELECT MAX(Num)
FROM #IDCompany), Department + ': ' + Departmentstatus, NULL)
FROM Iddepartmentstatus
WHERE Num = 2;
INSERT INTO #Result (Department3)
SELECT IIF(3 <=
(
SELECT MAX(Num)
FROM #IDCompany), Department + ': ' + Departmentstatus, NULL)
FROM Iddepartmentstatus
WHERE Num = 3;
INSERT INTO #Result (Department4)
SELECT IIF(4 <=
(
SELECT MAX(Num)
FROM #IDCompany), Department + ': ' + Departmentstatus, NULL)
FROM Iddepartmentstatus
WHERE Num = 4;
INSERT INTO #Result (Department5)
SELECT IIF(5 <=
(
SELECT MAX(Num)
FROM #IDCompany), Department + ': ' + Departmentstatus, NULL)
FROM Iddepartmentstatus
WHERE Num = 5;
TRUNCATE TABLE Iddepartmentstatus;
SET #Companycount = #Companycount + 1;
END;
DROP TABLE IF EXISTS Iddepartmentstatus;

Calculating average for each student

Suppose I have the following tables:
-- table student
create table Student(
num int primary key identity,
firstName varchar(30) not null,
lastName varchar(30)
)
-- table module
create table Module(
code int primary key identity,
name varchar(30) not null,
coefficient int not null)
-- table notation
create table Notation(
stud int references student,
Mod int references Module,
DateExam datetime default getdate(),
Note float check (Note between 0 and 20)
primary key(stud , Mod ))
What I want is to display student names, the student num and averages, ranked from best to worst.
Update:
average = sum (ni*ci)/ sum (ci); c: coefficient. n: note
Don't think you need module. and this assumes note is the field you want to average.
SELECT FirstName, LastName, Note, avg(note) over (partition by s.Num) AvgNote
FROM Student S
LEFT JOIN Notation N
on S.Num = N.Stud
GROUP BY FirstName, LastName, S.Num
ORDER BY as AvgNote Desc
Also, float as a data type when dealing with grades is a bad idea. Float is imprecise by design to support a smaller datastorage footprint. This doesn't matter when you're dealing with scientific notation and precision isn't necessary, but in this case I think decimal would be a better choice.
This gets both the note for each module for each student, and their average across all modules
select s1.FirstName, s1.LastName, m2.name as module_name, n3.Note, x1.av_note
from student s1
left join notation n3
on n3.stud = s1.num
left join module m2
on m2.code = n3.mod
left join
(
select stud, avg(note) as av_note
from notation
group by stud
) x1
on s1.num = x1.stud
order by av_note, lastname desc
Here is my solution:
select tab.num,tab.firstName, sum(noteMultiCoef)/sum(coefficient) as average from
(select st.num,st.firstName, coefficient, note*coefficient as noteMultiCoef
from notation nt, student st, Module md
where st.num= nt.stud
and nt.code = md.mod) tab
group by tab.num, tab.firstName
order by average desc

Join logic from two separate tables in sql

We returned a list of cardID's after a query and those cardID's belong to two tables Student and Personnel. So how can I join those cardID's with Student and Personnel so I can return a table that shows name of Student and Personnel according to cardID's?
Personnel table:
PERSONNELID NUMBER(9,0)
PERSONNELNAME VARCHAR2(20)
PERSONNELSURNAME VARCHAR2(20)
PERSONNELJOB VARCHAR2(40)
PERSONNELCARDID NUMBER(4,0)
Student table:
STUDENTID NUMBER(9,0)
STUDENTNAME VARCHAR2(20)
STUDENTSURNAME VARCHAR2(20)
STUDENTDEPT VARCHAR2(40)
STUDENTFACULTY VARCHAR2(20)
STUDENTCARDID NUMBER(4,0)
CardID table
CARDID NUMBER(4,0)
USERTYPE VARCHAR2(20)
CHARGE NUMBER(3,2)
CREDIT NUMBER(4,2)
PaymentDevice table:
ORDERNO NUMBER
PAYDEVIP NUMBER(8,0)
PAYDEVDATE DATE No
PAYDEVTIME VARCHAR2(8)
CHARGEDCARDID NUMBER(9,0)
MEALTYPE VARCHAR2(10)
I tried to return first 10 person's name and surname that eat at cafeteria on 27/12/2012
SELECT C.CARDID
FROM CARD C, PAYMENTDEVICE P
WHERE P.ORDERNO
BETWEEN (SELECT MIN(ORDERNO)
FROM PAYMENTDEVICE
WHERE PAYDEVDATE='27/12/2012') AND (SELECT MIN(ORDERNO)
FROM PAYMENTDEVICE
WHERE PAYDEVDATE='27/12/2012')+10 AND C.CARDID=P.CHARGEDCARDID;
Our orderNo isn't reset everyday but keeps increasing so we found the min orderNo that day and add 10 to this value to find first 10 person who eat on that day between those order numbers.
So what return from this query:
CARDID
1005
1000
1002
1003
1009
2000
2001
1007
2002
1004
1006
and those some of those cardId (start with 1) are studentCardId and some of them (starts with 2) are PersonnelCardId. So how can I match and write names accordingly?
SELECT *
FROM Personel p INNER JOIN Student s
ON p.PersonnelCardId = s.StudentCardId
INNER JOIN ReturnedQuery rq
ON rq.CardId = p.PersonnelCardId
updated:
SELECT p.PersonnelName, rq.CardId
FROM Personel p INNER JOIN ReturnedQuery rq
ON rq.CardId = p.PersonnelCardId
UNION
SELECT s.StudentName, rq.Cardid
FROM Student s INNER JOIN ReturnedQuery rq
ON s.StudentCardId = rq.Cardid
Your original query is actually pretty fragile. I'd rewrite it like so (and added the needed joins):
WITH First_Daily_Purchase as (SELECT chargedCardId,
MIN(payDevTime) as payDevTime,
MIN(orderNo) as orderNo
FROM PaymentDevice
WHERE payDevDate >=
TO_DATE('2012-12-27', 'YYYY-MM-DD')
AND payDevDate <
TO_DATE('2012-12-28', 'YYYY-MM-DD')
GROUP BY chargedCardId),
First_10_Daily_Purchasers as (SELECT chargedCardId
FROM (SELECT chargedCardId,
RANK() OVER(ORDER BY payDevTime,
orderNo) as rank
FROM First_Daily_Purchase) a
WHERE a.rank < 11)
SELECT a.chargedCardId, b.personnelName, b.personnelSurname
FROM First_10_Daily_Purchasers a
JOIN Personnel b
ON b.personnelCardId = a.chargedCardId
UNION ALL
SELECT a.chargedCardId, b.studentName, b.studentSurname
FROM First_10_Daily_Purchasers a
JOIN Student b
ON b.studentCardId = a.chargedCardId
(Have a working SQL Fiddle - generally bullet-proofing this took me a while.)
This should get you the first 10 people who made a purchase (not the first 11 purchases, which is what you were actually getting). This of course assumes that payDevTime is actually stored in a sortable format (if it isn't you have bigger problems than this query not working quite right).
That said, there's a number of troubling things about your schema design.

rewards the products qualify for

products purchased
--------------------------
bana
bana
bana
stra
kiwi
reward requirements table (related to a rewards table)
reward id, products
----------------------
1,bana
1,bana
1,bana
2,stra
2,bana
3,stra
4,cart
5,bana
5,bana
5,oliv
Can you help me with sql to get rewards
the products purchased qualifies for?
In the case above the reward ids would be:
1
2
3
If there is a better design that would make the solution easier I welcome those as well. I'm using product names for the sake of easier explaining, I hope. (I'll replace with product ids later)
This query will solve your problem.
select r.reward_id
from (
select reward_id, product, count(*) needed
from reward_requirements
group by reward_id, product
) r
left join (
select product, count(*) bought
from products_purchased
group by product
) p on r.product=p.product and p.bought >= r.needed
group by r.reward_id
having count(reward_id) = count(distinct p.product)
order by r.reward_id
To make your design better, you could redo the reward_requirements to have the columns (product, needed) instead of having to list it multiple times. It will also get rid of the first subquery.
With this schema:
CREATE TABLE product
(
product_id int IDENTITY
, name varchar(50)
)
CREATE TABLE requirement
(
requirement_id int IDENTITY
, product_id int
, quantity int
, reward_id int
)
CREATE TABLE reward
(
reward_id int IDENTITY
, reward varchar(50)
)
CREATE TABLE purchased
(
purchased_id int IDENTITY
, product_id int
, quantity int
)
Your query becomes:
SELECT requirement.reward_id
FROM requirement
LEFT JOIN purchased
ON purchased.product_id = requirement.product_id
AND purchased.quantity >= requirement.quantity
GROUP BY requirement.reward_id
HAVING COUNT(purchased.product_id) = COUNT(requirement.reward_id);
Here's a SQLFiddle to play around with: http://sqlfiddle.com/#!3/e93c9/7