SQL and Access calculating a sum value based on a variable field. - sql

Applogies if you saw my previous post. It was 'closed' for being open ended.
What i have is a three table system for Cars Salesman and Customers. This is mimicking a very simple Car sales system.
Within the Car Table is the following fields:
Registation:
Make:
Model:
Date of Purchase:
Customer ID:
Salesman ID:
Date Sold:
Date bought:
Price:
or each sale, made by a salesperson, they will earn a commission based on the following table:
Car Price(£) % Commission
0-5000 5
5000-10000 6
10000-15000 7
15000+ 8
The car is always sold for the price specified in the car details table.
Do I commission rate somewhere within the table (and automatically calculate this)r as a separate table.
If I am going to store this within the Car Table, how would I get the database to automatically calculate the commision and put it in the table, so that when the car is sold ?
Thanks

the simplest solution, programmatically speaking, would be to create a table COMMISSION with 4 fields: Id_Commission, Nr_Commission_Min_Value, Nr_Commission_Max_Value, Nr_Commission_Percent
you would then call a select statement like (tested and working in mysql, whose tsql is very, very similar to msaccess's):
SELECT DISTINCT Id_Car, Car.Ds_Car_Description, Car.Nr_Car_Price, Commission.Nr_Commission_Percent * Car.Nr_Car_Price / 100 As Commission_Payable FROM Car LEFT JOIN Commission ON (Car.Nr_Car_Price BETWEEN Commission.Nr_Commission_Min_Value AND Commission.Nr_Commission_Max_Value) WHERE Car.Ic_Car_Sold = 1 ORDER BY Car.Nr_Car_Price DESC
below, the sql for creating and populating these tables.
CREATE TABLE IF NOT EXISTS Commission (
Id_Commision int(11) NOT NULL,
Nr_Commission_Min_Value double NOT NULL,
Nr_Commission_Max_Value double NOT NULL,
Nr_Commission_Percent double NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
INSERT INTO Commission (Id_Commision, Nr_Commission_Min_Value, Nr_Commission_Max_Value, Nr_Commission_Percent) VALUES
(0, 0, 5000, 5),
(1, 5001, 10000, 6),
(2, 10001, 15000, 7),
(3, 15001, 1000000000000, 8);
CREATE TABLE IF NOT EXISTS Car (
Id_Car int(11) NOT NULL AUTO_INCREMENT,
Ds_Car_Description varchar(20) COLLATE utf8_bin NOT NULL,
Nr_Car_Price double NOT NULL,
Ic_Car_Sold tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (Id_Car)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin AUTO_INCREMENT=7 ;
INSERT INTO Car (Id_Car, Ds_Car_Description, Nr_Car_Price, Ic_Car_Sold) VALUES
(1, 'Lotus', 20000, 1),
(2, 'Renault 5', 1000, 1),
(3, 'Audi A3', 6000, 1),
(4, 'Fiat Mille', 300, 1),
(5, 'Land Rover', 12000, 0),
(6, 'Lotus', 25000, 1);
in naming fields, i use Id for main index, Nr for numbers, Ds for strings and Ic for booleans. a standard i picked up designing dbs for Banco do Brasil.

Related

CREATE FUNCTION for attribute to at most 3 people?

I am currently doing a hotel booking application on SQL Server 2018, and am trying to write a constraint for the RoomNo attribute of my SQL Server table. Essentially, I want each RoomNo to only be able to have at most 3 person, but ran into an error when trying to do the CREATE FUNCTION.
This are my current code:
CREATE TABLE Passenger
(
ID smallint ,
Name varchar (50) NOT NULL,
Email varchar (319) NULL,
DOB smalldatetime NOT NULL,
Gender char (1) NOT NULL CHECK (Gender IN ('M', 'F')),
RoomNo tinyint NOT NULL,
CONSTRAINT PK_Passenger PRIMARY KEY NONCLUSTERED (ID),
CONSTRAINT CHK_Passenger_Gender CHECK (Gender IN ('M', 'F'))
)
CREATE FUNCTION CalculateRoomNo
(
#value tinyint
)
RETURNS bit
AS
BEGIN
IF (SELECT COUNT(RoomNo) FROM Passenger GROUP BY RoomNo) <= 3
RETURN 0
RETURN 1
END
GO
ALTER TABLE Passenger
ADD CONSTRAINT CHK_RoomNoPax CHECK (dbo.CalculateRoomNo(RoomNo) = 0)
GO
When I add a passenger into the table, if it is formatted like this:
INSERT INTO Passenger
VALUES (1, 'Rob', 'Rob#gmail.com', '2017-10-04', 'M', 12)
INSERT INTO Passenger
VALUES (2, 'Darren', 'Darren#yahoo,com', '1976-12-21', 'F', 12)
INSERT INTO Passenger
VALUES (3, 'Peggy', '', '2006-03-15', 'F', 12)
INSERT INTO Passenger
VALUES (4, 'Carlos', '', '1981-04-06', 'F', 12)
It will stop at
INSERT INTO Passenger VALUES (3, 'Peggy', '', '2006-03-15', 'F', 12)
since RoomNo '12' has reached its maximum capacity.
But, if I added the values like such where the room numbers are different from each other:
INSERT INTO Passenger
VALUES (1, 'Rob', 'Rob#gmail.com', '2017-10-04', 'M', 69)
INSERT INTO Passenger
VALUES (2, 'Darren', 'Darren#yahoo,com', '1976-12-21', 'F', 74)
INSERT INTO Passenger
VALUES (3, 'Peggy', '', '2006-03-15', 'F', 45)
INSERT INTO Passenger
VALUES (4, 'Carlos', '', '1981-04-06', 'F', 72)
INSERT INTO Passenger
VALUES (5, 'John', 'johnny#hotmail.com', '1988-05-06', 'M', 69)
It will return an error:
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
Is there any way I can properly run this SQL?
The query with the GROUP BY can return more than 1 record if there's more than 1 RoomNo.
If you include a WHERE clause for the RoomNo then it can only be 1 COUNT
CREATE FUNCTION CalculateRoomNo
(
#RoomNo tinyint
)
RETURNS bit
AS
BEGIN
IF (SELECT COUNT(*) FROM Passenger WHERE RoomNo = #RoomNo) <= 3
RETURN 0
RETURN 1
END
Demo on db<>fiddle here
As mentioned by #LukStorms, your query has no filter on RoomNo, therefore it can return multiple rows. A scalar subquery must return a maximum of one row.
But the most correct way to achieve what you are trying to do, is not to use this function at all. Instead you can add another column, and create a unique constraint across that and the RoomNo
ALTER TABLE Passenger
ADD RoomNoPax tinyint NOT NULL
CONSTRAINT CHK_RoomNoPax CHECK (RoomNoPax >= 1 AND RoomNoPax <= 3);
ALTER TABLE Passenger
ADD CONSTRAINT UQ_RoomNo_RoomNoPax UNIQUE (RoomNo, RoomNoPax);
db<>fiddle
You now have an extra column which must have the value 1, 2 or 3. And there is a unique constraint over every pair of that value and the RoomNo, so you cannot now put more than 3 Passenger in each RoomNo.
You need to change the logic in the function.
You should be checking for the number of records for any given roomno, not the number of roomno in the entire table.
the subquery should return only one scalar value because you are performing a logical operation.
i.e.
if exists (select 1 from Passenger group by roomno having count(1) <= 3)
begin
return 1
end
else
begin
return 0
end
In the above query, we are checking for the number of persons assigned to each room number and if there is an existence of such case then it will return 1. In this case, it will not return more than one record.
Please modify the return value as per your requirement.
Please upvote if you find this answer useful

SQL Count returns wrong value in my INNER JOIN Statement

I am quite new to SQL and I am having some difficulty with my COUNT() feature.
It keeps returning the wrong COUNT() value as I am trying to calculate the TOTAL number of specific cars sold by users. My tables and results are here:
http://sqlfiddle.com/#!9/d2ef0/5
My Schema:
CREATE TABLE IF NOT EXISTS Users(
userID INT NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
forename VARCHAR(50) NOT NULL,
surname VARCHAR(50) NOT NULL,
PRIMARY KEY (userID)
);
CREATE TABLE IF NOT EXISTS CarType(
carTypeID INT NOT NULL AUTO_INCREMENT,
description VARCHAR(80),
PRIMARY KEY (carTypeID)
);
CREATE TABLE IF NOT EXISTS Country(
countryID INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100),
PRIMARY KEY (countryID)
);
CREATE TABLE IF NOT EXISTS Cars(
carID INT NOT NULL AUTO_INCREMENT,
carTypeID INT NOT NULL,
countryID INT NOT NULL,
description VARCHAR(100) NOT NULL,
make VARCHAR(100) NOT NULL,
model VARCHAR(100),
FOREIGN KEY (carTypeID) REFERENCES CarType(carTypeID),
FOREIGN KEY (countryID) REFERENCES Country(countryID),
PRIMARY KEY (carID)
);
CREATE TABLE IF NOT EXISTS Likes(
userID INT NOT NULL,
carID INT NOT NULL,
likes DOUBLE NOT NULL,
FOREIGN KEY (userID) REFERENCES Users(userID),
FOREIGN KEY (carID) REFERENCES Cars(carID)
);
CREATE TABLE IF NOT EXISTS Sold(
userID INT NOT NULL,
carID INT NOT NULL,
FOREIGN KEY (userID) REFERENCES Users(userID),
FOREIGN KEY (carID) REFERENCES Cars(carID)
);
INSERT INTO Users VALUES
(NULL, "micheal", "Micheal", "Sco"),
(NULL, "bensco", "Ben", "Sco"),
(NULL, "shanemill", "Shane", "Miller");
INSERT INTO CarType VALUES
(NULL, "Saloon"),
(NULL, "HatchBack"),
(NULL, "Low Rider");
INSERT INTO Country VALUES
(NULL, "UK"),
(NULL, "USA"),
(NULL, "JAPAN"),
(NULL, "GERMANY");
INSERT INTO Cars VALUES
(NULL, 1, 2, "Ford Mustang lovers", "Mustang", "Ford"),
(NULL, 2, 3, "Drift Kings", "Skyline", "Nissan"),
(NULL, 3, 1, "British classic", "Cooper", "Mini");
INSERT INTO Likes VALUES
(1, 1, 3),
(1, 2, 2),
(2, 3, 5),
(2, 3, 7),
(2, 3, 1),
(2, 3, 2);
INSERT INTO Sold VALUES
(1, 2),
(1, 3),
(1, 1),
(2, 2),
(2, 3),
(3, 1),
(3, 3);
This is the Sold table:
userID carID
1 2
1 3
1 1
2 2
2 3
3 1
3 3
This is my complex query:
SELECT DISTINCT Cars.carID, Cars.description, Cars.model, Country.name,
CarType.description, ROUND(AVG(Likes.likes)), COUNT(*)
FROM Cars
INNER JOIN Sold ON
Cars.carID = Sold.carID
INNER JOIN Country ON
Cars.countryID = Country.countryID
INNER JOIN CarType ON
Cars.carTypeID = CarType.carTypeID
INNER JOIN Likes ON
Cars.carID = Likes.carID
GROUP BY Cars.carID
The actual result from this complex SQL Query:
carID description model name description ROUND(AVG(Likes.likes)) COUNT(*)
1 Ford Mustang lovers Ford USA Saloon 3 2
2 Drift Kings Nissan JAPAN HatchBack 2 2
3 British classic Mini UK Low Rider 4 12
For example, the result for the last one is incorrect - it should not be 12
Would be nice if someone could tell me where I went wrong
Thanks
You are attempting to aggregate across two different dimensions -- Sold and Likes. The result is a Cartesian product of rows for each car, and this throws off the aggregations.
The solution is the pre-aggregate the results along each dimension:
SELECT c.carID, c.description, c.model, cy.name, ct.description,
l.avgLikes, s.NumSold
FROM Cars c INNER JOIN
(SELECT s.CarId, COUNT(*) as NumSold
FROM Sold s
GROUP BY s.CarId
) s
ON c.carID = s.carID INNER JOIN
Country cy
ON c.countryID = cy.countryID INNER JOIN
CarType ct
ON c.carTypeID = ct.carTypeID LEFT JOIN
(SELECT l.carId, AVG(Likes) as avgLikes
FROM Likes l
GROUP BY CarId
) l
ON c.carID = l.carID;
Here is the SQL Fiddle.
If all you want is the total number of specific cars sold by user then all your info is in the sold table. This query will give you what you want by carID. You can use that as a subquery if you want to join on other tables to get more info.
SELECT userID, carID, count(*) as totalSold FROM Sold GROUP BY userID, carID;

How to create an "on-the-fly" mapping table within a SELECT statement in Postgresql

I'm creating a select statement that combines two tables, zone and output,
based on a referenced device table and on a mapping of zone_number to output_type_id.
The mapping of zone_number to output_type_id doesn't appear
anywhere in the database, and I would like to create it "on-the-fly" within the select
statement. Below is my schema:
CREATE TABLE output_type (
id INTEGER NOT NULL,
name TEXT,
PRIMARY KEY (id)
);
CREATE TABLE device (
id INTEGER NOT NULL,
name TEXT,
PRIMARY KEY (id)
);
CREATE TABLE zone (
id SERIAL NOT NULL,
device_id INTEGER NOT NULL REFERENCES device(id),
zone_number INTEGER NOT NULL,
PRIMARY KEY (id),
UNIQUE (zone_number)
);
CREATE TABLE output (
id SERIAL NOT NULL,
device_id INTEGER NOT NULL REFERENCES device(id),
output_type_id INTEGER NOT NULL REFERENCES output_type(id),
enabled BOOLEAN NOT NULL,
PRIMARY KEY (id)
);
And here is some example data:
INSERT INTO output_type (id, name) VALUES
(101, 'Output 1'),
(202, 'Output 2'),
(303, 'Output 3'),
(404, 'Output 4');
INSERT INTO device (id, name) VALUES
(1, 'Test Device');
INSERT INTO zone (device_id, zone_number) VALUES
(1, 1),
(1, 2),
(1, 3),
(1, 4);
INSERT INTO output (device_id, output_type_id, enabled) VALUES
(1, 101, TRUE),
(1, 202, FALSE),
(1, 303, FALSE),
(1, 404, TRUE);
I need to get the associated enabled field from the output table for each zone for a given device.
Each zone_number maps to an output_type_id. For this example:
zone_number | output_type_id
----------------------------
1 | 101
2 | 202
3 | 303
4 | 404
One way to handle the mapping would be to create a new table
CREATE TABLE zone_output_type_map (
zone_number INTEGER,
output_type_id INTEGER NOT NULL REFERENCES output_type(id)
);
INSERT INTO zone_output_type_map (zone_number, output_type_id) VALUES
(1, 101),
(2, 202),
(3, 303),
(4, 404);
And use the following SQL to get all zones, plus the enabled flag, for device 1:
SELECT zone.*, output.enabled
FROM zone
JOIN output
ON output.device_id = zone.device_id
JOIN zone_output_type_map map
ON map.zone_number = zone.zone_number
AND map.output_type_id = output.output_type_id
AND zone.device_id = 1
However, I'm looking for a way to create the mapping of zone nunbers to output
types without creating a new table and without piecing together a bunch of AND/OR
statements. Is there an elegant way to create a mapping between the two fields
within the select statement? Something like:
SELECT zone.*, output.enabled
FROM zone
JOIN output
ON output.device_id = zone.device_id
JOIN (
SELECT (
1 => 101,
2 => 202,
3 => 303,
4 => 404
) (zone_number, output_type_id)
) as map
ON map.zone_number = zone.zone_number
AND map.output_type_id = output.output_type_id
AND zone.device_id = 1
Disclaimer: I know that ideally the enabled field would exist in the zone
table. However, I don't have control over that piece. I'm just looking for the
most elegant solution from the application side. Thanks!
You can use VALUES as an inline table and JOIN to it, you just need to give it an alias and column names:
join (values (1, 101), (2, 202), (3, 303), (4, 304)) as map(zone_number, output_type_id)
on ...
From the fine manual:
VALUES can also be used where a sub-SELECT might be written, for
example in a FROM clause:
SELECT f.*
FROM films f, (VALUES('MGM', 'Horror'), ('UA', 'Sci-Fi')) AS t (studio, kind)
WHERE f.studio = t.studio AND f.kind = t.kind;
UPDATE employees SET salary = salary * v.increase
FROM (VALUES(1, 200000, 1.2), (2, 400000, 1.4)) AS v (depno, target, increase)
WHERE employees.depno = v.depno AND employees.sales >= v.target;
So just to complement the accepted answer, the following code is a valid, self-contained Postgresql expression which will evaluate to an 'inline' relation with columns (zone_number, output_type_id):
SELECT * FROM
(VALUES
(1, 101),
(2, 202),
(3, 303),
(4, 304)
) as i(zone_number, output_type_id)
(The (VALUES ... AS ...) part alone will not make a valid expression, which is why I added the SELECT * FROM.)
JOIN
(SELECT 1 zone_number, 101 as output_type_id
UNION ALL
SELECT 2 zone_number, 202 as output_type_id
UNION ALL
SELECT 3 zone_number, 303 as output_type_id
) mappings on mappings.zone_number = zone.zone_number

SQL Stored procedure to obtain top customers

I'm trying to create a stored procedure that goes through a "SALES" table and returns the best two customers of a pharmacy (the two customers who have spent more money).
Here's some code:
Table creation:
create table Customer (
Id_customer int identity(1,1) Primary Key,
Name varchar(30),
Address varchar(30),
DOB datetime,
ID_number int not null check (ID_number > 0),
Contributor int not null check (Contributor > 0),
Customer_number int not null check (Customer_number > 0)
)
create table Sale (
Id_sale int identity(1,1) Primary Key,
Id_customer int not null references Customer(Id_customer),
Sale_date datetime,
total_without_tax money,
total_with_tax money
)
Well, I don't know if this is useful but I have a function that returns the total amount spent by a customer as long as I provide the customer's ID.
Here it is:
CREATE FUNCTION [dbo].[fGetTotalSpent]
(
#Id_customer int
)
RETURNS money
AS
BEGIN
declare #total money
set #total = (select sum(total_with_tax) as 'Total Spent' from Sale where Id_customer=#Id_customer)
return #total
END
Can someone help me get the two top customers?
Thanks
Chiapa
PS: Here's some data to insert so you can test it better:
insert into customer values ('Jack', 'Big street', '1975.02.01', 123456789, 123456789, 2234567891)
insert into customer values ('Jim', 'Little street', '1985.02.01', 223456789, 223456789, 2234567891)
insert into customer values ('John', 'Large street', '1977.02.01', 323456789, 323456789, 3234567891)
insert into customer values ('Jenny', 'Huge street', '1979.02.01', 423456789, 423456789, 4234567891)
insert into sale values (1, '2013.04.30', null, 20)
insert into sale values (2, '2013.05.22', null, 10)
insert into sale values (3, '2013.03.29', null, 30)
insert into sale values (1, '2013.05.19', null, 34)
insert into sale values (1, '2013.06.04', null, 21)
insert into sale values (2, '2013.06.01', null, 10)
insert into sale values (2, '2013.05.08', null, 26)
You can do this with a single query without any special functions:
select top 2 c.id_customer, c.name, sum(s.total_with_tax)
from customer c
join sale s on c.id_customer = s.id_customer
group by c.id_customer, c.name
order by sum(s.total_with_tax) desc
This joins onto a CTE with the top customers.
Remove the WITH TIES option if you want exactly 2 and don't want to include customers tied with the same spend.
WITH Top2
AS (SELECT TOP 2 WITH TIES Id_customer,
SUM(total_with_tax) AS total_with_tax
FROM Sale
GROUP BY Id_customer
ORDER BY SUM(total_with_tax) DESC)
SELECT *
FROM Customer C
JOIN Top2 T
ON C.Id_customer = T.Id_customer
I'm not really into SQL Server dialect, but this one will give you best customers in descending order along with money they spent:
select Id_customer, total_with_tax from
(select Id_customer, sum(total_with_tax) total_with_tax from Sale group by Id_customer)
order by total_with_tax desc

Summing a field while excluding certain fields in SQL

I am trying to suma column in a table, while excluding certain records that have the paid field set to true.
I am doing so like this:
SELECT SUM( cost ) AS total
FROM sales
WHERE passport = 'xxxxx'
AND paid <>1
The table is full of data, and I can display costs by themselves, or the entire total. Just adding on
AND paid <>1
Is what causes it to fail. The query does not fail as such, but NULL is returned which is quite useless.
This is the SQL for my table
CREATE TABLE sales (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
uuid varchar(64) NOT NULL,
firstname varchar(64) NOT NULL DEFAULT '',
lastname varchar(64) NOT NULL DEFAULT '',
passport varchar(64) DEFAULT NULL,
product varchar(64) NOT NULL,
quantity int(11) DEFAULT NULL,
cost double DEFAULT NULL,
paymenttype varchar(64) NOT NULL DEFAULT '',
paid tinyint(1) DEFAULT NULL,
tabno varchar(64) NOT NULL,
createdby int(10) unsigned DEFAULT NULL,
creationdate datetime DEFAULT NULL,
modifiedby int(10) unsigned DEFAULT NULL,
modifieddate timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
)
And the current data
INSERT INTO sales (id, uuid, firstname, lastname, passport, product, quantity, cost, paymenttype, paid, tabno, createdby, creationdate, modifiedby, modifieddate) VALUES
(20, ':8dcee958-d1ac-6791-6253-0a7344054295', 'Jason', 'Hoff', 'r454545', 'Nicaraguan nachoes', 4, 320, 'credit', 1, '23434', 2, '2010-07-06 04:10:18', 2, '2010-07-06 04:10:18'),
(19, ':3f03cda5-21bf-9d8c-5eaa-664eb2d4f5a6', 'Jason', 'Hoff', 'r454545', 'Nica Libre (doble 4 o 5 anos)', 1, 30, 'cash', NULL, '35', 2, '2010-07-06 03:35:35', 2, '2010-07-06 03:35:35'),
(18, ':f83da33b-2238-94b9-897c-debed0c3815e', 'Jason', 'Hoff', 'r454545', 'Helado con salsa de chocolate', 1, 40, 'cash', 1, '2', 2, '2010-07-05 21:30:58', 2, '2010-07-05 21:30:58');
The 'paid' value is NULL for that row. You would need to do
SELECT SUM( cost ) AS total
FROM test.sales
WHERE passport = 'r454545'
AND paid IS NULL or paid = 0
/*Or paid <> 1 as I see you are using tinyint datatype*/
Or, better would be to not allow NULLS in that column and have paid default to 0.
Your problem is that your condition does not match any rows.
The condition paid <> 1 does not match the row where paid is NULL.
Try this query: SELECT 1 <> NULL It will return NULL. A WHERE clause filters out rows in which the clause is either false or NULL.
Replace AND paid <> 1 with AND (paid IS NULL OR paid <> 1)
The book SQL Antipatterns describes this problem in detail, section Searching Nullable Columns. Strongly recommended book.
SELECT SUM( cost ) AS total
FROM sales
WHERE passport = 'xxxxx'
AND paid = false or paid is null
EDITED
use
IS NOT
instead of
!=
This should work
SELECT SUM( cost ) AS total
FROM sales
WHERE passport = 'xxxxx'
AND paid IS NOT true
This may mean there are no rows in the table for which paid <> 1. Or maybe there are but they cost of NULL.
I would not use "<>", but use "!=" instead;
SELECT SUM( cost ) AS total
FROM sales
WHERE passport = 'xxxxx'
AND paid != 1
If that doesn't work, can you post the table definition? And are there any records which do not have paid = 1?