CREATE FUNCTION for attribute to at most 3 people? - sql

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

Related

"Invalid Column Name" even though the column is created?

I have created a table :
create table Praktikant(
Pr_id char not null Primary key,
Name varchar(100) not null,
Mbiemri varchar(100) not null,
Gender char(1) not null,
Degree varchar(100) not null,
age int,
check (age >18),
foreign key (Pr_id) references Doctor(Dr_id) on update cascade
)
Inserts:
insert into Praktikant values ('1', 'Kastriot', 'Tusha', 'M', 'Student', 19);
insert into Praktikant values ('2', 'Trim', 'Dalipi', 'M', 'Student', 23);
insert into Praktikant values ('3', 'Elira', 'Rrmoku', 'F', 'Studente', 24);
insert into Praktikant values ('4', 'Qendresa', 'Krasniqi', 'F', 'Studente',20);
Query:
select age
from Praktikant
where age > 23
I don't understand why I get this message:
Msg 207, Level 16, State 1, Line 1
Invalid column name 'age'.
Use this query to get table info:
SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'Praktikant'
then find your table schema because i suspect that you have two tables with the same name in another schema.
after find schema use this query for select info:
select age
from [schema_name].[Praktikant]
where age > 23
You can use this query to get table columns list to check:
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Praktikant'

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 and Access calculating a sum value based on a variable field.

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.

SQL query: Last but one rank for user

My table structure looks like this:
create table rankings (
id IDENTITY NOT NULL,
user_id INT NOT NULL,
game_poule_id INT NOT NULL,
rank INT NOT NULL,
insertDate DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (game_poule_id) REFERENCES game_poules(id) ON DELETE CASCADE
);
All old rankings of users per game are saved in this table. Now I want to have the last but one rank in the table for all users in a gamepoule.
Has someone an idea how to achive this? Thanks
You need to self join the table to get the records you require.
For this answer I created your table without the foreign keys as they are not required to get it to work.
CREATE TABLE Rankings (
id int IDENTITY NOT NULL,
user_id INT NOT NULL,
game_poule_id INT NOT NULL,
rank INT NOT NULL,
insertDate DATETIME NOT NULL
);
Insert some sample data. Without more information I cannot simulate any better than this.
INSERT Rankings(user_id,game_poule_id,rank,insertDate)
VALUES(1, 100, 3, CURRENT_TIMESTAMP-2)
INSERT Rankings(user_id,game_poule_id,rank,insertDate)
VALUES(1, 100, 2, CURRENT_TIMESTAMP-1)
INSERT Rankings(user_id,game_poule_id,rank,insertDate)
VALUES(1, 101, 6, CURRENT_TIMESTAMP)
INSERT Rankings(user_id,game_poule_id,rank,insertDate)
VALUES(2, 100, 5, CURRENT_TIMESTAMP-2)
INSERT Rankings(user_id,game_poule_id,rank,insertDate)
VALUES(2, 100, 1, CURRENT_TIMESTAMP-1)
INSERT Rankings(user_id,game_poule_id,rank,insertDate)
VALUES(2, 101, 2, CURRENT_TIMESTAMP)
Query for the last but one rank
SELECT Rankings.game_poule_id, Rankings.user_id, rank, MAX(Rankings.insertDate)
FROM Rankings INNER JOIN
(SELECT game_poule_id, user_id, MAX(insertDate) max_insert_date
FROM rankings
GROUP BY game_poule_id, user_id) Max_Ranking_Date
ON Rankings.user_id = Max_Ranking_Date.user_id
AND Rankings.insertDate < Max_Ranking_Date.max_insert_date
AND Rankings.game_poule_id = Max_Ranking_Date.game_poule_id
GROUP BY Rankings.game_poule_id, Rankings.user_id, rank
PLEASE NOTE!
As you can see from the results you will not get a ranking for a game that only has one row per user. But since you are asking for the "last but one" that only makes sense for games with multiple entries.
EDIT:
I've just realised the query I have provided will not return one row per user per game. Anyone want to fix it? I have to get on with some work :)
Another possible (not very nice) solution
SELECT
*
FROM
rankings r
WHERE
FK_gamePoule = 0 AND
r.insertDate = COALESCE(
(SELECT
r2.insertDate
FROM
rankings r2
WHERE
r.FK_user = r2.FK_user ORDER BY r2.insertDate DESC
LIMIT 1
OFFSET 1), '2048-12-31 23:59:59')

Database auto-increment column

Is it possible to create a Database which has 1 column (but not the column of primary key) to be auto-increment? So that when I insert value to the database, i don't need to fill in the value myself, and DB will fill in that value for that column for me (and increment every time I do a new insert)?
Thank you.
Yes, of course it is possible. Just make this column a unique key (not a primary key) and it has to be declared with a special attribute: "IDENTITY" for SQL Server, and
"AUTO_INCREMENT" for MySQL (see the example below) . And another column can be a primary key.
On MySQL database the table could be declared like this:
CREATE TABLE `mytable` (
`Name` VARCHAR(50) NOT NULL,
`My_autoincrement_column` INTEGER(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`Name`),
UNIQUE KEY `My_autoincrement_column` (`My_autoincrement_column`)
);
Yes, you can do this. Here is a sample for SQL Server using IDENTITY:
CREATE TABLE MyTable (
PrimaryKey varchar(10) PRIMARY KEY,
IdentityColumn int IDENTITY(1,1) NOT NULL,
DefaultColumn CHAR(1) NOT NULL DEFAULT ('N')
)
INSERT INTO MyTable (PrimaryKey) VALUES ('A')
INSERT INTO MyTable (PrimaryKey) VALUES ('B')
INSERT INTO MyTable (PrimaryKey, DefaultColumn) VALUES ('C', 'Y')
INSERT INTO MyTable (PrimaryKey, DefaultColumn) VALUES ('D', 'Y')
INSERT INTO MyTable (PrimaryKey, DefaultColumn) VALUES ('E', DEFAULT)
--INSERT INTO MyTable (PrimaryKey, DefaultColumn) VALUES ('F', NULL) -- ERROR
--> Cannot insert the value NULL into column 'DefaultColumn', table 'tempdb.dbo.MyTable'; column does not allow nulls. INSERT fails.
SELECT * FROM MyTable
Here is an example using SQL Server using functions to roll-your-own incrementing column. This is by means not fault tolerant or the way I would do it. (I'd use the identity feature.) However, it is good to know that you can use functions to return default values.
DROP TABLE MyTable
GO
DROP FUNCTION get_default_for_mytable
GO
CREATE FUNCTION get_default_for_mytable
()
RETURNS INT
AS
BEGIN
-- Declare the return variable here
DECLARE #ResultVar int
-- Add the T-SQL statements to compute the return value here
SET #ResultVar = COALESCE((SELECT MAX(HomeBrewedIdentityColumn) FROM MyTable),0) + 1
-- Return the result of the function
RETURN #ResultVar
END
GO
CREATE TABLE MyTable (
PrimaryKey varchar(10) PRIMARY KEY,
IdentityColumn int IDENTITY(1,1) NOT NULL,
DefaultColumn CHAR(1) NOT NULL DEFAULT ('N'),
HomeBrewedIdentityColumn int NOT NULL DEFAULT(dbo.get_default_for_mytable())
)
GO
INSERT INTO MyTable (PrimaryKey) VALUES ('A')
INSERT INTO MyTable (PrimaryKey) VALUES ('B')
INSERT INTO MyTable (PrimaryKey, DefaultColumn) VALUES ('C', 'Y')
INSERT INTO MyTable (PrimaryKey, DefaultColumn) VALUES ('D', 'Y')
INSERT INTO MyTable (PrimaryKey, DefaultColumn) VALUES ('E', DEFAULT)
--INSERT INTO MyTable (PrimaryKey, DefaultColumn) VALUES ('F', NULL) -- ERRROR
--> Cannot insert the value NULL into column 'DefaultColumn', table 'tempdb.dbo.MyTable'; column does not allow nulls. INSERT fails.
SELECT * FROM MyTable
Results
PrimaryKey IdentityColumn DefaultColumn HomeBrewedIdentityColumn
---------- -------------- ------------- ------------------------
A 1 N 1
B 2 N 2
C 3 Y 3
D 4 Y 4
E 5 N 5
I think you can have only 1 identity autoincrement column per table, this columns doesn't have to be the primary key but it would mean you have to insert the primary key yourself.
If you already have a primary key which is auto increment then I would try and use this if possible.
If you are trying to get an row ID to range on for querying then I would look at creating a view which has the row ID in it (not SQL 2000 or below).
Could you add in what your primary key is and what you intend to use the auto increment column for and it might help come up with a solution
On sql server this is called an identity column
Oracle and DB2 have sequence but I think you are looking for identity and all major dbms (mysql, sql server, db2, oracle) support it.