Get a unique hash value based on value of 3 columns of a table - oracle

Say I have 3 columns in a table column_1,column_2,column_3.
I want to populate two new tables table_1.column_unique_ind and table_2.column_unique_ind with a unique value for each unique combination of column_1,column_2,column_3.
I want to compare these two columns later on to check for unique combination of column_1,column_2,column_3.
Is there any built in oracle function which I can use in this case?
What I want is,
For Any given combination of column_1,column_2,column_3 the
function should always return the same unique value, any day I use this function.
For two different combinations of column_1,column_2,column_3 the
function should always return different value

There's no built-in Oracle feature that I'm aware of that does what you're asking. Here's how I'd go about it (change your variable sizes/types/delimiter as needed).
Create a function to build the unique ID so you can re-use it to get the these ID's as needed.
FUNCTION get_unique_id(val_1_in VARCHAR2, val_2_in VARCHAR2, val_3_in VARCHAR2) RETURN VARCHAR2
AS
unique_id VARCHAR2(4000);
delimiter_l CONSTANT VARCHAR2(1) := '|';
BEGIN
unique_id := val_1_in || delimiter_l || val_2_in || delimiter_l || val_3_in;
return unique_id;
END;
Now, to load your table. I tested this with a temp table and it worked.
DECLARE
unique_id_l VARCHAR2(4000); --consider using table_1.column_unique_id%TYPE
BEGIN
FOR rec in
( --use your 3 columns here you want to make the unique value
SELECT col_1, col_2, col_3
FROM source_table
GROUP BY col_1, col_2, col_3 --this GROUP BY will insure uniqueness
)
LOOP
unique_id_l := get_unique_id(rec.col_1, rec.col_2, rec.col_3); --use your function to build the unique ID we want to submit into the new tables
INSERT INTO table_1 (column_unique_id)
VALUES (unique_id_l);
END LOOP;
END;

Use the CONCAT function:
DECLARE
n1 NUMBER := 1234;
n2 NUMBER := 567;
n3 NUMBER := 890;
c VARCHAR2(2000);
BEGIN
c := CONCAT(CONCAT(n1, '|'), CONCAT(CONCAT(n2, '|'), CONCAT(n3, '|')));
DBMS_OUTPUT.PUT_LINE('''' || c || '''');
END;
Share and enjoy.

Related

In PL/SQL can I set a variable dynamically?

In Oracle 12c R2, I have a function which receives a row type as a variable. In the function I want to read a table which contains a column name and a value, I want to then populate the row type variable passed in using the column name and the data from the table I read.
Here is a simplistic idea of what I want to do;
CREATE TABLE table_to_be_updated
(
key_value number,
cola varchar2(2),
colb varchar2(2),
colc varchar2(2),
cold varchar2(2),
cole varchar2(2),
colf varchar2(2)
);
CREATE TABLE table_default_value
(
default_stuff number,
column_name varchar(30),
column_default_value varchar2(2)
);
function do_defaults(in_table table_to_be_updated%rowtype, in_value number) return table_to_be_updated%rowtype
is
out_table table_to_be_updated%rowtype := in_table;
cursor my_curs
is
select * from table_default_value where default_stuff = in_value;
begin
for default_rec in my_curs
loop
out_table.[default_rec.column_name] := default_rec.column_default_value
end loop;
return out_table;
end;
insert into table_default_value (default_stuff,column_name,column_default_value) values (1,'cola','xx'));
insert into table_default_value (default_stuff,column_name,column_default_value) values (1,'colc','aa'));
insert into table_default_value (default_stuff,column_name,column_default_value) values (1,'cole','bb'));
In the line;
out_table.[default_rec.column_name] := [default_rec.column_default_value]
[default_rec.column_name] would be the column name, from the cursor, in out_table name I want to move data to.
and
[default_rec.column_default_value] is the value from the cursor I want to move into that column.
I suspect that what I want to do is impossible in PL/SQL, but I thought I'd ask.
There are other ways to accomplish updating the table directly, specifically using dynamic SQL with execute immediate, but I have a number of similar tables which all need to have the same things done to them, and I would prefer a single function to work on a record and then pass it back to have the calling routine update the proper table.
Here is the best I can come up with;
function do_defaults(in_table table_to_be_updated%rowtype, in_value number) return table_to_be_updated%rowtype
is
TYPE DEFAULT_TYPE IS TABLE OF VARCHAR2(2)
INDEX BY VARCHAR2(30);
DEFAULT_ARRAY DEFAULT_TYPE;
out_table table_to_be_updated%rowtype := in_table;
cursor my_curs
is
select * from table_default_value where default_stuff = in_value;
begin
DEFAULT_ARRAY('cola') := null;
DEFAULT_ARRAY('colb') := null;
DEFAULT_ARRAY('colc') := null;
DEFAULT_ARRAY('cold') := null;
DEFAULT_ARRAY('cole') := null;
DEFAULT_ARRAY('colf') := null;
for default_rec in my_curs
loop
DEFAULT_ARRAY(default_rec.column_name) := default_rec.column_default_value
end loop;
out_table.cola := DEFAULT_ARRAY('cola');
out_table.colb := DEFAULT_ARRAY('colb');
out_table.colc := DEFAULT_ARRAY('colc');
out_table.cold := DEFAULT_ARRAY('cold');
out_table.cole := DEFAULT_ARRAY('cole');
out_table.colf := DEFAULT_ARRAY('colf');
return out_table;
end;

How does Oracle Insert Into work when order of values is not defined?

I came across some code that looks like this. I understand that it will return the auto-generated id, but what I don't understand is when I pass cursor data when I call this function, how does it identify what values are to be inserted in which columns when the column order is not defined?
FUNCTION INSERT_ROW(DATA IN OWNER.TABLE%ROWTYPE)
RETURN OWNER.TABLE.ID%TYPE
IS
l_ID OWNER.MY_TABLE.ID%TYPE;
l_Data OWNER.MY_TABLE%ROWTYPE := DATA;
BEGIN
INSERT INTO OWNER.MY_TABLE
VALUES l_Data
RETURNING ID INTO l_ID;
I tried to look up many examples and I only come across ones where the values are defined in order like this
INSERT INTO my_table (val2, val3, val4) VALUES (2, 3, 4) RETURNING val1
INTO val1;
The order of columns in a table in Oracle IS defined. Take a look at the ALL_TAB_COLUMNS view - there's a COLUMN_ID column which defines the order of columns within the table. If a field list is not given in a SELECT (i.e. SELECT * FROM MY_TABLE) the columns from MY_TABLE will be returned in ALL_TAB_COLUMNS.COLUMN_ID order. This is also the same way columns are ordered in a %ROWTYPE variable, and it's the way that an INSERT which doesn't have a field list specified expects fields to be ordered.
The insert values statement in your code is a PL/SQL extension to the standard insert values clause that has parentheses. This is a page from the 12.2 manual about this topic:
https://docs.oracle.com/en/database/oracle/oracle-database/12.2/lnpls/INSERT-statement-extension.html#GUID-D81224C4-06DE-4635-A850-41D29D4A8E1B
The OWNER.TABLE%ROWTYPE data type defines a record with the same columns as the table and in the same order. You are just passing the data into the function in that format and passing it into a variable and then into the insert statement.
The main purpose of the RETURNING clause is to obtain the value of a derived column, a value which is generated during the insert process. Usually this is a technical primary key derived from a sequence, or since 12c an IDENTITY column.
So for instance:
create table my_table (
val1 number generated as identity primary key
, val2 varchar2(16)
, val3 varchar2(16)
, val4 date)
/
declare
id number;
begin
INSERT INTO my_table (val2, val3, val4)
VALUES ('one', 'test', sysdate)
RETURNING val1 INTO id;
dbms_output.put_line('new id = ' || id);
end;
/
This is why the examples you found specify columns in the INSERT projection: the value of the primary key is generated automatically, so there's no point in us assigning it a value in our code.
Now your function uses a record type in its insert statement. We can't do that with IDENTITY columns. This variant ...
declare
lrec my_table%rowtype;
id number;
begin
lrec.val2 := 'two';
lrec.val3 := 'test again';
lrec.val4 := sysdate;
INSERT INTO my_table
VALUES lrec
RETURNING val1 INTO id;
dbms_output.put_line('new id = ' || id);
end;
/
... will hurl
ORA-32795: cannot insert into a generated always identity column
But we can use a %rowtype with the old-fashioned sequence and trigger combo:
create table my_table (
val1 number primary key
, val2 varchar2(16)
, val3 varchar2(16)
, val4 date)
/
create sequence my_seq start with 42;
create or replace trigger my_trg
before insert on my_table for each row
begin
:new.val1 := my_seq.nextval;
end;
/
declare
lrec my_table%rowtype;
id number;
begin
lrec.val1 := 1;
lrec.val2 := 'three';
lrec.val3 := 'test again';
lrec.val4 := sysdate;
INSERT INTO my_table
VALUES lrec
RETURNING val1 INTO id;
dbms_output.put_line('new id = ' || id);
end;
/
Here is a LiveSQL demo (free Oracle OTN account required, alas). If you run it you will see that the trigger overrides the assigned value and the val1 column has the value from the sequence.

How to deal with sequence in insert from XMLTable?

I have write a PL/SQL function that takes input in XML format for the
following table:
TABLE: TBL_MEDICAL_CENTER_BILLS
Name Null Type
------------- -------- -------------
MED_RECORDNO NOT NULL NUMBER
MED_EMPID NVARCHAR2(10)
MED_BILL_HEAD NVARCHAR2(20)
MED_DATE DATE
MED_AMOUNT FLOAT(126)
Here is the function code:
FUNCTION save_medical_center_bills(medical_bill_data NVARCHAR2 ) RETURN clob IS ret clob;
xmlData XMLType;
v_code NUMBER;
v_errm VARCHAR2(100);
BEGIN
xmlData:=XMLType(medical_bill_data);
INSERT INTO TBL_MEDICAL_CENTER_BILLS SELECT x.* FROM XMLTABLE('/medical_center_bill'
PASSING xmlData
COLUMNS MED_RECORDNO NUMBER PATH 'MED_RECORDNO' default null,
MED_EMPID NVARCHAR2(11) PATH 'employee_id',
MED_BILL_HEAD NVARCHAR2(20) PATH 'bill_head' ,
MED_DATE DATE PATH 'effective_date',
MED_AMOUNT FLOAT PATH 'bill_amount'
) x;
ret:=to_char(sql%rowcount);
COMMIT;
RETURN '<result><status affectedRow='||ret||'>success</status></result>';
EXCEPTION
WHEN OTHERS THEN
v_code := SQLCODE;
v_errm := SUBSTR(SQLERRM, 1, 100);
DBMS_OUTPUT.PUT_LINE (v_code || ' ' || v_errm);
-- '<result><status>Error</status> <error_message>'|| 'Error Code:' || v_code || ' ' || 'Error Message:' || v_errm ||'</error_message> </result>';
RETURN '<result><status>Error</status> <error_message>'|| 'Error Message:' || v_errm ||'</error_message> </result>';
END save_medical_center_bills;
However, I want to keep table's first column MED_RECORDNO as incrementing sequence (at the moment I am keeping it null since I don't know how to put the sequence in the XMLTable clause) and the rest of the
inputs [MED_EMPID, MED_BILL_HEAD , MED_DATE , MED_AMOUNT] will be taken from the XML passed to the function.
I created a sequence and a trigger to keep this sequence incremented for that table column MED_RECORDNO:
CREATE SEQUENCE MED_RECORDNO_SEQ;
create or replace TRIGGER MED_RECORDNO_TRIGGER
BEFORE INSERT ON TBL_MEDICAL_CENTER_BILLS FOR EACH ROW
WHEN (new.MED_RECORDNO is null)
DECLARE
v_id TBL_MEDICAL_CENTER_BILLS.MED_RECORDNO%TYPE;
BEGIN
SELECT MED_RECORDNO_seq.nextval INTO v_id FROM DUAL;
:new.MED_RECORDNO := v_id;
END;
As you can see, my XMLTable is inserting 4 column values in a 5 column table, because columns MED_RECORDNO will take its value from sequence MED_RECORDNO_SEQ using TRIGGER MED_RECORDNO_TRIGGER.
I don't know any thing about doing this. If you have ever experience such things, then please share your idea.
I sort of hinted at this in an earlier answer. You should specify the names of of the columns in the table you are inserting into; this is good practice even if you are populating all of them, as it will avoid surprises if the table structure changes (or differs between environments), and makes it much easier to spot like having columns or values in the wrong order.
INSERT INTO TBL_MEDICAL_CENTER_BILLS (MED_EMPID, MED_BILL_HEAD, MED_DATE, MED_AMOUNT)
SELECT x.MED_EMPID, x.MED_BILL_HEAD, x.MED_DATE, x.MED_AMOUNT
FROM XMLTABLE('/medical_center_bill'
PASSING xmlData
COLUMNS MED_EMPID NVARCHAR2(11) PATH 'employee_id',
MED_BILL_HEAD NVARCHAR2(20) PATH 'bill_head' ,
MED_DATE DATE PATH 'effective_date',
MED_AMOUNT FLOAT PATH 'bill_amount'
) x;
The insert you have should actually work (if the column order in the table matches); the trigger will still replace the null value you get from the XMLTable with the sequence value. At least, until you make the MED_RECORDNO column not-null, and you probably want to if it's the primary key.
Incidentally, if you're on 11g or higher your trigger can assign the sequence straight to the NEW pseudorecord:
create or replace TRIGGER MED_RECORDNO_TRIGGER
BEFORE INSERT ON TBL_MEDICAL_CENTER_BILLS
FOR EACH ROW
BEGIN
:new.MED_RECORDNO := MED_RECORDNO_seq.nextval;
END;
The when null check implies you sometimes want to allow a value to be specified; that is a bad idea as manually inserted values can clash with sequence values, either giving you duplicates or a unique/primary key exception.

Updating table in SQLPLUS (Stored Procedure Loop with Comma Delimited Column)

Having some trouble writing my stored procedure. Using Oracle 11g
Goal: I want to be able to create separate rows in my table "info_table" from my table "places_table" with the column alternatenames. Under the column alternatenames from places_table, there is a comma delimited string with multiple alternate names. I want to create a row for each one of these alternate names in table "info_table".
ex of alternatenames column string:
Beijing,Beijingzi,Pei-ching-tzu
what I am hoping to achieve
ID Name
100000000 Beijing
100000001 Beijingzi
100000002 Pei-ching-tzu
Currently my code looks like this:
CREATE TABLE INFO_TABLE
(
INFOID NUMBER PRIMARY KEY,
NAME VARCHAR2(500),
LANGUAGE VARCHAR2(40),
STATUS VARCHAR2(50),
COUNTRY_CODE CHAR (10),
COUNTRY_CODE_2 CHAR (10),
GID CHAR(10),
SUPPLIERID CHAR(10),
LAST_MODIFIED CHAR(50)
);
CREATE SEQUENCE INFO_COUNTER
START WITH 100000000;
CREATE PROCEDURE LOAD_ALTERNATE_NAMES(ALTERNATENAMES_COLUMN VARCHAR2)
AS
COMMA_FINDER NUMBER := 1;
BEGIN
IF ALTERNATENAMES_COLUMN IS NOT NULL
THEN
<<SEPARATE_ALTERNATENAMES>> WHILE COMMA_FINDER!=0 LOOP
INSERT INTO INFO_TABLE
(INFOID, NAME, LANGUAGE, STATUS, COUNTRY_CODE, COUNTRY_CODE_2, GID, SUPPLIERID, LAST_MODIFIED)
VALUES
(INFO_COUNTER, SUBSTR(ALTERNATENAMES_COLUMN, INSTR(P.ALTERNATENAMES, ',', COMMA_FINDER+1)), NULL, 'ALTERNATE', P.COUNTRY_CODE, P.COUNTRY_CODE_2, P.GID, NULL, P.LASTMODIFIED)
FROM INFO_TABLE I, PLACES_TABLE P;
COMMA_FINDER := INSTR(ALTERNATENAMES, ',', COMMA_FINDER);
END LOOP SEPARATE_ALTERNATENAMES;
COMMA_FINDER:=1;
ENDIF;
END
/
LOAD_ALTERNATE_NAMES(SELECT ALTERNATENAMES FROM PLACES_TABLE);
currently the problem is that my INSERT statement in my loop is giving me "SQL Statement Ignored" and I am not sure why. I have taken a look at the stored procedure and loop documentation but can't figure out if I am doing something wrong or there is a typo.
can someone help me please?
Thank you in advance,
Norman
The INSERT statement has either the form:
INSERT INTO table (...) VALUES (...)
or:
INSERT INTO table (...) SELECT ... FROM ...
That's why Oracle issues an error message.
But there's more. You pass the ALTERNATENAMES string value to the stored procedure but need more data from the PLACES_TABLE. Furthermore, Oracle doesn't support stored procedure calls like this:
LOAD_ALTERNATE_NAMES(SELECT ALTERNATENAMES FROM PLACES_TABLE);
So I propose you create a stored procedure without parameters:
CREATE PROCEDURE LOAD_ALTERNATE_NAMES
AS
COMMA_FINDER NUMBER;
BEGIN
FOR REC IN (
SELECT * FROM PLACES_TABLE WHERE ALTERNATENAMES IS NOT NULL
) LOOP
COMMA_FINDER NUMBER := 1;
<<SEPARATE_ALTERNATENAMES>> WHILE COMMA_FINDER!=0 LOOP
INSERT INTO INFO_TABLE
(INFOID, NAME, LANGUAGE, STATUS, COUNTRY_CODE, COUNTRY_CODE_2, GID, SUPPLIERID, LAST_MODIFIED)
VALUES
(INFO_COUNTER.NEXTVAL, SUBSTR(REC.ALTERNATENAMES, INSTR(REC.ALTERNATENAMES, ',', COMMA_FINDER+1)), NULL, 'ALTERNATE', REC.COUNTRY_CODE, REC.COUNTRY_CODE_2, REC.GID, NULL, REC.LASTMODIFIED);
COMMA_FINDER := INSTR(REC.ALTERNATENAMES, ',', COMMA_FINDER);
END LOOP SEPARATE_ALTERNATENAMES;
END LOOP;
END
/
I hope that helps you proceed. I haven't test it and I'm afraid that SUBSTR will fail once it reaches the last name. But you'll figure that out.
Here is a little function I use to loop things like you are asking for. You can specify a delimiter.
The type...
type split_array is table of varchar2(32767) index by binary_integer;
The function...
function split(string_in varchar2, delim_in varchar2) return split_array is
i number :=0;
pos number :=0;
lv_str varchar2(32767) := string_in;
strings split_array;
dl number;
begin
-- determine first chuck of string
pos := instr(lv_str,delim_in,1,1);
-- get the length of the delimiter
dl := length(delim_in);
if (pos = 0) then --then we assume there is only 1 items in the list. so we just add the delimiter to the end which would make the pos length+1;
strings(1) := lv_str;
end if;
-- while there are chunks left, loop
while ( pos != 0) loop
-- increment counter
i := i + 1;
-- create array element for chuck of string
strings(i) := substr(lv_str,1,pos-1);
-- remove chunk from string
lv_str := substr(lv_str,pos+dl,length(lv_str));
-- determine next chunk
pos := instr(lv_str,delim_in,1,1);
-- no last chunk, add to array
if pos = 0 then
strings(i+1) := lv_str;
end if;
end loop;
-- return array
return strings;
end split;
How to use it...
declare
/* alternatenames varchar2(32767) := 'one,two,three,four'; */
nameArray split_array;
begin
for c1 in ( select alternatenames from yourTable where alternatenames is not null )
loop
nameArray := split(c1.alternatenames,',');
for i in 1..nameArray.count loop
/* dbms_output.put_line(nameArray(i)); */
insert into yourTable ( yourColumn ) values ( nameArray(i) );
end loop;
end loop;
end;
/

Find specific varchar in Oracle Nested Table

I'm new to PL-SQL, and struggling to find clear documentation of operations are nested tables. Please correct any misused terminology etc.
I have a nested table type that I use as a parameters for a stored procedure.
CREATE OR REPLACE TYPE "STRARRAY" AS TABLE OF VARCHAR2 (255)
In my stored procedure, the table is initialized and populated. Say I have a VARCHAR2 variable, and I want to know true or false if that varchar exists in the nested table.
I tried
strarray.exists('somevarchar')
but I get an ORA-6502
Is there an easier way to do that other than iterating?
FOR i IN strarray.FIRST..strarray.LAST
LOOP
IF strarray(i) = value THEN
return 1;--found
END IF;
END LOOP;
For single value check I prefer the "member" operator.
zep#dev> declare
2 enames strarray;
3 wordToFind varchar2(255) := 'King';
4 begin
5 select emp.last_name bulk collect
6 into enames
7 from employees emp;
8 if wordToFind member of enames then
9 dbms_output.put_line('Found King');
10 end if;
11 end;
12 /
Found King
PL/SQL procedure successfully completed
zep#dev>
You can use the MULTISET INTERSECT operator to determine whether the string you're interested in exists in the collection. For example
declare
l_enames strarray;
l_interesting_enames strarray := new strarray( 'KING' );
begin
select ename
bulk collect into l_enames
from emp;
if( l_interesting_enames = l_interesting_enames MULTISET INTERSECT l_enames )
then
dbms_output.put_line( 'Found King' );
end if;
end;
will print out "Found King" if the string "KING" is an element of the l_enames collection.
You should pass an array index, not an array value to an exists in case you'd like to determine whether this element exists in collection. Nested tables are indexed by integers, so there's no way to reference them by strings.
However, you might want to look at associative arrays instead of collections in case you wish to reference your array element by string index. This will look like this:
DECLARE
TYPE assocArray IS TABLE OF VARCHAR2(100) INDEX BY VARCHAR2(100);
myArray assocArray;
BEGIN
myArray('foo') := 'bar';
IF myArray.exists('baz') THEN
dbms_output.put_line(myArray('baz'));
ELSIF myArray.exists('foo') THEN
dbms_output.put_line(myArray('foo'));
END IF;
END;
Basically, if your array values are distinct, you can create paired arrays referencing each other, like,
arr('b') := 'a'; arr('a') := 'b';
This technique might help you to easily look up any element and its index.
When a nested table is declared as a schema-level type, as you have done, it can be used in any SQL query as a table. So you can write a simple function like so:
CREATE OR REPLACE FUNCTION exists_in( str VARCHAR2, tab stararray)
RETURN BOOLEAN
AS
c INTEGER;
BEGIN
SELECT COUNT(*)
INTO c
FROM TABLE(CAST(tab AS strarray))
WHERE column_value = str;
RETURN (c > 0);
END exists_in;

Resources