Incrementing a column value in a SQL database if there is a duplicate row - sql

Suppose I have a table like this:
Number Product Type
1 Meat Cow
1 Milk A
If I insert a new row:
INSERT INTO t1 (Product, Type) VALUES (Meat, Cow)
I don't want a new row. I want the column "Number" where Product = Meat and Type = Cow be incremented by 1:
Number Product Type
2 Meat Cow
1 Milk A
Is there a SQL command to do this?

You may try the following by using ON CONFLICT that starts from version 9.5 :
create table t1( Number int,
Product varchar(15),
Type varchar(15),
primary key (Product, Type)
);
insert into t1(Number,Product,Type) values(1,'Meat','Cow');
insert into t1(Number,Product,Type) values(1,'Meat','Cow')
on conflict (Product,Type) do update set Number = t1.Number + 1;
select * from t1;
number product type
------ ------- ----
2 Meat Cow
Rextester Demo
where composite unique(primary) key is a must for Product and Type columns, if not exists than 42P10: there is no unique or exclusion constraint matching the ON CONFLICT specification error raises.

You can use triggers to achieve what you want. Format :
CREATE OR REPLACE TRIGGER trigger_name
INSTEAD OF INSERT ON table_name
WHEN (your_condition_here) --for example, row exist
BEGIN
UPDATE ... --your update query
END;

Create Unique Index on Product and Type, then
INSERT INTO t1 (Product, Type) VALUES (Meat, Cow)
ON CONFLICT ON CONSTRAINT <your_key>
UPDATE set Number = Number + 1
Should work.

Related

In an INSERT, how to make the null values to be zero or null?

I have a table of 10 columns, and my INSERT statement only refers to specific columns in the table.
INSERT INTO SCHEMA.TABLE
(COL_1, COL_2)
VALUES
(VAL_1, VAL_2);
... or...
INSERT INTO SCHEMA.TABLE
(COL_1, COL_2)
SELECT VAL_1, VAL_2 FROM SCHEMA_2.TABLE_2;
However, when I execute it, the other columns are inserted always with a null value, instead of having the corresponding one depending on the column type (i.e. number). This is, if I have a numeric column, I should see a zero.
How can I do that insert properly?
*** Please consider I have no DDL privileges & I'm trying to insert into an existing table.
The easiest approach would probably to give your columns default values:
ALTER TABLE schema.table MODIFY (COL_1 NUMBER DEFAULT 0);
DEFAULT value it is; however, note that you have to pay attention to what you do because column might not get its default value. Here's an example:
SQL> create table test
2 (id number primary key,
3 name varchar2(10),
4 address varchar2(20) default 'Unknown', --> columns with default
5 num_val number default 0 --> values
6 );
Table created.
If you're inserting values without specifying column(s) that are supposed to get default values, everything will be as you'd want it to be:
SQL> insert into test (id, name) values (1, 'Little');
1 row created.
SQL> select * from test;
ID NAME ADDRESS NUM_VAL
---------- ---------- -------------------- ----------
1 Little Unknown 0
See? Both ADDRESS and NUM_VAL got default values.
However, if you mention those columns in INSERT statement, although setting them to NULL, they won't be set to their default values but NULL:
SQL> insert into test (id, name, address, num_val)
2 values (2, 'Foot', null, null);
1 row created.
SQL> select * from test;
ID NAME ADDRESS NUM_VAL
---------- ---------- -------------------- ----------
1 Little Unknown 0
2 Foot
As you can see, row with ID = 2 didn't get default values in ADDRESS and NUM_VAL columns.
Therefore, pay attention to what you do.
USE DEFAULT AS 0 for that column
or
use NVL( column_name, 0 ) --as per oracle syntax
--this would mean whenever theres null found for
-- that column set it to 0 (will work on insert)
or
Update column set column=0 where column IS NULL
--(will work after insert as the name suggests update)
Although frankly I don't recommend doing this, you can use a trigger to accomplish your goal:
CREATE OR REPLACE TRIGGER SCHEMA.TABLE_BI
BEFORE INSERT ON SCHEMA.TABLE
FOR EACH ROW
BEGIN
:NEW.COL_1 := COALESCE(:NEW.COL_1, 0); -- NUMBER column
:NEW.COL_2 := COALESCE(:NEW.COL_2, ' '); -- VARCHAR column
:NEW.COL_3 := COALESCE(:NEW.COL_3, SYSDATE); -- DATE column
END SCHEMA.TABLE_BI;
However, creating a trigger may require privileges you don't have.
To answer the question: that needs to be defined at table creation, determining default values. In this case, I wasn't able to do that because the table definition indicated NULL, even in the case of numbers.
Thanks anyway.

Auto incrementing column including year

I need to create a table with an ID column that increments automatically and concatenates the current year, for example:
year = 2018 ==> 20181 , 20182 , 20183 , etc
year = 2020 ==> 20201 , 20202 , 20203 , etc
How can I do that?
Firstly create a sequence :
CREATE SEQUENCE seq_myTable START WITH 1;
and then use it in a trigger :
CREATE OR REPLACE TRIGGER Trg_myTable
BEFORE INSERT ON myTable
FOR EACH ROW
DECLARE
v_year varchar2(4):=to_char(sysdate,'yyyy');
BEGIN
:new.id := v_year||seq_myTable.nextval;
END;
/
The following requires Oracle 12.1 or later:
create sequence demo_seq;
create table demo
( genid varchar2(10) default on null to_char(sysdate,'YYYY')||demo_seq.nextval
constraint demo_pk primary key
, somecol varchar2(10) );
insert into demo (somecol) values ('Kittens');
insert into demo (somecol) values ('Puppies');
select * from demo;
GENID SOMECOL
---------- ----------
20181 Kittens
20182 Puppies
The only limitation here is that the sequence doesn't restart for each year. To automate this, I think you would have to abandon the sequence and use a select max +1 approach with some explicit serialisation (dbms_lock or similar). Alternatively, schedule a job to restart the sequence each year.
I guess there is no such way to do this with automatic mechanisms as a generated Id.
I see a few options here:
use 2 columns: Id and Year
use a virtual column
use a function in a on insert trigger to build your custom id
I'd recommend using 2 columns and a primary key over the id column and a index over the year column as it's the most clean implementation i can think of (and perhaps the fastest as well).
+ you should use your id as id and another column to see which year it's been added, or even a date column if you need the information the record has been inserted

ORACLE PL/SQL Update another table value on insert

I am trying to create a trigger on any insert or update on a table which holds Diseases with animalName and diseaseName.
I have another table, Animals, which holds informations like the animalName (which is a primary key; don't comment on the design as it is not mine), and the amountOfDisease he had.
I want that, upon insert, update or delete in the Diseases table, the amount of Diseases is automatically updated.
I have a hard time understanding how I can obtain the current animalName so that I can update his amountOfDisease.
So far, I have this :
CREATE OR REPLACE TRIGGER update_animal_diseases
AFTER INSERT OR UPDATE OR DELETE ON Diseases
FOR EACH ROW
BEGIN
UPDATE Animals SET amountOfDisease = amountOfDisease + 1
WHERE animalName = :NEW.animalName;
END;
/
Which compile but doesn't work, as the values in Animals never get updated on inserting something into Diseases. I also tried this :
CREATE OR REPLACE TRIGGER update_animal_diseases
AFTER INSERT OR UPDATE ON Diseases
FOR EACH ROW
DECLARE
DiseasesCount INTEGER;
BEGIN
SELECT COUNT(*) INTO DiseasesCount
FROM Diseases
WHERE animalName = :OLD.animalName;
UPDATE Animals SET amountOfDisease = DiseasesCount WHERE animalName = :OLD.animalName;
END;
/
As you can see I also don't really understand what the :NEW and :OLD are. How can I solve my problem, which is to update an animal amountOfDisease on any modification of the Diseases table ?
To be clear, what I get on INSERT-ing into Diseases is just nothing. Nothing happens as far as I can tell.
I see two possible causes.
In the first version you are always adding +1, even on delete.
Also if amountofdisease field is null on a record after adding +1 it will null anyway.
Maybe something like this should work for you.
Tables:
CREATE TABLE animals
( animalname VARCHAR2 (10),
amountofdisease NUMBER);
CREATE TABLE diseases
( animalname VARCHAR2 (10),
diseasename VARCHAR2 (20));
Trigger:
CREATE OR REPLACE TRIGGER apps.diseases_aiud1
BEFORE DELETE OR INSERT OR UPDATE
ON diseases
REFERENCING NEW AS new OLD AS old
FOR EACH ROW
DECLARE
BEGIN
IF INSERTING
THEN
UPDATE animals
SET amountofdisease = NVL (amountofdisease, 0) + 1
WHERE animalname = :new.animalname;
ELSIF DELETING
THEN
UPDATE animals
SET amountofdisease = NVL (amountofdisease, 0) - 1
WHERE animalname = :old.animalname;
ELSIF UPDATING
THEN
UPDATE animals
SET amountofdisease = NVL (amountofdisease, 0) + 1
WHERE animalname = :new.animalname;
UPDATE animals
SET amountofdisease = NVL (amountofdisease, 0) - 1
WHERE animalname = :old.animalname;
END IF;
END diseases_aiud1;
/
Note de use of :new and :old depending on the event.
Loading sample animals:
insert into animals values ('jaguar',0);
insert into animals values ('capibara',0);
insert into animals values ('fox',0);
commit;
Test 1 Insert
insert into diseases values
('jaguar','pneumonia');
insert into diseases values
('jaguar','epistaxis');
commit;
select *
from animals;
Result 1:
1 row created.
1 row created.
Commit complete.
ANIMALNAME AMOUNTOFDISEASE
---------- ---------------
jaguar 2
capibara 0
fox 0
3 rows selected.
Test 2 delete:
delete from diseases
where animalname = 'jaguar' and diseasename = 'pneumonia'
;
insert into diseases values
('fox','hydrophobia');
Result 2:
ANIMALNAME AMOUNTOFDISEASE
---------- ---------------
jaguar 1
capibara 0
fox 1
Test 3 Update:
update diseases
set animalname = 'capibara'
where animalname = 'fox';
Result 3:
ANIMALNAME AMOUNTOFDISEASE
---------- ---------------
jaguar 1
capibara 1
fox 0
As a side note it should be recommended to write a package in order to handle this logic. Triggers are tricky, hardest to maintain and can lead to unexpected results in some scenarios.
Regards,
If I had to speculate, you don't have matching rows in Animals for all animals in Diseases. You can find these by doing:
select d.*
from diseases d
where not exists (select 1 from animals a where a.animal = d.animal);
If this is the case, then you should structure the database to have an explicit foreign key relationship from diseases to animals. This will ensure that only valid animals are in the table.
As far as trigger,
:NEW means when New record in inserted to your primary table(primary table here means a table where you sets triggers on insert) it takes always New I'd to insert record related records as per trigger in relative table (relative table means in trigger you defined to insert / update record when primary table effected. )
:OLD means in relative table it takes last inserted I'd as new id in relative table. It can be use as foriegn key in second table.
I hope it's meaning full for your question.

How to return values from INSERT other that the row that was inserted

I have a large number of rows that i want to insert simultaneously into a PostgreSQL database. I need to track what id is assigned for each row that is inserted. For example say we have the table:
CREATE TABLE example
(
id serial,
name text,
CONSTRAINT example_pkey PRIMARY KEY (id),
);
Now i have some data with ids that i dont want inserted (as the serial id column will assign a new id), but i need to keep track of the mapping between the old id and new id:
old id | name
-------------
-1 | foo
-2 | bar
-3 | baz
So i wrote this query
WITH data(oldid,name) AS ( VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
INSERT INTO example(name)
SELECT name FROM data d
RETURNING id, d.oldid
Expecting to get something back like:
id | oldid
-----------
1 | -1
2 | -2
3 | -3
However this doesn't work, as i don't believe you can return a column that wasn't inserted. Is there any alternative way to do this?
I ended up creating a function that wrapped the inserting of a single row:
CREATE OR REPLACE FUNCTION add_example(
in_name text)
RETURNS integer AS
$BODY$
DECLARE
new_id integer;
BEGIN
INSERT INTO example(name)
VALUES (in_name) RETURNING id INTO new_id;
RETURN new_id;
END;
$BODY$
LANGUAGE plpgsql;
Then i can do:
WITH data(oldid, name) AS (VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
SELECT oldid, add_example(name) AS id
FROM data
Which returns what i expect. I'd like to see if this can be done without the function though.
CREATE SEQUENCE data_id_seq;
CREATE TABLE DATA (
id integer default nextval('data_id_seq') NOT NULL PRIMARY KEY,
oldid integer,
name text,
);
INSERT INTO DATA(oldid,name) values (-1,'foo'),(-2,'bar'),(-3,'baz') returning id,oldid;
The optional RETURNING clause causes INSERT to compute and return
value(s) based on each row actually inserted
from https://www.postgresql.org/docs/current/static/sql-insert.html
so column parasite is unavoidable for such solution:
alter table example add column old bigint;
WITH d(oldid,name) AS ( VALUES
(-1,'foo'),
(-2,'bar'),
(-3,'baz')
)
INSERT INTO example(name,old)
SELECT "name", oldid FROM d
RETURNING id, old

Unique constraint on one column with excluding row with same values in other

I'd like to add a unique key to column value but I must ignore rows that have the same values in columns value and header_id. For example, consider this table:
id | header_id | value
1 | 1 | a
2 | 1 | a
3 | 2 | a
So rows 1 and 2 point to same object and the unique key should accept them, but row 3 has a different header_id (pointing to another object) and, because it has the same value as object 1, it should violate unique constraint and raise an error.
Edit 16.2:1327:
I'm using a core framework that generates columns to handle history so I cannot normalize the table. My class has lots of columns but for this example I'm only considering the value column.
You could do it if you can change your table structure slightly:
your_table
id header_value
1 1
2 1
3 2
header_value
id header_id value
1 1 a
2 2 a
Add a foreign key constraint from your_table.header_value to header_value.id.
Now you can add a unique constraint on header_value.value.
You could use a trigger to simulate a unique constraint with your desired properties. Something like this would do the trick:
create or replace function sort_of_unique() returns trigger as $$
declare
got_one boolean;
begin
select exists(
select 1
from your_table
where header_id != new.header_id
and value = new.value
) into got_one;
if got_one then
raise exception 'Uniqueness violation in your_table';
end if;
return new;
end;
$$ language plpgsql;
create trigger sort_of_unique_trigger
before insert or update on your_table
for each row execute procedure sort_of_unique();
Then you'd get things like this happening:
=> insert into your_table (id, header_id, value) values (1, 1, 'a');
=> insert into your_table (id, header_id, value) values (2, 1, 'a');
=> insert into your_table (id, header_id, value) values (3, 2, 'a');
ERROR: Uniqueness violation in your_table
=> insert into your_table (id, header_id, value) values (3, 2, 'b');
=> update your_table set value = 'a' where id = 3;
ERROR: Uniqueness violation in your_table
You can create partial unique indexes by attaching a WHERE clause to the index. This allows you to apply your uniqueness constraint to slices of the table; however, I can't think of a way to get the WHERE clause to specify an "anti-slice" so I don't see a way to make this work with a partial index. I could be missing something obvious though.
After a while I found something. Using constrain CHECK with function to determine if exist (Cannot use SELECT in CHECK statement but you can use function with desired select)
CREATE OR REPLACE FUNCTION is_value_free(_header_id integer, _value varchar) RETURNS BOOLEAN AS
$$
BEGIN
RETURN NOT EXISTS (SELECT header_id,value FROM myschema.mytalbe WHERE value LIKE _value AND header_id != _header_id LIMIT 1);
END;
$$ LANGUAGE plpgsql;
ALTER TABLE mytable ADD CONSTRAINT uniq_value CHECK (is_value_free(header_id,value))