I'm fairly new to SQL triggers and I'm struggling whit this:
I have a table that stores data about properties and their prices:
id_property price area price_m2
1 1500 60 25
2 3500 85 41
The clients could change the price of their property often, but the area won't. So I want to create a trigger that updates the price_m2 column when the price column is updated.
I've tried something like that or similar variations:
First create the function
CREATE FUNCTION update_precio_m2() RETURNS TRIGGER
AS
$$
BEGIN
update my_table
set price_m2 = new.price/old.area
where (old.id = new.id);
RETURN new;
end
$$
language plpgsql;
Then create the trigger
CREATE TRIGGER update_price_m2
AFTER UPDATE ON my_table
FOR EACH ROW
WHEN (
old.price IS DISTINCT FROM new.price
)
EXECUTE PROCEDURE update_price_m2();
But when I change the price I got unexpected results, like the price_m2 column change for various id's when I only want to change the value for one id (the one who changed).
Note
I know it's an antipattern to store columns whose value depends on the operation between two other columns, but there is a reason for that in this case
Thanks!
Just to follow up on this question so it can be closed, my recommendation in the comments was to use a generated column, which have been available since postgres 12:
https://www.postgresql.org/docs/current/ddl-generated-columns.html
The syntax would be something like this:
CREATE TABLE my_table (
id_property bigint GENERATED ALWAYS AS IDENTITY,
price int,
area int,
price_m2 int GENERATED ALWAYS AS (price / area) STORED
);
I'm using oracle 12c.
I need to generate a default value as a unique ID (PK) in my table. This value should be fixed in length as 16 digits. The format should be like 'YYYYMMDDHHmmXXXX'. The last part XXXX should be incremental from '0001' to '9999'. Also, the XXXX part should reset to 0001 per minute.
How can I generate such ID?
I don't want to discuss how good your primary key generation approach is, but i think this could be implemented using Triggers:
create table myTest
(
id varchar2(16) primary key,
val number
);
create index imyTest01 on mytest(substr(id, 1, 12), substr(id, 13));
CREATE OR REPLACE TRIGGER myTest_trg before insert on myTest for each row
begin
lock table mytest in exclusive mode; --If you have concurrent Inserts
select to_char(sysdate, 'YYYYMMDDHHMI')||lpad(((nvl(max(to_number(substr(id, 13))), 0))+1), 4, '0')
into :new.id
from myTest
where substr(id, 1, 12) = to_char(sysdate, 'YYYYMMDDHHMI');
end;
--Testing:
begin
for i in 1 .. 1000 loop
insert into mytest(val) values(i);
end loop;
end;
This can also be modified to use Numbers instead of Varchar as PK. Please be also aware of performance and locking issues with this implementation.
Does anyone see what's wrong with this code for SQL Server?
IF NOT EXISTS(SELECT *
FROM sys.columns
WHERE Name = 'OPT_LOCK'
AND object_ID = Object_id('REP_DSGN_SEC_GRP_LNK'))
BEGIN
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ADD OPT_LOCK NUMERIC(10, 0)
UPDATE REP_DSGN_SEC_GRP_LNK
SET OPT_LOCK = 0
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ALTER COLUMN OPT_LOCK NUMERIC(10, 0) NOT NULL
END;
When I run this, I get:
Msg 207, Level 16, State 1, Line 3
Invalid column name 'OPT_LOCK'.
on the update command.
Thanks.
In this case you can avoid the problem by adding the column as NOT NULL and setting the values for existing rows in one statement as per my answer here.
More generally the problem is a parse/compile issue. SQL Server tries to compile all statements in the batch before executing any of the statements.
When a statement references a table that doesn't exist at all the statement is subject to deferred compilation. When the table already exists it throws an error if you reference a non existing column. The best way round this is to do the DDL in a different batch from the DML.
If a statement both references a non existing column in an existing table and a non existent table the error may or may not be thrown before compilation is deferred.
You can either submit it in separate batches (e.g. by using the batch separator GO in the client tools) or perform it in a child scope that is compiled separately by using EXEC or EXEC sp_executesql.
The first approach would require you to refactor your code as an IF ... cannot span batches.
IF NOT EXISTS(SELECT *
FROM sys.columns
WHERE Name = 'OPT_LOCK'
AND object_ID = Object_id('REP_DSGN_SEC_GRP_LNK'))
BEGIN
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ADD OPT_LOCK NUMERIC(10, 0)
EXEC('UPDATE REP_DSGN_SEC_GRP_LNK SET OPT_LOCK = 0');
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ALTER COLUMN OPT_LOCK NUMERIC(10, 0) NOT NULL
END;
The root cause of the error is the newly added column name is not reflected in the sys.syscolumns and sys.columns table until you restart SQL Server Management Studio.
For your information,you can replace the IF NOT EXISTS with the COL_LENGTH function. It takes two parameters,
Table Name and
Column you are searching for
If the Column is found then it returns the range of the datatype of the column Ex: Int (4 bytes), when not found then it returns a NULL.
So, you could use this as follows and also combine 3 Statements into one.
IF (SELECT COL_LENGTH('REP_DSGN_SEC_GRP_LNK','OPT_LOCK')) IS NULL
BEGIN
ALTER TABLE REP_DSGN_SEC_GRP_LNK
ADD OPT_LOCK NUMERIC(10, 0) NOT NULL DEFAULT 0
END;
Makes it simpler.
In PostgreSQL, how do I get the last id inserted into a table?
In MS SQL there is SCOPE_IDENTITY().
Please do not advise me to use something like this:
select max(id) from table
( tl;dr : goto option 3: INSERT with RETURNING )
Recall that in postgresql there is no "id" concept for tables, just sequences (which are typically but not necessarily used as default values for surrogate primary keys, with the SERIAL pseudo-type).
If you are interested in getting the id of a newly inserted row, there are several ways:
Option 1: CURRVAL(<sequence name>);.
For example:
INSERT INTO persons (lastname,firstname) VALUES ('Smith', 'John');
SELECT currval('persons_id_seq');
The name of the sequence must be known, it's really arbitrary; in this example we assume that the table persons has an id column created with the SERIAL pseudo-type. To avoid relying on this and to feel more clean, you can use instead pg_get_serial_sequence:
INSERT INTO persons (lastname,firstname) VALUES ('Smith', 'John');
SELECT currval(pg_get_serial_sequence('persons','id'));
Caveat: currval() only works after an INSERT (which has executed nextval() ), in the same session.
Option 2: LASTVAL();
This is similar to the previous, only that you don't need to specify the sequence name: it looks for the most recent modified sequence (always inside your session, same caveat as above).
Both CURRVAL and LASTVAL are totally concurrent safe. The behaviour of sequence in PG is designed so that different session will not interfere, so there is no risk of race conditions (if another session inserts another row between my INSERT and my SELECT, I still get my correct value).
However they do have a subtle potential problem. If the database has some TRIGGER (or RULE) that, on insertion into persons table, makes some extra insertions in other tables... then LASTVAL will probably give us the wrong value. The problem can even happen with CURRVAL, if the extra insertions are done intto the same persons table (this is much less usual, but the risk still exists).
Option 3: INSERT with RETURNING
INSERT INTO persons (lastname,firstname) VALUES ('Smith', 'John') RETURNING id;
This is the most clean, efficient and safe way to get the id. It doesn't have any of the risks of the previous.
Drawbacks? Almost none: you might need to modify the way you call your INSERT statement (in the worst case, perhaps your API or DB layer does not expect an INSERT to return a value); it's not standard SQL (who cares); it's available since Postgresql 8.2 (Dec 2006...)
Conclusion: If you can, go for option 3. Elsewhere, prefer 1.
Note: all these methods are useless if you intend to get the last inserted id globally (not necessarily by your session). For this, you must resort to SELECT max(id) FROM table (of course, this will not read uncommitted inserts from other transactions).
Conversely, you should never use SELECT max(id) FROM table instead one of the 3 options above, to get the id just generated by your INSERT statement, because (apart from performance) this is not concurrent safe: between your INSERT and your SELECT another session might have inserted another record.
See the RETURNING clause of the INSERT statement. Basically, the INSERT doubles as a query and gives you back the value that was inserted.
Leonbloy's answer is quite complete. I would only add the special case in which one needs to get the last inserted value from within a PL/pgSQL function where OPTION 3 doesn't fit exactly.
For example, if we have the following tables:
CREATE TABLE person(
id serial,
lastname character varying (50),
firstname character varying (50),
CONSTRAINT person_pk PRIMARY KEY (id)
);
CREATE TABLE client (
id integer,
CONSTRAINT client_pk PRIMARY KEY (id),
CONSTRAINT fk_client_person FOREIGN KEY (id)
REFERENCES person (id) MATCH SIMPLE
);
If we need to insert a client record we must refer to a person record. But let's say we want to devise a PL/pgSQL function that inserts a new record into client but also takes care of inserting the new person record. For that, we must use a slight variation of leonbloy's OPTION 3:
INSERT INTO person(lastname, firstname)
VALUES (lastn, firstn)
RETURNING id INTO [new_variable];
Note that there are two INTO clauses. Therefore, the PL/pgSQL function would be defined like:
CREATE OR REPLACE FUNCTION new_client(lastn character varying, firstn character varying)
RETURNS integer AS
$BODY$
DECLARE
v_id integer;
BEGIN
-- Inserts the new person record and retrieves the last inserted id
INSERT INTO person(lastname, firstname)
VALUES (lastn, firstn)
RETURNING id INTO v_id;
-- Inserts the new client and references the inserted person
INSERT INTO client(id) VALUES (v_id);
-- Return the new id so we can use it in a select clause or return the new id into the user application
RETURN v_id;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Now we can insert the new data using:
SELECT new_client('Smith', 'John');
or
SELECT * FROM new_client('Smith', 'John');
And we get the newly created id.
new_client
integer
----------
1
you can use RETURNING clause in INSERT statement,just like the following
wgzhao=# create table foo(id int,name text);
CREATE TABLE
wgzhao=# insert into foo values(1,'wgzhao') returning id;
id
----
1
(1 row)
INSERT 0 1
wgzhao=# insert into foo values(3,'wgzhao') returning id;
id
----
3
(1 row)
INSERT 0 1
wgzhao=# create table bar(id serial,name text);
CREATE TABLE
wgzhao=# insert into bar(name) values('wgzhao') returning id;
id
----
1
(1 row)
INSERT 0 1
wgzhao=# insert into bar(name) values('wgzhao') returning id;
id
----
2
(1 row)
INSERT 0
The other answers don't show how one might use the value(s) returned by RETURNING. Here's an example where the returned value is inserted into another table.
WITH inserted_id AS (
INSERT INTO tbl1 (col1)
VALUES ('foo') RETURNING id
)
INSERT INTO tbl2 (other_id)
VALUES ((select id from inserted_id));
See the below example
CREATE TABLE users (
-- make the "id" column a primary key; this also creates
-- a UNIQUE constraint and a b+-tree index on the column
id SERIAL PRIMARY KEY,
name TEXT,
age INT4
);
INSERT INTO users (name, age) VALUES ('Mozart', 20);
Then for getting last inserted id use this for table "user" seq column name "id"
SELECT currval(pg_get_serial_sequence('users', 'id'));
SELECT CURRVAL(pg_get_serial_sequence('my_tbl_name','id_col_name'))
You need to supply the table name and column name of course.
This will be for the current session / connection
http://www.postgresql.org/docs/8.3/static/functions-sequence.html
For the ones who need to get the all data record, you can add
returning *
to the end of your query to get the all object including the id.
You can use RETURNING id after insert query.
INSERT INTO distributors (id, name) VALUES (DEFAULT, 'ALI') RETURNING id;
and result:
id
----
1
In the above example id is auto-increment filed.
The better way is to use Insert with returning. Though there are already same answers, I just want to add, if you want to save this to a variable then you can do this
insert into my_table(name) returning id into _my_id;
Postgres has an inbuilt mechanism for the same, which in the same query returns the id or whatever you want the query to return.
here is an example. Consider you have a table created which has 2 columns column1 and column2 and you want column1 to be returned after every insert.
# create table users_table(id serial not null primary key, name character varying);
CREATE TABLE
#insert into users_table(name) VALUES ('Jon Snow') RETURNING id;
id
----
1
(1 row)
# insert into users_table(name) VALUES ('Arya Stark') RETURNING id;
id
----
2
(1 row)
Try this:
select nextval('my_seq_name'); // Returns next value
If this return 1 (or whatever is the start_value for your sequence), then reset the sequence back to the original value, passing the false flag:
select setval('my_seq_name', 1, false);
Otherwise,
select setval('my_seq_name', nextValue - 1, true);
This will restore the sequence value to the original state and "setval" will return with the sequence value you are looking for.
I had this issue with Java and Postgres.
I fixed it by updating a new Connector-J version.
postgresql-9.2-1002.jdbc4.jar
https://jdbc.postgresql.org/download.html:
Version 42.2.12
https://jdbc.postgresql.org/download/postgresql-42.2.12.jar
Based on #ooZman 's answer above, this seems to work for PostgreSQL v12 when you need to INSERT with the next value of a "sequence" (akin to auto_increment) without goofing anything up in your table(s) counter(s). (Note: I haven't tested it in more complex DB cluster configurations though...)
Psuedo Code
$insert_next_id = $return_result->query("select (setval('"your_id_seq"', (select nextval('"your_id_seq"')) - 1, true)) +1");