Combine two identifiers in PostgreSQL - database

I am creating a database in postgreSQL
I have two tables, one containing details about a house and another for each room in the house. The house_id is set up using house_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY
I want the room_id to be 1, 2, .. n for n rooms in the house. Which starts at 1 again for a new house_id.
This mean I have to combine these two identifiers somehow, and create a sequence for room_id which only counts upwards for a new house_id.
Is this possible in postgreSQL? Or should I settle for a room_id integer where I basically check the max room_id for each house_id and add one to it to form the
new room_id

Only deal with insert operation. Since update can be update to any integer value. delete can also delete any value.
I enforce the not null constraint. Otherwise we need to deal with null case.
demo
all raise notice is for debugging.
one special case, when table room have zero row, then assign room_id value to 1 and set the sequence nextval to 2.
if house_id is new value then set the room_id to 1 and set the sequence nextval to 2.
CREATE OR REPLACE FUNCTION restart_seq ()
RETURNS TRIGGER
AS $$
BEGIN
RAISE NOTICE 'new.house_id: %', NEW.house_id;
RAISE NOTICE 'all.house_id: %', (
SELECT
array_agg(house_id)
FROM
room);
RAISE NOTICE 'new.house_id already exists: %', (
SELECT
NEW.house_id IN (
SELECT
house_id
FROM
room));
IF (NEW.house_id IN (
SELECT
house_id
FROM
room)) IS FALSE THEN
NEW.room_id = 1;
ALTER SEQUENCE room_room_id_seq
RESTART WITH 2;
RAISE NOTICE 'currval(''room_room_id_seq''): %', currval('room_room_id_seq');
RETURN new;
END IF;
IF (
SELECT
count(house_id)
FROM
room) = 0 THEN
NEW.room_id = 1;
ALTER SEQUENCE room_room_id_seq
RESTART WITH 2;
RETURN new;
END IF;
RETURN new;
END;
$$
LANGUAGE plpgsql;

Related

MS SQL How to field add auto increment dependent on another field

For example, there is a table
int type
int number
int value
How to make that when inserting a value into a table
indexing started from 1 for different types.
type 1 => number 1,2,3...
type 2 => number 1,2,3...
That is, it will look like this.
type
number
value
1
1
-
1
2
-
1
3
-
2
1
-
1
4
-
2
2
-
3
1
-
6
1
-
1
5
-
2
3
-
6
2
-
Special thanks to #Larnu.
As a result, in my case, the best solution would be to create a table for each type.
As I mentioned in the comments, neither IDENTITY nor SEQUENCE support the use of another column to denote what "identity set" they should use. You can have multiple SEQUENCEs which you could use for a single table, however, this doesn't scale. If you are specific limited to 2 or 3 types, for example, you might choose to create 3 SEQUENCE objects, and then use a stored procedure to handle your INSERT statements. Then, when a user/application wants to INSERT data, they call the procedure and that procedure has logic to use the SEQUENCE based on the value of the parameter for the type column.
As mentioned, however, this doesn't scale well. If you have an undeterminate number of values of type then you can't easily handle getting the right SEQUENCE and handling new values for type would be difficult too. In this case, you would be better off using a IDENTITY and then a VIEW. The VIEW will use ROW_NUMBER to create your identifier, while IDENTITY gives you your always incrementing value.
CREATE TABLE dbo.YourTable (id int IDENTITY(1,1),
[type] int NOT NULL,
number int NULL,
[value] int NOT NULL);
GO
CREATE VIEW dbo.YourTableView AS
SELECT ROW_NUMBER() OVER (PARTITION BY [type] ORDER BY id ASC) AS Identifier,
[type],
number,
[value]
FROM dbo.YourTable;
Then, instead, you query the VIEW, not the TABLE.
If you need consistency of the column (I name identifier) you'll need to also ensure row(s) can't be DELETEd from the table. Most likely by adding an IsDeleted column to the table defined as a bit (with 0 for no deleted, and 1 for deleted), and then you can filter to those rows in the VIEW:
CREATE VIEW dbo.YourTableView AS
WITH CTE AS(
SELECT id,
ROW_NUMBER() OVER (PARTITION BY [type] ORDER BY id ASC) AS Identifier,
[type],
number,
[value],
IsDeleted
FROM dbo.YourTable)
SELECT id,
Identifier,
[type],
number,
[value]
FROM CTE
WHERE IsDeleted = 0;
You could, if you wanted, even handle the DELETEs on the VIEW (the INSERT and UPDATEs would be handled implicitly, as it's an updatable VIEW):
CREATE TRIGGER trg_YourTableView_Delete ON dbo.YourTableView
INSTEAD OF DELETE AS
BEGIN
SET NOCOUNT ON;
UPDATE YT
SET IsDeleted = 1
FROM dbo.YourTable YT
JOIN deleted d ON d.id = YT.id;
END;
GO
db<>fiddle
For completion, if you wanted to use different SEQUENCE object, it would look like this. Notice that this does not scale easily. I have to CREATE a SEQUENCE for every value of Type. As such, for a small, and known, range of values this would be a solution, but if you are going to end up with more value for type or already have a large range, this ends up not being feasible pretty quickly:
CREATE TABLE dbo.YourTable (identifier int NOT NULL,
[type] int NOT NULL,
number int NULL,
[value] int NOT NULL);
CREATE SEQUENCE dbo.YourTable_Type1
START WITH 1 INCREMENT BY 1;
CREATE SEQUENCE dbo.YourTable_Type2
START WITH 1 INCREMENT BY 1;
CREATE SEQUENCE dbo.YourTable_Type3
START WITH 1 INCREMENT BY 1;
GO
CREATE PROC dbo.Insert_YourTable #Type int, #Number int = NULL, #Value int AS
BEGIN
DECLARE #Identifier int;
IF #Type = 1
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type1;
IF #Type = 2
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type2;
IF #Type = 3
SELECT #Identifier = NEXT VALUE FOR dbo.YourTable_Type3;
INSERT INTO dbo.YourTable (identifier,[type],number,[value])
VALUES(#Identifier, #Type, #Number, #Value);
END;

IIF with Multiple Case Statements for Computed Column

I'm trying to add this as a Formula (Computed Column) but I'm getting an error message saying it is not valid.
Can anyone see what is wrong with the below formula?
IIF
(
select * from Config where Property = 'AutomaticExpiry' and Value = 1,
case when [ExpiryDate] IS NULL OR sysdatetimeoffset()<[ExpiryDate] then 1 else 0 end,
case when [ExpiryDate] IS NULL then 1 else 0 end
)
From BOL: ALTER TABLE computed_column_definition
computed_column_expression Is an expression that defines the value of
a computed column. A computed column is a virtual column that is not
physically stored in the table but is computed from an expression that
uses other columns in the same table. For example, a computed column
could have the definition: cost AS price * qty. The expression can be
a noncomputed column name, constant, function, variable, and any
combination of these connected by one or more operators. The
expression cannot be a subquery or include an alias data type.
Wrap the login in function. Something like this:
CREATE FUNCTION [dbo].[fn_CustomFunction]
(
#ExpireDate DATETIME2
)
RETURNS BIT
AS
BEGIN;
DECLARE #Value BIT = 0;
IF EXISTS(select * from Config where Property = 'AutomaticExpiry' and Value = 1)
BEGIN;
SET #Value = IIF (sysdatetimeoffset()< #ExpireDate, 1, 0)
RETURN #value;
END;
RETURN IIF(#ExpireDate IS NULL, 1, 0);
END;
GO
--DROP TABLE IF EXISTS dbo.TEST;
CREATE TABLE dbo.TEST
(
[ID] INT IDENTITY(1,1)
,[ExpireDate] DATETIME2
,ComputeColumn AS [dbo].[fn_CustomFunction] ([ExpireDate])
)
GO
INSERT INTO dbo.TEst (ExpireDate)
VALUES ('2019-01-01')
,('2018-01-01')
,(NULL);
SELECT *
FROM dbo.Test;
Youre trying to do something, what we're not quite sure - you've made a classic XY problem mistake.. You have some task, like "implement auto login expiry if it's on in the prefs table" and you've devised this broken solution (use a computed column/IIF) and have sought help to know why it's broken.. It's not solving the actual core problem.
In transitioning from your current state to one where you're solving the problem, you can consider:
As a view:
CREATE VIEW yourtable_withexpiry AS
SELECT
*,
CASE WHEN [ExpiryDate] IS NULL OR config.[Value] = 1 AND SysDateTimeOffset() < [ExpiryDate] THEN 1 ELSE 0 END AS IsValid
FROM
yourtable
LEFT JOIN
config
ON config.property = 'AutomaticExpiry'
As a trigger:
CREATE TRIGGER trg_withexpiry ON yourtable
AFTER INSERT OR UPDATE
AS
IF NOT EXISTS(select * from Config where Property = 'AutomaticExpiry' and Value = 1)
RETURN;
UPDATE yourtable SET [ExpiryDate] = DATE_ADD(..some current time and suitable offset here..)
FROM yourtable y INNER JOIN inserted i ON y.pk = i.pk;
END;
But honestly, you should be doing this in your front end app. It should be responsible for reading/writing session data and keeping things up to date and kicking users out if they're over time etc.. Using the database for this is, to a large extent, putting business logic/decision processing into a system that shouldn't be concerned with it..
Have your front end language implement a code that looks up user info upon some regular event (like page navigation or other activity) and refreshes the expiry date as a consequence of the activity, only if the expiry date isn't passed. For sure too keep the thing valid if the expiry is set to null if you want a way to have people active forever (or whatever)

trigger for insert or update after checking relationship

I have this 3 tables:
And i need to build a trigger that: A date ("encontro") can only works when theres a friendship ("amizade") between 2 profiles ("perfis").
I've created this trigger but i feel lost.. HELP ME
CREATE TRIGGER relaƧoes_after_insert
ON encontros
INSTEAD OF insert -
as
begin
declare #idperfilA int;
declare #idperfilB int;
declare #data datetime;
declare #count int;
declare cursor_1 cursor for select * from inserted;
open cursor_1;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
WHILE ##FETCH_STATUS = 0
BEGIN
if exists( select * from inserted i, amizade a
where i.IDPERFILA = a.IDPERFILA and i.IDPERFILB = a.IDPERFILB and GETDATE() > DATA)
RAISERROR('there isnt friendship', 16, 10);
else
insert into ENCONTROS select * from inserted;
end;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
END
close cursor_1;
deallocate cursor_1;
I think the better answer would be to not create use a trigger for this at all. Instead I would create and enforce a foreign key constraint between encontros and amizade.
As far as I can tell, this will result in doing what you want without having to write your own code to try and recreate behavior provided by the database. It also makes it much easier to understand from a database design point of view.
alter table dbo.encontros
add constraint fk_amizade__encontros
foreign key (idperflia, idperflib) references dbo.amizade (idperflia, idperflib)
/* optional
on delete { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
on update { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
--*/*
;
More about table constraints.
NO ACTION
The SQL Server Database Engine raises an error and the delete action on the row in the parent table is rolled back.
CASCADE
Corresponding rows are deleted from the referencing table if that row is deleted from the parent table.
SET NULL
All the values that make up the foreign key are set to NULL when the corresponding row in the parent table is deleted. For this constraint to execute, the foreign key columns must be nullable.
SET DEFAULT
All the values that comprise the foreign key are set to their default values when the corresponding row in the parent table is deleted. For this constraint to execute, all foreign key columns must have default definitions. If a column is nullable and there is no explicit default value set, NULL becomes the implicit default value of the column.
Based on your reply to #3N1GM4:
#3N1GM4 if exists some friendship with a date after today (for example) it is an error, so the friendship doesnt exist. But i dont know if it matters at this point. IDPERFILA and IDPERFILB will match A and B at amizade table, but i need to make sure that they were not the same
You could create a check constraint on amizade that will prevent rows with invalid dates from being inserted into the table.
alter table dbo.amizade
add constraint chk_data_lt_getdate ([data] < get_date());
More about check constraints; more examples from Gregory Larson.
original answer:
I'm still waiting on some clarification on the question, but one of the versions in this should be on the right path:
create trigger relaƧoes_after_insert
on encontros
instead of insert
as
begin
/* To abort when any row doesn't have a matching friendship */
if not exists (
select 1
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
begin;
raiserror('there isnt friendship', 16, 10);
else
insert into encontros
select * from inserted;
end;
end;
/* To insert all rows that have a matching friendship, you could use this instead */
insert into encontros
select i.*
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
end;
The only potential issue I see with using an inner join instead of exists for the second option (inserting rows that have a friendship and ignoring ones that don't) is if there could ever be an issue where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb) or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila) would return duplicates of the inserted rows from each condition returning a match.

Save sequence in special table or get last value?

Do you have advice or best practice or recommendation about sequence for identifiers?
I work on a database where all the identifier or document numbers are 'complex' sequence. For example the sequence for our invoices are INVCCC-2016-0000 where INV is fixed, CCC is the client reference, 2016 is the year and 0000 is a counter from 1 to 9999. This number must be unique and at this moment we keep it in a columns.
When I create a new invoice I need to check the last number created for this client this year then increment it of one then save my data in my database.
I see two way to do it
I create a special table that contain and maintain all last used number for each client. Each time I have a new invoice I check the number in this table, I increment it of one, I use this number to save my invoice then I update the sequence table. 1 READ, 1 INSERT, 1 UPDATE (may be an INSERT in sequence is new)
var keyType = "INV" + ClientPrefix + "-" + Year;
var keyValue = Context.SequenceTable.SingleOrDefault(y => y.KeyType == keyType).KeyValue;
I check the last number in my invoice table, I increment it then I save my invoice. 1 READ, 1 INSERT. I don't need tu update another table and this seems more logic to me. But my database administrator tell me this can create lock or other troubles.
var keyType = "INV" + ClientPrefix + "-" + Year;
var keyValue = Invoices.Where(y => y.InvoiceId.StartsWith(keyType)).OrderByDescending(y => y.InvoiceId).LastOrDefault();
Note I use SQL server before 2015 version and then before the SEQUENCE feature. I fear an inconsistency with solution 1. I fear performance issue with solution 2.
I suggest a table to maintain the last used value for the custom sequence. This method will also ensure there are no gaps, if that is a business requirement.
The example below uses a transactional stored procedure to avoid a race condition in the event an invoice number is generated concurrently for the same client. You'll need to change the CATCH block to use RAISERROR instead of THROW if you are using a pre SQL Server 2012 version.
CREATE TABLE InvoiceSequence (
ClientReferenceCode char(3) NOT NULL
, SequenceYear char(4) NOT NULL
, SequenceNumber smallint NOT NULL
CONSTRAINT PK_InvoiceSequence PRIMARY KEY (ClientReferenceCode, SequenceYear)
);
GO
CREATE PROC dbo.GetNextInvoiceSequence
#ClientReferenceCode char(3) = 'CCC'
, #SequenceYear char(4) = '2016'
AS
SET XACT_ABORT, NOCOUNT ON;
DECLARE #SequenceNumber smallint;
BEGIN TRY
BEGIN TRAN;
UPDATE dbo.InvoiceSequence
SET #SequenceNumber = SequenceNumber = SequenceNumber + 1
WHERE
ClientReferenceCode = #ClientReferenceCode
AND SequenceYear = #SequenceYear;
IF ##ROWCOUNT = 0
BEGIN
SET #SequenceNumber = 1;
INSERT INTO dbo.InvoiceSequence(ClientReferenceCode, SequenceYear, SequenceNumber)
VALUES (#ClientReferenceCode, #SequenceYear, #SequenceNumber);
END;
IF #SequenceNumber > 9999
BEGIN
RAISERROR('Invoice sequence limit reached for client %s year %s', 16, 1, #ClientReferenceCode, #SequenceYear) AS InvoiceNumber;
END;
COMMIT;
SELECT 'INV' + #ClientReferenceCode + '_' + #SequenceYear + '_' + RIGHT('000' + CAST(#SequenceNumber AS varchar(4)), 4);
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH;
GO
--sample usage:
--note that year could be assigned in the proc if it is based on the current date
EXEC dbo.GetNextInvoiceSequence
#ClientReferenceCode = 'CCC'
, #SequenceYear = '2016';

Postgres round robin id sequence

Say I have table:
CREATE TABLE custom_sequence (
name TEXT NOT NULL,
number INTEGER DEFAULT NULL,
is_expired BOOLEAN DEFAULT FALSE
);
And on insert I have to find first expired number and put it in new record.
For example:
"a" 1 FALSE
"b" 2 TRUE
"c" 3 FALSE
insert new:
"c" 2 FALSE
I can do that using TRIGGER:
CREATE OR REPLACE FUNCTION write_custom_number()
RETURNS TRIGGER AS
$$
DECLARE
next_number INTEGER;
BEGIN
SELECT
CASE
WHEN count(*) > 0 THEN min(number)
WHEN count(*) = 0 THEN
CASE
WHEN (SELECT
count(*)
FROM custom_sequence) > 0
THEN (SELECT
count(*)
FROM custom_sequence) + 1
WHEN (SELECT
count(*)
FROM custom_sequence) = 0
THEN
1
END
END
INTO next_number
FROM custom_sequence
WHERE is_expired = TRUE;
IF next_number IS NULL
THEN
next_number = 1;
END IF;
NEW.number := next_number;
RETURN NEW;
END
$$
LANGUAGE plpgsql;
CREATE TRIGGER write_custom_number
BEFORE INSERT ON custom_sequence FOR EACH ROW
EXECUTE PROCEDURE write_custom_number();
But then I'll need to lock table for every INSERT! Is there any better way to solve this problem?
No, there's no workaround. It's inherent to what you want. You can't reuse the first expired number and still have concurrency, since someone else might take it and insert it before you commit otherwise.
To make this concurrent you'd need some kind of global sequence manager that could keep track of IDs it'd already handed out and do dirty reads of the table to see its state. There's nothing like that in the database engine and you can't do it at the SQL level. It'd also be extremely hard to get right.
So no. If you want to re-use IDs, you have to do your transactions serially.

Resources