How to properly use an oracle in the request, the "If" and "LIKE" condition - sql

i need help with the code, i have two pieces of code that write two variables: v_responsible_job and v_responsible.I would like to combine these queries into one and add a condition if.
If the answer to this query is "ABC", then the first query must be completed, and if not,the second query:
https://dbfiddle.uk/?rdbms=oracle_18&fiddle=4ce28fad7e33f00c5538e49943ae7dechere is the demo data
SELECT NAME FROM data_separators WHERE id = way.DS_ID
example answer:
NAME
ABC Info
Scool
offise
ABC SHOP
first call :
BEGIN
SELECT full_name, job INTO v_responsible, job_id
FROM physical_persons
WHERE id IN (SELECT physical_person
FROM data_separators WHERE id = way.DS_ID) AND rownum = 1;
SELECT name
INTO v_responsible_job
FROM jobs
WHERE id = job_id AND
rownum = 1;
if v_responsible_job is not null then
if length(v_responsible_job) > 0 then
v_responsible_job := ', '|| v_responsible_job;
end if;
end if;
EXCEPTION WHEN OTHERS THEN
v_responsible_job := '';
END;
second call :
BEGIN
SELECT full_name, job
INTO v_responsible, job_id
FROM physical_persons
WHERE id IN (SELECT RESPONSIBLE
FROM ADRESSES
WHERE name = p1.name) AND
rownum = 1;
SELECT name
INTO v_responsible_job
FROM jobs
WHERE id = job_id AND
rownum = 1;
if v_responsible_job is not null then
if length(v_responsible_job) > 0 then
v_responsible_job := ', '|| v_responsible_job;
end if;
end if;
EXCEPTION WHEN OTHERS THEN
v_responsible_job := '';
END;
my varianr answer , but his don't work(
BEGIN
select count(*) into v_count_word FROM data_separators WHERE id = way.DS_ID and NAME LIKE '%ABC%';
select count(RESPONSIBLE) into v_count_respon FROM ADRESSES WHERE name = p1.name;
if v_count_word > 0 then
SELECT full_name, job INTO v_responsible, job_id FROM physical_persons WHERE id IN (SELECT physical_person FROM data_separators WHERE id = way.DS_ID) AND rownum = 1;
SELECT name INTO v_responsible_job FROM jobs WHERE id = job_id AND rownum = 1;
if v_responsible_job is not null then
if length(v_responsible_job) > 0 then
v_responsible_job := ', '|| v_responsible_job;
end if;
end if;
end if;
if v_count_respon > 0 and v_count_word = 0 then
SELECT full_name, job INTO v_responsible, job_id FROM physical_persons WHERE id IN (SELECT RESPONSIBLE
FROM ADRESSES
WHERE name = p1.name) AND rownum = 1;
SELECT name INTO v_responsible_job FROM jobs WHERE id = job_id AND rownum = 1;
if v_responsible_job is not null then
if length(v_responsible_job) > 0 then
v_responsible_job := ', '|| v_responsible_job;
end if;
end if;
end if;
if v_count_respon = 0 and v_count_word = 0 then
SELECT full_name, job INTO v_responsible, job_id FROM physical_persons WHERE id IN (SELECT physical_person FROM data_separators WHERE id = way.DS_ID) AND rownum = 1;
SELECT name INTO v_responsible_job FROM jobs WHERE id = job_id AND rownum = 1;
if v_responsible_job is not null then
if length(v_responsible_job) > 0 then
v_responsible_job := ', '|| v_responsible_job;
end if;
end if;
end if;
EXCEPTION WHEN OTHERS THEN
v_responsible_job := '-';
END;

Combine the first two queries using EXISTS and then use LISTAGG to aggregate the values:
DECLARE
v_responsible_jobs VARCHAR2(4000);
BEGIN
SELECT LISTAGG( name, ',' ) WITHIN GROUP ( ORDER BY name )
INTO v_responsible_jobs
FROM jobs j
WHERE EXISTS(
SELECT 1
FROM physical_persons pp
WHERE pp.job_id = j.job_id
AND id IN (
SELECT physical_person
FROM data_separators
WHERE id = way.DS_ID
)
);
END;
/
An other way would be to use BULK COLLECT INTO a collection data type and then to iterate over the collection concatenating the string; but that is more complicated than using LISTAGG to do all the work for you.

Related

pl/sql multiple if or multiple begin when data found block

I am new to writing stored procedures. I want to see which is the best way to execute a functionality like below:
cursor c1 is select * from cases where caseid = '2332534534';
begin
for t_case in c1
loop
/..///
if t_case.oCode is not null then
select id into v_ao from lkp_ao where upper(descr) = upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode));
if v_ao = 0 then
select id into v_ao from lkp_ao where substr(upper(descr),1,5) like substr(upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode)),1,5)||'%' and rownum=1;
if v_ao = 0 then
select id into v_ao from lkp_ao where substr(upper(descr),1,5) like substr(upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode)),1,4)||'%' and rownum=1;
if v_ao = 0 then
select id into v_ao from lkp_ao where substr(upper(descr),1,5) like substr(upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode)),1,3)||'%' and rownum=1;
if v_ao = 0 then
v_ao := '';
end if;
end if;
end if;
end if;
else
v_ao := '';
end if;
/..///
end loop;
commit;
exception
when others then
log_error(0, 'INSERT_case - exception outside' || SQLERRM || ' code ' || SQLCODE, 1);
commit;
end;
This part of code would not work. so instead of select id into v_ao , i will have to check
select count(*) into v_ao from lkp_ao where upper(descr) = upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode));
if v_ao = 0 then
//do something
else
select id into v_ao from lkp_ao where upper(descr) = upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode));
end if;
So i am executing 1 query for count and 1 query to get the actual id in the else part.
The other way of doing is using begin when no data found execute the 2nd query and inside that no data found open another begin and so on. So basically there will be 5 begin and exception when no data found block which i feel is huge code.
Whats the simplest way to do this kind of condition in oracle stored procedure? thanks in advance.
Get all the matching rows in a single statement (so you do not have to query the table multiple times) and then use a CASE statement to order the rows and only get the best match:
DECLARE
v_substr CASES.OCODE%TYPE;
cursor c1 is select * from cases where caseid = '2332534534';
BEGIN
for t_case in c1
loop
/* ... */
if t_case.oCode is not null then
IF instr(t_case.oCode,',') = 0 THEN
v_substr := UPPER(t_case.oCode);
ELSE
v_substr := UPPER(SUBSTR(t_case.oCode, 1, INSTR(t_case.oCode,',')-1));
END IF;
v_ao := ''; -- same as NULL
BEGIN
SELECT id
INTO v_ao
from lkp_ao
WHERE substr(upper(descr),1,3) = SUBSTR(v_substr, 1, 3)
ORDER BY
CASE
WHEN upper(descr) = v_substr
THEN 1
WHEN substr(upper(descr),1,5) = SUBSTR(v_substr, 1, 5)
THEN 2
WHEN substr(upper(descr),1,4) = SUBSTR(v_substr, 1, 4)
THEN 3
WHEN substr(upper(descr),1,3) = SUBSTR(v_substr, 1, 3)
THEN 4
END
FETCH FIRST ROW ONLY;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END IF;
/* .. */
end loop;
commit;
exception
when others then
log_error(0, 'INSERT_case - exception outside' || SQLERRM || ' code ' || SQLCODE, 1);
commit; -- Really commit after an exception?
END;
/
If you want to make it even more efficient then do all the matching in the cursor query (then you do not have to context switch in every iteration of the cursor between PL/SQL and SQL):
DECLARE
v_substr CASES.OCODE%TYPE;
CURSOR c1 IS
SELECT c.*,
l.id AS ao
FROM (
SELECT c.*,
CASE INSTR(oCode, ',')
WHEN 0 THEN oCode
ELSE SUBSTR(oCode, 1, INSTR(oCode, ',') - 1)
END AS term
FROM cases c
) c
LEFT OUTER JOIN LATERAL (
SELECT id
from lkp_ao a
WHERE substr(upper(a.descr),1,3) = SUBSTR(c.term, 1, 3)
ORDER BY
CASE
WHEN upper(a.descr) = c.term
THEN 1
WHEN substr(upper(a.descr),1,5) = SUBSTR(c.term, 1, 5)
THEN 2
WHEN substr(upper(a.descr),1,4) = SUBSTR(c.term, 1, 4)
THEN 3
WHEN substr(upper(a.descr),1,3) = SUBSTR(c.term, 1, 3)
THEN 4
END
FETCH FIRST ROW ONLY
) l
ON (1 = 1)
WHERE caseid = '2332534534';
BEGIN
for t_case in c1
loop
/* ... */
v_ao := t_case.ao;
/* .. */
end loop;
commit;
exception
when others then
log_error(0, 'INSERT_case - exception outside' || SQLERRM || ' code ' || SQLCODE, 1);
commit; -- Really commit after an exception?
END;
/
Well,
if you declare additional variables and run all select statements (each of them returning its own v_ao# value) and
apply the max function so that query wouldn't end up with NO_DATA_FOUND; it'll return NULL instead
SQL> select 1 from dual where 1 = 2;
no rows selected
SQL> select max(1) From dual where 1 = 2;
MAX(1)
----------
SQL>
then you wouldn't have to enclose each select into its own begin-exception-end block
use nested CASE expressions to return the final result
Something like this:
declare
v_ao1 number;
v_ao2 number;
v_ao3 number;
v_ao4 number;
v_ao number;
begin
for t_case in c1 loop
select max(id) into v_ao1 from lkp_ao where upper(descr) = upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode));
select max(id) into v_ao2 from lkp_ao where substr(upper(descr),1,5) like substr(upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode)),1,5)||'%' and rownum=1;
select max(id) into v_ao3 from lkp_ao where substr(upper(descr),1,5) like substr(upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode)),1,4)||'%' and rownum=1;
select max(id) into v_ao4 from lkp_ao where substr(upper(descr),1,5) like substr(upper(nvl(substr(t_case.oCode, 1, instr(t_case.oCode,',')-1), t_case.oCode)),1,3)||'%' and rownum=1;
v_ao := case when nvl(v_ao1, 0) = 0 then
case when nvl(v_ao2, 0) = 0 then
case when nvl(v_ao3, 0) = 0 then
case when nvl(v_ao4, 0) = 0 then null
else v_ao4
end
else v_ao3
end
else v_ao2
end
else v_ao1
end;
end loop;
end;
Sometimes, in such cases, it's better to get length of longest match. One of the variants for this is to use UTL_RAW.BIT_XOR:
with
t(s) as (
select 'AAAABB' from dual union all
select 'AAAABC' from dual union all
select 'AAABBC' from dual union all
select 'BBBXXXX' from dual union all
select 'BBBYYY' from dual
)
,search_strings(str) as (
select 'AAAAB' from dual union all
select 'BBBZZZ' from dual
)
select
t.s,s.str,
utl_raw.bit_xor(utl_raw.cast_to_raw(t.s),utl_raw.cast_to_raw(s.str)) s_xor,
length(
regexp_substr(
utl_raw.bit_xor(utl_raw.cast_to_raw(t.s),utl_raw.cast_to_raw(s.str))
,'^(00)+'
)
)/2 as n_matches
from t, search_strings s
;
Results:
S STR S_XOR N_MATCHES
------- ------ ------------------------------ ----------
AAAABB AAAAB 000000000042 5
AAAABC AAAAB 000000000043 5
AAABBC AAAAB 000000030043 3
BBBXXXX AAAAB 030303191A5858
BBBYYY AAAAB 030303181B59
AAAABB BBBZZZ 0303031B1818
AAAABC BBBZZZ 0303031B1819
AAABBC BBBZZZ 030303181819
BBBXXXX BBBZZZ 00000002020258 3
BBBYYY BBBZZZ 000000030303 3
10 rows selected.
As you can see, BIT_XOR returns '00' for equal chars so you can count a number of 00 in it. Column n_matches returns this number of matched symbols

How to assign the same value in certain block with PostgreSQL?

Here , I want to assign all the value of router_index in the same block with the first row of index value in this block.
E.g. The router_index value from Row 1 to 4 should be 5,383 and from 5 to 7 should be 2,703...
I wonder if I can do it with pure PostgreSQL ?
Thanks for your help!!!!
do $$
declare
cnt integer := 0;
head_index_id varchar := '0';
cur_index_id varchar := '0';
fixed_index varchar := '10';
cnt_limit integer;
begin
select max(per_id) from detailed_router into cnt_limit;
while cnt <= cnt_limit loop
select router_index from detailed_router where per_id = cnt into cur_index_id;
if head_index_id = '0' and cur_index_id != fixed_index then
head_index_id := cur_index_id;
else
if cur_index_id = fixed_index then
-- update
update detailed_router set router_index = head_index_id where per_id = cnt;
else
select router_index from detailed_router where per_id = cnt into head_index_id;
end if;
end if;
raise notice 'cnt %', cnt;
raise notice 'cur_index_id %', cur_index_id;
raise notice 'head_index_id %', head_index_id;
cnt := cnt + 1;
end loop;
end $$
If you want "10" values to be filled with the previous non-"10" value and you have a column that specifies the ordering, you can use window functions:
select t.*,
max(router_index) over (partition by driver_index_code, grp) as imputed_router_index
from (select t.*,
count(*) filter (where router_index > 10) over (partition by driver_index_code order by <ordering column>) as grp
from t
) t;

PL/SQL procedure to output line the given date if not existing, latest date should be given

I have this table informationvalues with the contents:
Now I create a procedure where I need to input a date parameter which should output line the correct attr with given price. If the date doesn't exist the latest date should be selected.
The solution table for to_date('01-jan-19') would look like this:
This would be then output line in the procedure.
Should I select to correct tuple and output line it or would it be best to just bulk collect everything and then check in a for loop with an if statement what tuple I need to display.
What I have so far:
A select statement with the tuples I am looking for:
create or replace procedure print_inf_value(closingDate Date) is
cursor d1 (closingDate Date) is
select t.attr, t.dateOfValue, t.price
from (
select i.*,
row_number() over (
partition by attr
order by case when dateOfValue = closingdate then 1 else 2 end, dateOfValue desc
) rn
from InformationValues i
) t
where t.rn = 1;
BEGIN
dbms_output.put_line('Information Value ');
dbms_output.put_line('--------------------------------');
FOR d1_rec IN d1 LOOP
dbms_output.put_line(d1_rec.attr || ' ' || d1_rec.price );
END LOOP;
END;
Or a procedure where I bulk collect everything and then I need to sort out what tuple I need:
create or replace procedure print_inf_value(closingDate Date) is
TYPE d1 IS TABLE OF informationvalues%rowtype;
emps d1;
begin select * bulk collect into emps
from informationvalues;
FOR i IN 1 .. emps.COUNT LOOP
if emps(i).dateofvalue = closingDate then
dbms_output.put_line(emps(i).attr || ' ' || emps(i).price );
/*else*/
end if;
END LOOP;
END;
Both are not working right, so what am I missing to display tuple with the correct date.
Please try:
CREATE OR REPLACE PROCEDURE print_inf_value (closingDate DATE)
IS
BEGIN
DBMS_OUTPUT.put_line (RPAD ('ATTR', 20) || RPAD ('PRICE', 20));
FOR o
IN (select attr, trim(case when price < 1 then to_char(price,90.9) else to_char(price) end) price from (
select attr, price, dateofvalue,
row_number() over (partition by attr order by dateofvalue desc) rn from informationvalues
) i where dateofvalue = closingdate
or (rn = 1 and not exists (select 1 from informationvalues iv where iv.attr = i.attr and dateofvalue = closingdate) )
)
LOOP
DBMS_OUTPUT.put_line (RPAD (o.attr, 20) || RPAD ( o.price, 20));
END LOOP;
END;
Sample execution:
set serveroutput on;
begin
print_inf_value(date'2019-01-01');
end;
Output:
ATTR PRICE
age 2
electronics 0.5
gender 3
hobbies 0.5
homeAddress 7
maritalStatus 1
mobilePhone 5
musicTaste 0.1
socialContacts 1

using dynamic sql to create column for select statement

I'm writing a stored procedure for paginated results and this result can be ordered by certain values. I did have a switch case in a select statement but because it was trying to do an orderby on rownum it was very slow.
Now I am trying to use dyanmic sql to build the query outside the select but I don't know if what I am doing is possible.
Here is my SQL in Oracle SQL Developer:
create or replace PROCEDURE Sp_tsa_trainees_pagination (
schemeid IN INT,
searchval IN VARCHAR2,
pagesize IN INT DEFAULT 20,
currentpage IN INT DEFAULT 1,
--orderby IN VARCHAR2,
cursor_ OUT SYS_REFCURSOR)
AS
-- LOCAL VARIABLES
totalcount INT;
numberofpages INT;
startposition NUMBER;
endposition NUMBER;
orderby VARCHAR2(100) := 'surname asc' ;
dynamic_query VARCHAR(255) := 'row_number() over (order by t.SURNAME DESC, t.FORENAMES DESC) AS rnum';
BEGIN
-- Get total number of trainees in scheme
select COUNT(t.ORG_REGISTRATION_ID)
into totalcount FROM v_trainee t
where t.ORG_REGISTRATION_ID = schemeid
AND t.status = 'A' and LOWER(t.trainee_name) like '%' || LOWER(searchval) || '%';
-- calculate number of pages in the pagination by dividing total number of records by how many to display for each page
numberofpages := totalcount / pagesize;
-- get start position by multiplying number of records to display for each page by current page
startposition := pagesize *( currentpage-1);
-- add calculated start position by number of records to display to get end position
endposition := startposition + pagesize;
CASE orderby
WHEN 'surname desc' THEN dynamic_query := 'row_number() over (order by t.SURNAME DESC, t.FORENAMES DESC) AS rnum';
WHEN 'surname asc' THEN dynamic_query := 'row_number() over (order by t.SURNAME ASC, t.FORENAMES ASC) AS rnum';
END CASE;
OPEN cursor_ FOR
Select * from
(
SELECT
-- order by based on selection
dynamic_query rnum,
t.ORG_REGISTRATION_ID SearchId,
t.FORENAMES Forenames,
t.FORENAME Forename,
t.SURNAME Surname,
t.person_id PersonId,
t.trainee_name TraineeName,
t.STATUS Status,
t.IPD_ANNUAL_REVIEW_DATE AnnualReviewDate,
t.ANNUAL_REVIEW_STATUS AnnualReviewStatus,
t.payment_received PaymentRecieved,
t.TRAINEE_ID TraineeId,
t.IPD_SIGNUP_DATE IpdSignupDate,
t.START_DATE StartDate,
t.END_DATE EndDate,
t.LENGTH_ON_SCHEME LengthOnScheme,
t.EMPLOYEE_NUMBER EmploymentNumber,
t.SELECTED_LEVEL SelectedLevel,
t.SELECTED_LEVEL_DESCRIPTION SelectedLevelDescription,
t.ELIGIBLE_LEVEL EligibleLevel,
t.ELIGIBLE_LEVEL_DESCRIPTION EligibleLevelDescription,
sce.FORENAMES SceForenames,
sce.FORENAME SceForename,
sce.SURNAME SceSurname,
sce.mentor_name SceName,
sce.EMPLOYEE_NUMBER SceEmployeeNumber,
de.FORENAMES DeForenames,
de.FORENAME DeForename,
de.SURNAME DeSurname,
de.mentor_name DeName,
de.EMPLOYEE_NUMBER DeEmployeeNumber,
t.COMPLETED_ATTRIBUTE_LEVELS CompletedAttributeLevels,
t.ATTRIBUTE_LEVEL_COUNT AttributeLevelCount,
-- get percentage
CASE t.ATTRIBUTE_LEVEL_COUNT
WHEN 0 THEN 0
ELSE
COMPLETED_ATTRIBUTE_LEVELS / t.ATTRIBUTE_LEVEL_COUNT * 100
END percentage,
DECODE(F_ISTRAINEEGROUPMEMBER(t.ORG_REGISTRATION_ID, 'S', t.person_id),'Y','N','Y') WithoutTsaGroup,
orr.status SchemeStatus,
(select count(*) from TRAINING_GROUP_TRAINEE tgt where tgt.trainee_id = t.TRAINEE_ID) NUMBER_OF_GROUPS,
TotalCount
FROM v_trainee t
INNER JOIN org_registration orr ON t.ORG_REGISTRATION_ID = orr.id
LEFT OUTER JOIN v_mentor sce ON t.sce_id = sce.MENTOR_ID
LEFT OUTER JOIN v_mentor de ON t.de_id = de.MENTOR_ID
where t.ORG_REGISTRATION_ID = schemeid AND t.status = 'A'
and LOWER(t.trainee_name) like '%' || LOWER(searchval) || '%'
)
where rnum >= startposition and rnum <= endposition;
END;
I want to use this variable with the assigned sql:
dynamic_query rnum,
But when I execute the stored procedure I get this error:
ORA-01722: invalid number ORA-06512: at
"db.SP_TSA_TRAINEES_PAGINATION", line 46 ORA-06512: at line 13
So basically my question is can I assign a SQL to VARCHAR2 and then use it in a select statement dynamically.
You may need dynamic SQL for this. For example:
create or replace procedure testDyn(n in number, C OUT SYS_REFCURSOR) is
vDynamicPart varchar2(1000);
vSQl varchar2(1000);
begin
--
if (n = 1) then
vDynamicPart := 'count(1)';
else
vDynamicPart := 'count(null)';
end if;
--
vSQl := 'select ' || vDynamicPart || ' from dual';
open C for vSQl;
end;
If you call it
declare
n1 number;
n2 number;
C1 SYS_REFCURSOR;
C2 SYS_REFCURSOR;
begin
testDyn(1, C1);
testDyn(2, C2);
fetch C1 into n1;
fetch C2 into n2;
dbms_output.put_line('n1: ' || n1);
dbms_output.put_line('n2: ' || n2);
end;
you get:
n1: 1
n2: 0

Query doesn't update all rows

There's quite big table, more than 10 000 000 rows. It has columns OBJ_ID, DATE_OF_CHANGE, USER. And I added a new column, RECORD_ID, it is empty for now.
I need to update it so RECORD_ID should have numeric values ascending for OBJ_ID and DATE_OF_CHANGE.
I came up with this:
CREATE SEQUENCE REC_ID_SEQ
START WITH 1
INCREMENT BY 1
CACHE 100;
/
CREATE OR REPLACE TRIGGER TRG_REC_ID_SEQ
BEFORE INSERT ON T_HISTORY
FOR EACH ROW
BEGIN
:NEW.RECORD_ID := REC_ID_SEQ.NEXTVAL;
END;
/
DECLARE
O_ID NUMBER := 0;
S_DATE DATE := SYSDATE;
HIST_NUM NUMBER := 0;
LOOP_COUNT NUMBER := 0;
BEGIN
FOR O IN (SELECT ROWID ROW_ID, D.* FROM T_HISTORY D ORDER BY D.OBJ_ID, D.DATE_OF_CHANGE)
LOOP
LOOP_COUNT := LOOP_COUNT + 1;
IF O.OBJ_ID != O_ID OR O.DATE_OF_CHANGE!= S_DATE
THEN
HIST_NUM := HIST_NUM + 1;
END IF;
UPDATE T_HISTORY T SET T.RECORD_ID = HIST_NUM WHERE T.ROWID = O.ROW_ID;
O_ID := O.OBJ_ID;
S_DATE := O.DATE_OF_CHANGE;
IF LOOP_COUNT > 100000 THEN
COMMIT; LOOP_COUNT := 0;
END IF;
END LOOP;
END;
/
But when the command stops working (no errors) I see that about half of rows were not updated. How do I do this the right way?
Use MERGE command and rowid pseudocolumn as a substitute of primary key:
merge into T_HISTORY t
using (
select rownum as xx, t.*
from (
select t.*, rowid as x_rowid
from T_HISTORY t
order by OBJ_ID, DATE_OF_CHANGE
) t
) xx
on (xx.x_rowid = t.rowid )
when matched then update
set t.RECORD_ID = xx;
Live demo: http://sqlfiddle.com/#!4/aad05/2
Similar to #krokodilko's solution, using analytical function:
MERGE INTO t_history t
USING (SELECT obj_id,
date_of_change,
ROW_NUMBER () OVER (ORDER BY obj_id, date_of_change) rn
FROM t_history) r
ON (t.obj_id = r.obj_id AND t.date_of_change = r.date_of_change)
WHEN MATCHED
THEN
UPDATE SET t.record_id = r.rn;