You Asked
Hi Tom
Given the below JSON example, how do I know which notes and which occupants belong to which jobs (given that there is no unique identifier in the data itself? The relationship is defined by the JSON structure.
Is there a pseudo column or similar that can tell me the job array index that each record is from? I'm aware of FOR ORDINALITY but I think that is effectively a rownum and I assume there is no relationship between this and the actual array position?
WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT row_number,
job_name,
note_title,
note_text,
occupant_name,
occupant_gender
FROM json_table((select doc from json),'$.jobs[*]' NULL ON ERROR
COLUMNS(row_number FOR ORDINALITY,
job_name VARCHAR2(20) PATH '$.name',
NESTED PATH '$.notes[*]'
COLUMNS (note_title VARCHAR2(20) PATH '$.title',
note_text VARCHAR2(20) PATH '$.text'),
NESTED PATH '$.occupants[*]'
COLUMNS (occupant_name VARCHAR2(20) PATH '$.name',
occupant_gender VARCHAR2(20) PATH '$.gender')))
ROW_NUMBER JOB_NAME NOTE_TITLE NOTE_TEXT OCCUPANT_NAME OCCUPANT_GENDER
---------- -------------------- -------------------- -------------------- -------------------- --------------------
1 Wash Car Address 1 High Street
2 Wash Car Warning Scaffolding Required
3 Wash Car Mr Smith Male
4 Wash Car Mrs Smith Female
5 Wash Car Address 1 Another Street
6 Wash Car Mr Jones Male
7 Wash Car Mrs Jones Female
7 rows selected.
There is a one-to-many relationship between job and note and a on one-to-many relationship between job and occupant. I want to extract this data using PL/SQL into a jobs table with a child notes table and a child occupants table on which I'll create the primary key on the jobs records and the foreign key on the notes and occupants records.
The above select isn't overly helpful and to achieve this I'll probably need to loop on all the jobs first record, then get all the notes for each job record and all the occupants for each job record.
Get jobs:
WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT job_name
FROM json_table((select doc from json),'$.jobs[*]' NULL ON ERROR
COLUMNS(job_name VARCHAR2(20) PATH '$.name'))
JOB_NAME
--------------------
Wash Car
Wash Car
But, then when I proceed to get the notes records for each of these jobs records, I won't know what notes belong to which job. Similarly, with the occupants record:
WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT job_name,
note_title,
note_text
FROM json_table((select doc from json),'$.jobs[*]' NULL ON ERROR
COLUMNS(job_name VARCHAR2(20) PATH '$.name',
NESTED PATH '$.notes[*]'
COLUMNS (note_title VARCHAR2(20) PATH '$.title',
note_text VARCHAR2(20) PATH '$.text')))
/
JOB_NAME NOTE_TITLE NOTE_TEXT
-------------------- -------------------- --------------------
Wash Car Address 1 High Street
Wash Car Warning Scaffolding Required
Wash Car Address 1 Another Street
The only way I can think to achieve this is to select each jobs record one at a time in a loop using the array index i.e. jobs[0], jobs[1] etc and then get the notes for jobs[0] and then the notes for jobs[1] etc.
WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT job_name
FROM json_table((select doc from json),'$.jobs[0]' NULL ON ERROR
COLUMNS(job_name VARCHAR2(20) PATH '$.name'))
/
JOB_NAME
--------------------
Wash Car
Insert the job with a unique primary key identifier then get the notes and insert them into the notes table with a foreign key to the job primary key:
WITH json AS (
SELECT '{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}' doc
FROM dual
)
SELECT job_name,
note_title,
note_text
FROM json_table((select doc from json),'$.jobs[0]' NULL ON ERROR
COLUMNS(job_name VARCHAR2(20) PATH '$.name',
NESTED PATH '$.notes[*]'
COLUMNS (note_title VARCHAR2(20) PATH '$.title',
note_text VARCHAR2(20) PATH '$.text')))
/
JOB_NAME NOTE_TITLE NOTE_TEXT
-------------------- -------------------- --------------------
Wash Car Address 1 High Street
Wash Car Warning Scaffolding Required
Next get the job in array jobs[1] and then the notes and occupants for job in array jobs[1] etc.
This seems long winded to me. Am I missing a better way?
and Chris said...
I'm guessing you mean 12.1.0.2 for the version, as this is when json_table was released!
Anyway, so solve your problem it'd be nice if you could:
- Extract the jobs first with the names and notes as arrays
- Then convert these arrays to rows with further json_table expressions.
Unfortunately, if you try and nest them like this you get:
with json as
(select
'{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}'
doc
from dual
), jobs as (
select row_number, job_name, notes, names
from json_table (
( select doc from json ) ,
'$.jobs[*]' null on error
columns ( row_number for ordinality,
job_name varchar2 ( 20 ) path '$.name',
notes varchar2(1000) format json path '$.notes',
names varchar2(1000) format json path '$.occupants'
)
)
)
select row_number, job_name,
note_id, note_title, note_text,
name_id, name, gender
from jobs,
json_table(notes, '$[*]'
columns
note_id for ordinality,
note_title varchar2(30) path '$.title',
note_text varchar2(30) path '$.text'
),
json_table(names, '$[*]'
columns
name_id for ordinality,
name varchar2(30) path '$.name',
gender varchar2(30) path '$.gender'
);
ORA-40556: unsupported chaining of JSON_TABLE
:(
But there is a way around this!
Use the no_merge hint. This stops Oracle rewriting it one big json expression:
with json as
(select
'{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}'
doc
from dual
), jobs as (
select /*+ no_merge */row_number, job_name, notes, names
from json_table (
( select doc from json ) ,
'$.jobs[*]' null on error
columns ( row_number for ordinality,
job_name varchar2 ( 20 ) path '$.name',
notes varchar2(1000) format json path '$.notes',
names varchar2(1000) format json path '$.occupants'
)
)
)
select row_number, job_name,
note_id, note_title, note_text,
name_id, name, gender
from jobs,
json_table(notes, '$[*]'
columns
note_id for ordinality,
note_title varchar2(30) path '$.title',
note_text varchar2(30) path '$.text'
),
json_table(names, '$[*]'
columns
name_id for ordinality,
name varchar2(30) path '$.name',
gender varchar2(30) path '$.gender'
);
ROW_NUMBER JOB_NAME NOTE_ID NOTE_TITLE NOTE_TEXT NAME_ID NAME GENDER
1 Wash Car 1 Address 1 High Street 1 Mr Smith Male
1 Wash Car 1 Address 1 High Street 2 Mrs Smith Female
1 Wash Car 2 Warning Scaffolding Required 1 Mr Smith Male
1 Wash Car 2 Warning Scaffolding Required 2 Mrs Smith Female
2 Wash Car 1 Address 1 Another Street 1 Mr Jones Male
2 Wash Car 1 Address 1 Another Street 2 Mrs Jones Female
Do this and you can insert everything in a single statement with insert all!
You'll need to compute a row_number for each entity so you only insert the rows once. Then insert each row when the relevant rownum = 1:
create table jobs (
job_id int,
jb varchar2(20)
);
create table notes (
job_id int,
title varchar2(20),
text varchar2(30)
);
create table occupants (
job_id int,
name varchar2(20),
gender varchar2(30)
);
insert all
when rnj = 1 then into jobs values (row_number, job_name)
when rnnm = 1 then into occupants values (row_number, name, gender)
when rnnt = 1 then into notes values (row_number, note_title, note_text)
with json as
(select
'{"jobs":[{"name":"Wash Car","notes":[{"title":"Address","text":"1 High Street"},{"title":"Warning","text":"Scaffolding Required"}],"occupants":[{"name":"Mr Smith","gender":"Male"},{"name":"Mrs Smith","gender":"Female"}]},
{"name":"Wash Car","notes":[{"title":"Address","text":"1 Another Street"}],"occupants":[{"name":"Mr Jones","gender":"Male"},{"name":"Mrs Jones","gender":"Female"}]}]}'
doc
from dual
), jobs as (
select /*+ no_merge */row_number, job_name, notes, names
from json_table (
( select doc from json ) ,
'$.jobs[*]' null on error
columns ( row_number for ordinality,
job_name varchar2 ( 20 ) path '$.name',
notes varchar2(1000) format json path '$.notes'
,
names varchar2(1000) format json path '$.occupants'
)
)
)
select row_number, job_name,
note_id, note_title, note_text,
name_id, name, gender,
row_number() over (partition by row_number order by row_number) rnj,
row_number() over (partition by row_number, note_id order by row_number, name_id) rnnt,
row_number() over (partition by row_number, name_id order by row_number, note_id) rnnm
from jobs,
json_table(notes, '$[*]'
columns
note_id for ordinality,
note_title varchar2(30) path '$.title',
note_text varchar2(30) path '$.text'
),
json_table(names, '$[*]'
columns
name_id for ordinality,
name varchar2(30) path '$.name',
gender varchar2(30) path '$.gender'
);
select * from jobs;
JOB_ID JB
1 Wash Car
2 Wash Car
select * from occupants;
JOB_ID NAME GENDER
1 Mr Smith Male
1 Mrs Smith Female
2 Mr Jones Male
2 Mrs Jones Female
select * from notes;
JOB_ID TITLE TEXT
1 Address 1 High Street
1 Warning Scaffolding Required
2 Address 1 Another Street
Rating
(4 ratings)
Is this answer out of date? If it is, please let us know via a Comment