Parent key not found (oracle) - oracle

I want to insert 400000 random lines into a table with an oracle procedure.
I got an 'violated - parent key not found' Error.
create or replace
PROCEDURE TESTDATA AS
X INTEGER;
BEGIN
FOR X IN 1..400000 LOOP
INSERT INTO SALES
SELECT CAST(DBMS_RANDOM.VALUE(1, 10) as INTEGER) as "CUSTOMER_ID",
CAST(DBMS_RANDOM.VALUE(1, 2) as INTEGER) as "PRODUCT_ID",
TO_DATE(TRUNC(DBMS_RANDOM.VALUE(TO_CHAR(TO_DATE('1-jan-2000'),'J'),TO_CHAR(TO_DATE('21-dec-2012'),'J'))),'J') as "SALES_DATE",
CAST(DBMS_RANDOM.VALUE(1, 100) as INTEGER) as "PIECES"
FROM DUAL;
END LOOP;
END TESTDATA;
-- -----------------------------------------------------
-- Table SALES
-- -----------------------------------------------------
CREATE TABLE SALES (
CUSTOMER_ID INTEGER,
PRODUCT_ID INTEGER,
SALES_DATE DATE,
PIECES INTEGER,
PRIMARY KEY (CUSTOMER_ID, PRODUCT_ID, SALES_DATE),
FOREIGN KEY (CUSTOMER_ID) REFERENCES CUSTOMER (CUSTOMER_ID),
FOREIGN KEY (PRODUCT_ID) REFERENCES PRODUCT (PRODUCT_ID)
);
I hope that anyone could help me.
Roman

Your create table statement includes foreign keys on the CUSTOMER and PRODUCT tables. Your insert statement uses random values to populate CUSTOMER_ID and PRODUCT_ID. It is highly unlikely that random values will match existing keys in the referenced tables, so it's unsurprising that you get foreign key violations.
As to how you fix it, well that depends on what you actually want to achieve. As you're populating your table with random numbers you clearly don't care about the data, so you might as well drop the foreign keys. Alternatively you can use primary key values from the referenced tables in the insert statement.
"How can I use the primary key values from the referenced tables?"
You have twenty permutations of PRODUCT and CUSTOMER. So you will need to thrash through them 20000 times to generate 400000 records.
"In the customer table I have inserted 10 rows (1-10) and in the product table 2 rows (1-2)"
Here's a version of your procedure which loops through Products and Customers to generate random combinations of them. There's a third outer loop which allows you to produce as many sets of twenty combos as your heart desires.
create or replace procedure testdata
( p_iters in pls_integer := 1)
as
type sales_nt is table of sales%rowtype;
recs sales_nt;
rec sales%rowtype;
tot_ins simple_integer := 0;
begin
recs := new sales_nt();
dbms_random.seed(val=>to_number(to_char(sysdate,'sssss')));
<< iterz >>
for idx in 1..p_iters loop
<< prodz >>
for c_rec in ( select customer_id from customer
order by dbms_random.value )
loop
<< custz >>
for p_rec in ( select product_id from product
order by dbms_random.value )
loop
rec.customer_id := c_rec.customer_id;
rec.product_id := p_rec.product_id;
rec.sales_date := date '2000-01-01' + dbms_random.value(1, 6000);
rec.pieces := dbms_random.value(1, 100);
recs.extend();
recs(recs.count()) := rec;
end loop custz;
end loop prodz;
forall idx in 1..recs.count()
insert into sales
values recs(idx);
tot_ins := tot_ins + sql%rowcount;
recs.delete();
end loop iterz;
dbms_output.put_line('test records generated = '||to_char(tot_ins));
end testdata;
/
Proof of the pudding:
SQL> set serveroutput on
SQL> exec testdata(2)
test records generated = 40
PL/SQL procedure successfully completed.
SQL> select * from sales;
CUSTOMER_ID PRODUCT_ID SALES_DAT PIECES
----------- ---------- --------- ----------
9 2 21-FEB-02 42
9 1 10-AUG-05 63
7 1 23-FEB-12 54
7 2 21-NOV-12 80
1 2 06-NOV-15 56
1 1 08-DEC-09 47
4 2 08-JUN-10 58
4 1 19-FEB-09 43
2 2 04-SEP-02 64
2 1 09-SEP-15 69
6 2 20-FEB-08 60
...
2 1 11-JAN-16 19
3 2 03-FEB-10 58
3 1 25-JUL-10 66
9 2 26-FEB-16 70
9 1 15-MAY-14 90
6 2 03-APR-05 60
6 1 21-MAY-15 19
40 rows selected.
SQL>
note
As #mathguy points out, the OP's code constrains the range of random values to fit the range of primary keys in the referenced tables. However, I will leave this answer in place because its general approach is both safer (guaranteed to always match a referenced primary key) and less brittle (can cope with inserting or deleting PRODUCT or CUSTOMER records.

Related

PLSQL Trigger - Update another table before inserting a new record

I have 3 tables that are related to each other:
ACCOUNTS
CARDS
TRANSACTIONS
I want to change the money amount from account every time I execute a new transaction. I want to decrease the account value with each new move.
I tried writing this trigger:
create or replace trigger ceva_trig1
before insert on miscari
for each row
declare
new_val micari.valoare%tipe := new.valoare;
begin
update conturi
set sold = sold - new_val
where nrcont = (select nrcont
from conturi
join carti_de_credit on conturi.nrcont = carti_de_credit.nrcont
join miscari on carti_de_credit.nr_card = miscari.nrcard)
and sold >= new_val;
end;
May anyone help me correct the syntax that crashes here?
I've created those tables with minimal number of columns, just to make trigger compile.
SQL> create table conturi
2 (sold number,
3 nrcont number
4 );
Table created.
SQL> create table miscari
2 (valoare number,
3 nrcard number
4 );
Table created.
SQL> create table carti_de_credit
2 (nrcont number,
3 nr_card number
4 );
Table created.
Trigger:
SQL> create or replace trigger ceva_trig1
2 before insert on miscari
3 for each row
4 begin
5 update conturi c
6 set c.sold = c.sold - :new.valoare
7 where c.nrcont = (select r.nrcont
8 from carti_de_credit r
9 where r.nrcont = c.nrcont
10 and r.nr_card = :new.nrcard
11 )
12 and c.sold >= :new.valoare;
13 end;
14 /
Trigger created.
SQL>
How does it differ from your code? Like this:
SQL> create or replace trigger ceva_trig1
2 before insert on miscari
3 for each row
4 declare
5 new_val micari.valoare%tipe := new.valoare;
6 begin
7 update conturi
8 set sold = sold - new_val
9 where nrcont = (select nrcont
10 from conturi
11 join carti_de_credit on conturi.nrcont = carti_de_credit.nrcont
12 join miscari on carti_de_credit.nr_card = miscari.nrcard)
13 and sold >= new_val;
14 end;
15 /
Warning: Trigger created with compilation errors.
SQL> show err
Errors for TRIGGER CEVA_TRIG1:
LINE/COL ERROR
-------- -----------------------------------------------------------------
2/11 PL/SQL: Item ignored
2/26 PLS-00208: identifier 'TIPE' is not a legal cursor attribute
4/3 PL/SQL: SQL Statement ignored
10/15 PL/SQL: ORA-00904: "NEW_VAL": invalid identifier
10/15 PLS-00320: the declaration of the type of this expression is incomplete or malformed
SQL>
Explained:
it isn't tipe but type
new column values are referenced with a colon, i.e. :new.valoare
you shouldn't make typos regarding table & column names; it is miscari, not micari
it is bad practice to write query which references the same table (miscari, line #12) trigger is created for. As it is being changed, you can't select values from it as it is mutating
lucky you, you don't have to do that at all. How? Have a look at my code.
Attempting to maintain an ongoing for transactions in one table in another table is always a bad idea. Admittedly in an extremely few cases it's necessary, but should be the design of last resort not an initial one; even when necessary it's still a bad idea and therefore requires much more processing and complexity.
In this instance after you correct all the errors #Littlefoot points out then your real problems begin. What do you do when: (Using Littlefoot's table definitions)
I delete a row from miscari?
I update a row in miscari?
The subselect for nrcont returns 0 rows?
The condition sold >= new_val is False?
If any of conditions occur the value for sold in conturi is incorrect and may not be correctable from values in the source table - miscari. An that list may be just the beginning of the issues you face.
Suggestion: Abandon the idea of keeping an running account of transaction values. Instead derive it when needed. You can create a view that does that and select from the view.
So maybe instead of "create table conturi ..."

Creating an oracle table with auto increment feature

How to create an oracle table with an auto increment column such that whenever exsisiting value is getting inserted it should increment the counter otherwise it should insert a new count
For instance if I have a column with phone number and status
There should be an another column named counter on which auto increment feature will be present
Whenever exsiting phonenumber is inserted again then counter must be increment and if a new value is inserted then counter should add a new initial value for that number
Depending on how you want to insert the data. If you are going to be inserting many rows at the same time then try a MERGEstatement.
Join with the phone number, if found increment the counter column value else set the counter to 1.
If you are going to be inserting one row at a time then this is best done in the code that performs an insert.
EDIT: I did not think this through. Now that I am, I think it is unnecessary to use a counter column.
If you are going to insert phone numbers multiple times anyway, why don't you simply count each phone number? It doesn't have to be stored.
You can't create a table like that.
You can, however, add your own logic into the place where you INSERT new rows - eg, it's not in the table itself. You can also go the route of a TRIGGER.
Additionally, you may wish to simply have your ID be a unique GUID that gets generated and create this duplicate counter whenever it is necessary, using ROW_NUMBER() OVER like EMP_ID in this example from the oracle website:
SELECT department_id, last_name, employee_id, ROW_NUMBER()
OVER (PARTITION BY department_id ORDER BY employee_id) AS emp_id
FROM employees;
DEPARTMENT_ID LAST_NAME EMPLOYEE_ID EMP_ID
------------- ------------------------- ----------- ----------
10 Whalen 200 1
20 Hartstein 201 1
20 Fay 202 2
30 Raphaely 114 1
30 Khoo 115 2
30 Baida 116 3
30 Tobias 117 4
30 Himuro 118 5
30 Colmenares 119 6
For Auto Increment You can create a sequence as below.
CREATE SEQUENCE name_of_sequence
START WITH 1
INCREMENT BY 1;
For the second part of your query you can define a trigger that automatically populates the primary key value using above sequence

Unique value generation for primary key for multiple tables

I do have 3 tables. I want to generate ID for each below table and that should be unique across each table.
Table1 ID - primary
ID
1 -> 2 -> 3
Table2 ID - primary
ID
4 -> 5
Table3 ID - primary
ID
6 -> 7 -> 8
Whenever a new entry is made to the above tables it should generate unique values across the tables
For next time when I want to insert 2 records to table 1 it should be
Table1 ID - primary
ID
9 -> 10.
Do we can create a trigger to accomplish this in Oracle
Use a trigger on each table and create a Sequence
CREATE SEQUENCE seq
START WITH 1
INCREMENT BY 1
NOCACHE
NOCYCLE;
Then on the trigger on insert do something like:
select seq.nextval
into :new.id
from dual;
This was asked before. Look here
CREATE OR REPLACE TRIGGER my_trigger
BEFORE INSERT
ON qname
FOR EACH ROW
-- Optionally restrict this trigger to fire only when really needed
WHEN (new.qname_id is null)
DECLARE
v_id qname.qname_id%TYPE;
BEGIN
-- Select a new value from the sequence into a local variable. As David
-- commented, this step is optional. You can directly select into :new.qname_id
SELECT qname_id_seq.nextval INTO v_id FROM DUAL;
-- :new references the record that you are about to insert into qname. Hence,
-- you can overwrite the value of :new.qname_id (qname.qname_id) with the value
-- obtained from your sequence, before inserting
:new.qname_id := v_id;
END my_trigger;

How to generate alphanumeric id in Oracle

In my vb application I want an autogenerated id of alphanumeric characters, like prd100. How can I increment it using Oracle as backend?
Any particular reason it needs to be alphanumeric? If it can just be a number, you can use an Oracle sequence.
But if you want just a random string, you could use the dbms_random function.
select dbms_random.string('U', 20) str from dual;
So you could probably combine these 2 ideas (in the code below, the sequence is called oid_seq):
SELECT dbms_random.string('U', 20) || '_' || to_char(oid_seq.nextval) FROM dual
There are two parts to your question. The first is how to create an alphanumeric key. The second is how to get the generated value.
So the first step is to determine the source of the alpha and the numeric components. In the following example I use the USER function and an Oracle sequence, but you will have your own rules. I put the code to assemble the key in a trigger which is called whenever a row is inserted.
SQL> create table t1 (pk_col varchar2(10) not null, create_date date)
2 /
Table created.
SQL> create or replace trigger t1_bir before insert on t1 for each row
2 declare
3 n pls_integer;
4 begin
5 select my_seq.nextval
6 into n
7 from dual;
8 :new.pk_col := user||trim(to_char(n));
9 end;
10 /
Trigger created.
SQL>
The second step requires using the RETURNING INTO clause to retrieve the generated key. I am using SQL*PLus for this example. I confess to having no idea how to wire this syntax into VB. Sorry.
SQL> var new_pk varchar2(10)
SQL> insert into t1 (create_date)
2 values (sysdate)
3 returning pk_col into :new_pk
4 /
1 row created.
SQL> print new_pk
NEW_PK
--------------------------------
APC61
SQL>
Finally, a word of warning.
Alphanumeric keys are a suspicious construct. They reek of "smart keys" which are, in fact, dumb. A smart key is a value which contains multiple parts. At somepoint you will find yourself wanting to retrieving all rows where the key starts with 'PRD', which means using SUBSTR() or LIKE. Even worse someday the definition of the smart key will change and you will have to cascade a complicated update to your table and its referencing foreign keys. A better ides is to use a surrogate key (number) and have the alphanumeric "key" defined as separate columns with a UNIQUE composite constraint to enforce the business rule.
SQL> create table t1 (id number not null
2 , alpha_bit varchar2(3) not null
3 , numeric_bit number not null
4 , create_date date
5 , constraint t1_pk primary key (id)
6 , constraint t1_uk unique (alpha_bit, numeric_bit)
7 )
8 /
Table created.
SQL>

Obtaining an inserted recordid on Oracle db

I'm using Oracle on database server, from an XP client, using VB6 and ADO. In one transaction, I'm inserting one record into a parent table, which has a trigger and sequence to create a unique recordid, then that recordid is used for the relationship to a child table for a variable number of inserts to the child table. For performance, this is being sent in one execute command from my client app. For instance (simplified example):
declare Recordid int;
begin
insert into ParentTable (_field list_) Values (_data list_);
Select ParentTableSequence.currVal into Recordid from dual;
insert into ChildTable (RecordID, _field list_) Values (Recordid, _data list_);
insert into ChildTable (RecordID, _field list_) Values (Recordid, _data list_);
... multiple, variable number of additional ChildTable inserts
commit;
end;
This is working fine. My question is: I also need to return to the client the Recordid that was created for the inserts. On SQL Server, I can add something like a select to Scope_Identity() after the commit to return a recordset to the client with the unique id.
But how can I do something similar for Oracle (doesn't have to be a recordset, I just need that long integer value)? I've tried a number of things based on results from searching the 'net, but have failed in finding a solution.
These two lines can be compressed into a single statement:
-- insert into ParentTable (field list) Values (data list);
-- Select ParentTableSequence.currVal into Recordid from dual;
insert into ParentTable (field list) Values (data list)
returning ParentTable.ID into Recordid;
If you want to pass the ID back to the calling program you will need to define your program as a stored procedure or function, returning Recordid as an OUT parameter or a RETURN value respectively.
Edit
MarkL commented:
This is more of an Oracle PL/SQL
question than anything else, I
believe.
I confess that I no nothing about ADO, so I don't know whether the following example will work in your case. It involves building some infrastructure which allows us to pass an array of values into a procedure. The following example creates a new department, promotes an existing employee to manage it and assigns two new hires.
SQL> create or replace type new_emp_t as object
2 (ename varchar2(10)
3 , sal number (7,2)
4 , job varchar2(10));
5 /
Type created.
SQL>
SQL> create or replace type new_emp_nt as table of new_emp_t;
2 /
Type created.
SQL>
SQL> create or replace procedure pop_new_dept
2 (p_dname in dept.dname%type
3 , p_loc in dept.loc%type
4 , p_mgr in emp.empno%type
5 , p_staff in new_emp_nt
6 , p_deptno out dept.deptno%type)
7 is
8 l_deptno dept.deptno%type;
9 begin
10 insert into dept
11 (dname, loc)
12 values
13 (p_dname, p_loc)
14 returning deptno into l_deptno;
15 update emp
16 set deptno = l_deptno
17 , job = 'MANAGER'
18 , mgr = 7839
19 where empno = p_mgr;
20 forall i in p_staff.first()..p_staff.last()
21 insert into emp
22 (ename
23 , sal
24 , job
25 , hiredate
26 , mgr
27 , deptno)
28 values
29 (p_staff(i).ename
30 , p_staff(i).sal
31 , p_staff(i).job
32 , sysdate
33 , p_mgr
34 , l_deptno);
35 p_deptno := l_deptno;
36 end pop_new_dept;
37 /
Procedure created.
SQL>
SQL> set serveroutput on
SQL>
SQL> declare
2 dept_staff new_emp_nt;
3 new_dept dept.deptno%type;
4 begin
5 dept_staff := new_emp_nt(new_emp_t('MARKL', 4200, 'DEVELOPER')
6 , new_emp_t('APC', 2300, 'DEVELOPER'));
7 pop_new_dept('IT', 'BRNO', 7844, dept_staff, new_dept);
8 dbms_output.put_line('New DEPTNO = '||new_dept);
9 end;
10 /
New DEPTNO = 70
PL/SQL procedure successfully completed.
SQL>
The primary keys for both DEPT and EMP are assigned through triggers. The FORALL syntax is a very efficient way of inserting records (it also works for UPDATE and DELETE). This could be written as a FUNCTION to return the new DEPTNO instead, but it is generally considered better practice to use a PROCEDURE when inserting, updating or deleting.
That would be my preferred approach but I admit it's not to everybody's taste.
Edit 2
With regards to performance, bulk operations using FORALL will definitely perform better than a handful of individual inserts. In SQL, set operations are always preferable to record-by-record. However, if we are dealing with only a handful of records each time it can be hard to notice the difference.
Building a PL/SQL collection (what you think of as a temporary table in SQL Server) can be expensive in terms of memory. This is especially true if there are many users running the code, because it comes out of the session level allocation of memory, not the Shared Global Area. When we're dealing with a large number of records it is better to populate an array in chunks, perhaps using the BULK COLLECT syntax with a LIMIT clause.
The Oracle online documentation set is pretty good. The PL/SQL Developer's Guide has a whole chapter on Collections. Find out more.

Resources