Unexpected behavior in Oracle when using Group By with JSON_TABLE - sql

I have a denormalized VIEW we'll call VIEW_X which looks like the following (It's just a regular simple view - not materialized or anything like that):
ID GROUP_ID PART_1_ID PART_2_ID
1 1723189 cd69f0f4-a5ed-4196-916d-401e98ffec75 X1
1 1723189 cd69f0f4-a5ed-4196-916d-401e98ffec75 X2
2 1723185 8d5132cb-1b6e-4e79-9698-fd1962eb808f K1
2 1723188 a191cb01-32ac-4ab4-bd6b-3ef777e395ca K1
It's denormalized in that it actually represents a structure like this:
{
id: 1,
group_id: 1723189,
part_1_id: 'cd69f0f4-a5ed-4196-916d-401e98ffec75'
part_2_ids: ["X1", "X2"]
}
the PART_2_ID in this view is the result of selecting from a JSON_TABLE where the data in the original table is stored in an array like ["X1", "X2"]:
JSON_TABLE(a.PART_2_IDS, '$' COLUMNS (
NESTED PATH '$[*]'
COLUMNS (
PART_2_ID VARCHAR2(4000) PATH '$'
)
)) p2
When I run a query like this on this view I get 0 results although the expected result is a single result with the ID of 2:
SELECT ID
FROM VIEW_X
WHERE PART_2_ID IN ('K1')
GROUP BY ID
HAVING COUNT(DISTINCT(PART_2_ID)) = 1
ID
--
(no results)
figure 1
Curiously enough if I run just the following I get the expected two results as there are two rows with ID 2 where there is a match on PART_2_ID as K1:
SELECT ID
FROM VIEW_X
WHERE PART_2_ID IN ('K1')
ID
--
2
2
If, however, I run either of the following queries I get a match on ID 1:
SELECT ID
FROM VIEW_X
WHERE PART_2_ID IN ('X1')
GROUP BY ID
HAVING COUNT(DISTINCT(PART_2_ID)) = 1
ID
--
1
SELECT ID
FROM VIEW_X
WHERE PART_2_ID IN ('X1', 'X2')
GROUP BY ID
HAVING COUNT(DISTINCT(PART_2_ID)) = 2
ID
--
1
I don't understand why figure 1 is not returning the expected result - is there something I'm overlooking? Is this a quirk with how JSON_TABLE works?

I cannot replicate this in:
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production; or
Oracle Database 18c Enterprise Edition Release 18.0.0.0.0 - Production on https://livesql.oracle.com
Oracle Setup:
CREATE TABLE table1 ( document CLOB CONSTRAINT ensure_json CHECK (document IS JSON) );
INSERT INTO table1 ( document ) VALUES ( '{"id":1,"group_id":1723189,"part_1_id":"cd69f0f4-a5ed-4196-916d-401e98ffec75","part_2_ids":["X1","X2"]}' );
INSERT INTO table1 ( document ) VALUES ( '{"id":2,"group_id":1723185,"part_1_id":"8d5132cb-1b6e-4e79-9698-fd1962eb808f","part_2_ids":["K1"]}' );
INSERT INTO table1 ( document ) VALUES ( '{"id":2,"group_id":1723188,"part_1_id":"a191cb01-32ac-4ab4-bd6b-3ef777e395ca","part_2_ids":["K1"]}' );
CREATE VIEW VIEW_X AS
SELECT p.*
FROM table1 t
CROSS JOIN
JSON_TABLE(
t.document,
'$'
COLUMNS (
id PATH '$.id',
group_id PATH '$.group_id',
part_1_id PATH '$.part_1_id',
NESTED PATH '$.part_2_ids[*]'
COLUMNS (
PART_2_ID VARCHAR2(4000) PATH '$'
)
)
) p;
Query 1:
SELECT *
FROM VIEW_X;
Results:
ID GROUP_ID PART_1_ID PART_2_ID
---------- ---------- ------------------------------------ ---------
1 1723189 cd69f0f4-a5ed-4196-916d-401e98ffec75 X1
1 1723189 cd69f0f4-a5ed-4196-916d-401e98ffec75 X2
2 1723185 8d5132cb-1b6e-4e79-9698-fd1962eb808f K1
2 1723188 a191cb01-32ac-4ab4-bd6b-3ef777e395ca K1
Query 2:
SELECT ID
FROM VIEW_X
WHERE PART_2_ID IN ('K1')
GROUP BY ID
HAVING COUNT(DISTINCT(PART_2_ID)) = 1;
Results:
ID
--
2

Related

How to count how many times a tag appear inside a CLOB PLSQL oracle 11g

i have a clob column in a table. In this clob i have a XML and i want to count how many times a tag appear inside this clob.
For example:
<TPQ>
<LTP>N<LTP>
<SUBLTP>N</SUBLTP>
<TIMES>446</TIMES>
<TIMES>321</TIMES>
<TIMES>546</TIMES>
<TIMES>547</TIMES>
<LTP>N</LTP>
<LTP2>N<LTP2>
<SUBLTP>N</SUBLTP>
<NODES>1</NODES>
<NODES>2</NODES>
<NODES>3</NODES>
<NODES>4</NODES>
<SUBLTP>H</SUBLTP>
<SUBLTP3>A</SUBLTP3>
<SUBLTP2>N</SUBLTP2>
<LTP2>N</LTP2>
</TPQ>
I want to know that the tag "TIMES" appears 4 times, and tag "NODES" appears 4 times.
Im using this query for getting all TIMES tag but i need know how to count:
SELECT EXTRACT(xmltype.createxml(T.columnCLOB), '//TPQ/LTP/TIMES').getStringVal()
FROM table1 T;
and the result is this:
Result of the Select Statement is
<TIMES>446</TIMES><TIMES>321</TIMES><TIMES>546</TIMES><TIMES>547</TIMES>
This in a example, i need a solution for a dinamic clob column that can have x tags inside, not always with the same structure. But i only need to know how many times appears a specified tag.
You can use:
SELECT t.id,
x.tag_name,
COUNT(*)
FROM table_name t
CROSS JOIN XMLTABLE(
'//*'
PASSING XMLTYPE(t.xml)
COLUMNS
tag_name varchar2(100) path 'name()'
) x
GROUP BY t.id, x.tag_name
Which, for the sample data:
CREATE TABLE table_name (id NUMBER, xml CLOB);
INSERT INTO table_name (id, xml)
VALUES (1, '<TPQ>
<LTP>N</LTP>
<SUBLTP>N</SUBLTP>
<TIMES>446</TIMES>
<TIMES>321</TIMES>
<TIMES>546</TIMES>
<TIMES>547</TIMES>
<LTP>N</LTP>
<LTP2>N</LTP2>
<SUBLTP>N</SUBLTP>
<NODES>1</NODES>
<NODES>2</NODES>
<NODES>3</NODES>
<NODES>4</NODES>
<SUBLTP>H</SUBLTP>
<SUBLTP3>A</SUBLTP3>
<SUBLTP2>N</SUBLTP2>
<LTP2>N</LTP2>
</TPQ>');
Outputs:
ID
TAG_NAME
COUNT(*)
1
LTP
2
1
LTP2
2
1
SUBLTP2
1
1
NODES
4
1
TPQ
1
1
SUBLTP
3
1
TIMES
4
1
SUBLTP3
1
If you only want a specific tag name and want to aggregate the tags' contents then:
SELECT t.id,
x.tag_name,
COUNT(*),
LISTAGG(x.value, ',') WITHIN GROUP (ORDER BY value) AS contents
FROM table_name t
CROSS JOIN XMLTABLE(
'//TIMES'
PASSING XMLTYPE(t.xml)
COLUMNS
tag_name VARCHAR2(100) PATH 'name()',
value VARCHAR2(4000) PATH 'text()'
) x
GROUP BY t.id, x.tag_name
Which outputs:
ID
TAG_NAME
COUNT(*)
CONTENTS
1
TIMES
4
321,446,546,547
db<>fiddle here
XPATH functions can be used
with
x as
(select xmltype('<TPQ><LTP>N</LTP><SUBLTP>N</SUBLTP>
<TIMES>446</TIMES><TIMES>321</TIMES><TIMES>546</TIMES><TIMES>547</TIMES>
<LTP>N</LTP><LTP2>N</LTP2><SUBLTP>N</SUBLTP>
<NODES>1</NODES><NODES>2</NODES><NODES>3</NODES><NODES>4</NODES>
<SUBLTP>H</SUBLTP><SUBLTP3>A</SUBLTP3><SUBLTP2>N</SUBLTP2><LTP2>N</LTP2></TPQ>') xval
from dual)
select z.*
from x, xmltable ('count(/TPQ/TIMES)' passing x.xval) z;

How to insert long-format data into two separate tables using SQL?

I have selected the following data that I want to insert into the database.
Letter
Value
A
1
A
2
B
3
B
4
Since there is a repetition of "A" and "B" in this format, I want to split data into two separate tables: table1 and table2.
table1:
ID
Letter
1
A
2
B
ID here is automatically inserted by database (using a sequence).
table2:
table1_id
Value
1
1
1
2
2
3
2
4
In this particular example, I don't gain anything on storage but it illustrates in the best way what problem I have encountered.
How can I use SQL or PL/SQL to insert data into table1 and table2?
First populate table1 from the source
insert table1(Letter)
select distinct Letter
from srcTable;
Then load data from the source decoding letter to id
insert table2(table1_id, Value)
select t1.id, src.value
from srcTable src
join table1 t1 on t1.Letter = src.Letter;
You may use multitable insert with workaround to get stable nextval on sequence. Since nextval is evaluated on each row regardless of when condition, it is not sufficient to use it inside values.
insert all
when rn = 1 then into l(id, val) values(seq, letter)
when rn > 0 then into t(l_id, val) values(seq, val)
with a(letter, val) as (
select 'A', 1 from dual union all
select 'A', 2 from dual union all
select 'B', 3 from dual union all
select 'B', 4 from dual union all
select 'C', 5 from dual
)
, b as (
select
a.*,
l.id as l_id,
row_number() over(partition by a.letter order by a.val asc) as rn
from a
left join l
on a.letter = l.val
)
select
b.*,
max(decode(rn, 1, coalesce(
l_id,
extractvalue(
/*Hide the reference to the sequence due to restrictions
of multitalbe insert*/
dbms_xmlgen.getxmltype('select l_sq.nextval as seq from dual')
, '/ROWSET/ROW/SEQ/text()'
) + 0
))
) over(partition by b.letter) as seq
from b
select *
from l
ID | VAL
-: | :--
1 | A
2 | B
3 | C
select *
from t
L_ID | VAL
---: | --:
1 | 1
1 | 2
2 | 3
2 | 4
3 | 5
db<>fiddle here
Principally you need to produce and ID value for the table1 to be inserted into table2. For this, You can use INSERT ... RETURNING ID INTO v_id statement after creating the tables containing some constraints especially unique ones such as PRIMARY KEY and UNIQUE
CREATE TABLE table1( ID INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, letter VARCHAR2(1) NOT NULL );
ALTER TABLE table1 ADD CONSTRAINT uk_tab1_letter UNIQUE(letter);
CREATE TABLE table2( ID INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, table1_id INT, value INT );
ALTER TABLE table2 ADD CONSTRAINT fk_tab2_tab1_id FOREIGN KEY(table1_id) REFERENCES table1 (ID);
and adding exception handling in order not to insert repeating letters to the first table. Then use the following code block ;
DECLARE
v_id table1.id%TYPE;
v_letter table1.letter%TYPE := 'A';
v_value table2.value%TYPE := 1;
BEGIN
BEGIN
INSERT INTO table1(letter) VALUES(v_letter) RETURNING ID INTO v_id;
EXCEPTION WHEN OTHERS THEN NULL;
END;
INSERT INTO table2(table1_id,value) SELECT id,v_value FROM table1 WHERE letter = v_letter;
COMMIT;
END;
/
and run by changing the initialized values for v_letter&v_value as 'A'&2, 'B'&1,'B'&2 ..etc respectively.
Alternatively you can convert the code block to a stored procedure or function such as
CREATE OR REPLACE PROCEDURE Pr_Ins_Tabs(
v_letter table1.letter%TYPE,
v_value table2.value%TYPE
) AS
v_id table1.id%TYPE;
BEGIN
BEGIN
INSERT INTO table1(letter) VALUES(v_letter) RETURNING ID INTO v_id;
EXCEPTION WHEN OTHERS THEN NULL;
END;
INSERT INTO table2(table1_id,value) SELECT id,v_value FROM table1 WHERE letter = v_letter;
COMMIT;
END;
/
in order to revoke resiliently such as
BEGIN
Pr_Ins_Tabs('A',2);
END;
/
Demo
PS. If your DB verion is prior to 12c, then create sequences(seq1&seq2) and use seq1.nextval&seq2.nextval within the Insert statements as not possible to use GENERATED ALWAYS AS IDENTITY clause within the table creation DDL statements.

Select rows base on Subset

I've a scenario where I need to write sql query base on result of other query.
Consider the table data:
id attribute
1 a
1 b
2 a
3 a
3 b
3 c
I want to write query to select id base on attribute set.
I mean first I need to check attribute of id 1 using this query:
select attribute from table where id = 1
then base on this result I need to select subset of attribute. like in our case 1(a,b) is the subset of 3(a,b,c). My query should return 3 on that case.
And if I want to check base on 2(a) which is the subset of 1(a,b) and 3(a,b,c), it should return 1 and 3.
I hope, it's understandable. :)
You could use this query.
Logic is simple: If there isn't any item in A and isn't in B --> A is subset of B.
DECLARE #SampleData AS TABLE
(
Id int, attribute varchar(5)
)
INSERT INTO #SampleData
VALUES (1,'a'), (1,'b'),
(2,'a'),
(3,'a'),(3,'b'),(3,'c')
DECLARE #FilterId int = 1
;WITH temp AS
(
SELECT DISTINCT sd.Id FROM #SampleData sd
)
SELECT * FROM temp t
WHERE t.Id <> #FilterId
AND NOT EXISTS (
SELECT sd2.attribute FROM #SampleData sd2
WHERE sd2.Id = #FilterId
AND NOT EXISTS (SELECT * FROM #SampleData sd WHERE sd.Id = t.Id AND sd.attribute = sd2.attribute)
)
Demo link: Rextester
I would compose a query for that in three steps: first I'd get the attributes of the desired id, and this is the query you wrote
select attribute from table where id = 1
Then I would get the number of attributes for the required id
select count(distinct attribute) from table where id = 1
Finally I would use the above results as filters
select id
from table
where id <> 1 and
attribute in (
select attribute from table where id = 1 /* Step 1 */
)
group by id
having count(distinct attribute) = (
select count(distinct attribute) from table where id = 1 /* Step 2 */
)
This will get you all the id's that have a number of attributes among those of the initially provided id equal to the number the initial id has.

how to give column values as xml element names in Oracle

I have two tables. I am trying to join them and prepare an XML by using SQL/XML (SQLX) in oracle. Here the problem is XMLELEMENT function is taking hardcoded values for element name, but I want to have those names as a column data. Is it possible?
create table PRODUCTEDIT
(
PRODUCTEDIT_NUM NUMBER(12) primary key,
API_NAME VARCHAR2(255)
);
create table PRODUCTEDITPARAMETER
(
PRODUCTEDIT_NUM NUMBER(12) not null,
PARAMETER_SEQ NUMBER(9) not null,
PARAMETER_VALUE VARCHAR2(4000),
CONSTRAINT fk_producteditparameter
FOREIGN KEY (PRODUCTEDIT_NUM )
REFERENCES PRODUCTEDIT(PRODUCTEDIT_NUM )
);
There are 2 records in first table.
PRODUCTEDIT_NUM Api_Name
1 ModifyProd
2 CreateProd
Records in second table:
PRODUCTEDIT_NUM PARAMETER_SEQ PARAMETER_VALUE
1 1 10
1 2 Data
1 3 1
1 4 Data1
1 5 1
2 1 11
2 2 Voice
2 3 1
Now I want to get XMLOUTPUT like below:
<?xml version='1.0'?>
<ModifyProd>
<1>10</1>
<2>Data</2>
<3>1</3>
<4>Data1</4>
<5>1</5>
</ModifyProd>
<CreateProd>
<1>11</1>
<2>Voice</2>
<3>1</3>
</CreateProd>
In above XML we have XMLELEMENT names (ModifyProd,CreateProd, 1,2 e.t.c) coming from table data.I am not able to achieve that by using SQLXML in oracle.
I tried below, but doesn't seem to be working. XMLELEMENT is not taking the value the way I am passing.
SELECT XMLROOT(
XMLELEMENT(d.api_name,
(SELECT XMLAGG(
XMLELEMENT(e.parameter_seq,e.parameter_value
)
)
FROM producteditparameter e
WHERE e.productedit_num = d.productedit_num
)
),version '1.0', standalone yes
)
FROM productedit d
I assume you are looking for this:
WITH t AS
(SELECT 'foo' AS ELEMENT_NAME, 'bar' AS ELEMENT_CONTENT FROM dual)
SELECT XMLELEMENT(EVALNAME ELEMENT_NAME, ELEMENT_CONTENT)
FROM t;
<foo>bar</foo>
Update based on additional input
Your result is not a well-formed XML. A XML document must have only one single root element. So, either you ask for several XML documents, then you can do this:
SELECT
XMLELEMENT(
EVALNAME Api_Name,
XMLAGG(XMLELEMENT(EVALNAME parameter_seq, e.parameter_value) ORDER BY parameter_seq)
) AS xml_result
FROM PRODUCTEDITPARAMETER e
JOIN PRODUCTEDIT d USING (productedit_num)
GROUP BY productedit_num, Api_Name;
<ModifyProd><1>10</1><2>Data</2><3>1</3><4>Data1</4><5>1</5></ModifyProd>
<CreateProd><1>11</1><2>Voice</2><3>1</3></CreateProd>
or if you need a single XML you have to enclose it by another element, e.g.
SELECT
XMLELEMENT("Products",
XMLAGG(
XMLELEMENT(
EVALNAME Api_Name,
XMLAGG(XMLELEMENT(EVALNAME parameter_seq, e.parameter_value) ORDER BY parameter_seq)
)
)
) AS xml_result
FROM PRODUCTEDITPARAMETER e
JOIN PRODUCTEDIT d USING (productedit_num)
GROUP BY productedit_num, Api_Name;
<Products><ModifyProd><1>10</1><2>Data</2><3>1</3><4>Data1</4><5>1</5></ModifyProd><CreateProd><1>11</1><2>Voice</2><3>1</3></CreateProd></Products>
Another solution if I understood what you want:
WITH t AS
( SELECT LEVEL col1, 'test' || LEVEL col2
FROM DUAL
CONNECT BY LEVEL < 10)
SELECT XMLSERIALIZE (DOCUMENT (xmltype (CURSOR (SELECT col1, col2 FROM t))) INDENT SIZE = 0)
FROM DUAL;
Then, you can use a XSD to transform the output as you want.
Hope it helps

PostgreSQL query on text array value

I have a table where one column has an array - but stored in a text format:
mytable
id ids
-- -------
1 '[3,4]'
2 '[3,5]'
3 '[3]'
etc ...
I want to find all records that have the value 5 as an array element in the ids column.
I was trying to achieve this by using the "string to array" function and removing the [ symbols with the translate function, but couldn't find a way.
You can do this: http://www.sqlfiddle.com/#!1/5c148/12
select *
from tbl
where translate(ids, '[]','{}')::int[] && array[5];
Output:
| ID | IDS |
--------------
| 2 | [3,5] |
You can also use bool_or: http://www.sqlfiddle.com/#!1/5c148/11
with a as
(
select id, unnest(translate(ids, '[]','{}')::int[]) as elem
from tbl
)
select id
from a
group by id
having bool_or(elem = 5);
To see the original elements:
with a as
(
select id, unnest(translate(ids, '[]','{}')::int[]) as elem
from tbl
)
select id, '[' || array_to_string(array_agg(elem), ',') || ']' as ids
from a
group by id
having bool_or(elem = 5);
Output:
| ID | IDS |
--------------
| 2 | [3,5] |
Postgresql DDL is atomic, if it's not late yet in your project, just structure your stringly-typed array to a real array: http://www.sqlfiddle.com/#!1/6e18c/2
alter table tbl
add column id_array int[];
update tbl set id_array = translate(ids,'[]','{}')::int[];
alter table tbl drop column ids;
Query:
select *
from tbl
where id_array && array[5]
Output:
| ID | ID_ARRAY |
-----------------
| 2 | 3,5 |
You can also use contains operator: http://www.sqlfiddle.com/#!1/6e18c/6
select *
from tbl
where id_array #> array[5];
I prefer the && syntax though, it directly connotes intersection. It reflects that you are detecting if there's an intersection between two sets(array is a set)
http://www.postgresql.org/docs/8.2/static/functions-array.html
If you store the string representation of your arrays slightly differently, you can cast to array of integer directly:
INSERT INTO mytable
VALUES
(1, '{3,4}')
,(2, '{3,5}')
,(3, '{3}');
SELECT id, ids::int[]
FROM mytable;
Else, you have to put in one more step:
SELECT (translate(ids, '[]','{}'))::int[]
FROM mytable
I would consider making the column an array type to begin with.
Either way, you can find your row like this:
SELECT id, ids
FROM (
SELECT id, ids, unnest(ids::int[]) AS elem
FROM mytable
) x
WHERE elem = 5