I am trying to aggregate A into a list of dictionaries. Oracle's LISTAGG() works but does run into the 4k max char limit. I tried with XMLAGG but now I'm getting "and quot;" instead of "". Please suggest the best way of resolving this. XMLCAST ? Will the final outputs of listagg() and the work around be identical ?
A
N
{"1":"09","2":"11","3":"2010","4":"XYZ","5":""}
1
{"1":"09","2":"11","3":"2010","4":"XYZ","6":""}
2
{"1":"09","2":"11","3":"2010","4":"XYZ","7":""}
3
select
-- '[' ||
-- LISTAGG(cte.A, ' , ') WITHIN GROUP(
-- ORDER BY
-- cte.N
-- )
-- || ']'
'[' ||
rtrim(xmlagg(xmlelement(e,cte.A,', ').extract('//text()') order by cte.N).getclobval(),', ')
|| ']' aggr_lsts
from cte;
Bad Output:
[{"1":"09","2":"11","3":"2010","4":"XYZ","5":""}, {"1":"09","2":"11","3":"2010","4":"XYZ","6":""}, {"1":"09","2":"11","3":"2010","4":"XYZ","7":""}]
Good Output:
[{"1":"09","2":"11","3":"2010","4":"XYZ","5":""} , {"1":"09","2":"11","3":"2010","4":"XYZ","6":""} , {"1":"09","2":"11","3":"2010","4":"XYZ","7":""}]
Thank you.
You may use xmlcast function to correctly deserialize XML data as a clob.
Please note, that XMLType methods are deprecated since Oracle 11.2, so use XMLQUERY and XMLCAST instead.
with sample(col, rn) as (
select column_value, rownum
from sys.odcivarchar2list(
'&',
'>',
'<',
'"',
'correctly serializable text'
)
)
select
rtrim(xmlcast(xmlquery(
'//text()'
passing xmlagg(
xmlelement(e,col,', ')
order by rn
)
returning content
) as clob), ', ') as res
from sample
RES
&, >, <, ", correctly serializable text
But for 12.2 and above you may use JSON functions directly to produce correct JSON array: JSON_ARRAYAGG (with a restriction on 32k JSON keys per object, if I remember correctly).
with sample(col, rn) as (
select column_value, rownum
from sys.odcivarchar2list(
'{"a": 1, "b": "2"}',
'{"a": 2, "b": "qwe"}',
'{"a": 3, "c": "test"}'
)
)
select
json_arrayagg(
col order by rn
) as res
from sample
RES
["{"a": 1, "b": "2"}","{"a": 2, "b": "qwe"}","{"a": 3, "c": "test"}"]
fiddle
Don't use XML functions with JSON data. From Oracle 12.2, use JSONAGG to aggregate the rows:
SELECT JSON_ARRAYAGG(a FORMAT JSON ORDER BY n RETURNING CLOB) As output
FROM table_name
Note: you need FORMAT JSON to treat your input as valid JSON, otherwise it will be treated as a string and quoted. You also need RETURNING CLOB otherwise the default return type is a VARCHAR2 which is limited to 4000 characters and will give you the same issues as LISTAGG.
Which, for the sample data:
CREATE TABLE table_name (A, N) AS
SELECT '{"1":"09","2":"11","3":"2010","4":"XYZ","5":""}', 1 FROM DUAL UNION ALL
SELECT '{"1":"09","2":"11","3":"2010","4":"XYZ","6":""}', 2 FROM DUAL UNION ALL
SELECT '{"1":"09","2":"11","3":"2010","4":"XYZ","7":""}', 3 FROM DUAL;
Outputs:
OUTPUT
[{"1":"09","2":"11","3":"2010","4":"XYZ","5":""},{"1":"09","2":"11","3":"2010","4":"XYZ","6":""},{"1":"09","2":"11","3":"2010","4":"XYZ","7":""}]
fiddle
Related
I want to apply TRIM function for my columns. But TRIM after Format function is not working. It's not trimming the spaces.
If I do it before format as below then it gives me error for datatype because the columns have other datatypes than string and byte as well.
Please tell me a solution for this.
Meantime, you can apply some extra processing on top of original query to get desired result - as in below example
select *,
trim(replace(regexp_replace(format('%t', t), r' *, *| *\)|\( *', '/'), '/NULL/', '/_/'), '/') HashColumn
from your_table t
if applied to sample data
with your_table as (
select ' 1' A, '2 ' B, null C, 4 D union all
select ' 12 ', null, '4', 5
)
output is
Consider below approach
create temp function json_extract_values(input string) returns array<string> language js as """
return Object.values(JSON.parse(input));""";
select *,
( select string_agg(trim(value), '/')
from unnest(json_extract_values(replace(to_json_string(t), ':null', ':"_"'))) value
) as HashColumn
from your_table t
if applied to dummy data as below
with your_table as (
select ' 1' A, '2 ' B, null C, 4 D union all
select ' 12 ', null, '4', 5
)
output is
which, I hope, is exactly what you are looking for
this refers to a question asked by someone else previously
previous question
my question is how do I adapt this solution so that before any function/script is ran the name and value fields are stripped of any additional + and updated so no additional + remain.
For e.g.
Name Value
A+B+C+ 1+2+3+
A++B 1++2
this should be updated to
Name Value
A+B+C 1+2+3
A+B 1+2
once this update has taken place, I can run the solution provided in the previous question.
Thanks
You need to replace ++ with + and to remove the + at the end of the string.
/* sample data */
with input(Name, Value) as (
select 'A+B+C+' ,'1+2+3+' from dual union all
select 'A++B' ,'1++2' from dual
)
/* query */
select trim('+' from regexp_replace(name, '\+{2,}', '+') ) as name,
trim('+' from regexp_replace(value, '\+{2,}', '+') ) as value
from input
If you need to update a table, you may need:
update yourTable
set name = trim('+' from regexp_replace(name, '\+{2,}', '+') ),
value= trim('+' from regexp_replace(value, '\+{2,}', '+') )
In a more compact way, without the external trim ( assuming you have no leading +):
/* sample data */
with input(Name, Value) as (
select 'A+B+C+' ,'1+2+3+' from dual union all
select 'A++B+++C+' ,'1++2+++3+' from dual union all
select 'A+B' ,'1+2' from dual
)
/* query */
select regexp_replace(name, '(\+)+(\+|$)', '\2') as name,
regexp_replace(value, '(\+)+(\+|$)', '\2') as value
from input
You could use something on the lines of:
Select substr('1+2+3+', 0, length('1+2+3+')-1) from dual ;
Select replace('1++2', '++', '+') from dual;
I'm assuming you have the output already present in a variable you can play with.
EDIT:
Here's a function that can solve the problem (You can call this function in your select clauses thereby solving the problem):
CREATE OR REPLACE Function ReplaceChars
( name_in IN varchar2 )
RETURN varchar2
IS
changed_string varchar2(20) ;
BEGIN
changed_string:=replace(name_in, '++', '+') ;
CASE WHEN substr(changed_string, -1) in ('+')
then
changed_string:=substr(changed_string,0, length(changed_string) - 1) ;
else changed_string:=changed_string ;
end CASE ;
RETURN changed_string;
END;
You can use the below:
LTRIM(RTRIM (REGEXP_REPLACE (column_name, '\+{2,}', '+'), '+'),'+')
Eg:
SELECT LTRIM(RTRIM (REGEXP_REPLACE ('+A+++B+C+++D++', '\+{2,}', '+'), '+'),'+') VALUE
FROM DUAL;
returns output: A+B+C+D
if youre working with ssms, GIVE IT A GO:::
UPDATE tablename
SET colname=
CASE colname WHEN LIKE '%++%' THEN
WHILE colname LIKE '%++%'
(REPLACE(colname,++,+))
END LOOP
WHEN LIKE '%+' THEN
SUBSTR(colname, 1, LENGTH(colname)-1)
WHEN LIKE '+%' THEN
SUBSTR(colname, 2, LENGTH(colname))
ELSE
colname
END
I have to write a query on a table which has a varchar column. Value in this column may have a numbers as substring
Lets possible say the column values are
Data
-----------------------
abc=123/efg=143/ijk=163
abc=123/efg=153/ijk=173
now I have to query the table where data contains the numbers [123,143,163] but shouldnt contain any other number.
How can I write this select query ?
This looks like a very bad database design. If you are interested in separate information stored in a string, then don't store the string but the separate information in separate columns. Change this if possible and such queries will become super simple.
However, for the time being it's easy to find the records as described, provided there are always three numbers in the string as in your sample data. Add a slash at the end of the string, so every number has a leading = and a trailing /. Then look up the numbers in the string with LIKE.
select *
from mytable
where data || `/` like '%=123/%'
and data || `/` like '%=143/%'
and data || `/` like '%=163/%';
If these three numbers are in the string, then all numbers match. Hence there is no other number not matching.
If there can be more numbers in the string but no duplicates, then count equal signs to determine how many numbers are in the string:
select *
from mytable
where data || '/' like '%=123/%'
and data || '/' like '%=143/%'
and data || '/' like '%=163/%'
and regexp_count(data, '=') = 3;
And here is a query accepting even duplicate numbers in the string:
select *
from mytable
where regexp_count(data, '=') >= 3
and regexp_count(data, '=') =
regexp_count(data || '/', '=123/') +
regexp_count(data || '/', '=143/') +
regexp_count(data || '/', '=163/');
Oracle Setup:
CREATE TABLE table_name ( data ) AS
SELECT 'abc=123/efg=143/ijk=163' FROM DUAL UNION ALL
SELECT 'abc=123/efg=153/ijk=173' FORM DUAL;
Then you can create some virtual columns to represent the data:
ALTER TABLE table_name ADD abc GENERATED ALWAYS AS (
TO_NUMBER( REGEXP_SUBSTR( data, '(^|/)abc=(\d+)(/|$)', 1, 1, NULL, 2 ) )
) VIRTUAL;
ALTER TABLE table_name ADD efg GENERATED ALWAYS AS (
TO_NUMBER( REGEXP_SUBSTR( data, '(^|/)efg=(\d+)(/|$)', 1, 1, NULL, 2 ) )
) VIRTUAL;
ALTER TABLE table_name ADD ijk GENERATED ALWAYS AS (
TO_NUMBER( REGEXP_SUBSTR( data, '(^|/)ijk=(\d+)(/|$)', 1, 1, NULL, 2 ) )
) VIRTUAL;
And can add appropriate indexes if you want:
CREATE INDEX table_name__abc_efg_ijk__idx ON table_name( abc, efg, ijk );
Query:
Then if you are only going to have those three keys you can do:
SELECT abc, efg, ijk
FROM table_name
WHERE abc = 123
AND efg = 143
AND ijk = 163;
However, if you could get more than three keys and want ignore additional values then you could do:
CREATE TYPE intlist AS TABLE OF INT;
/
SELECT *
FROM table_name
WHERE INTLIST( 143, 123, 163 )
=
CAST(
MULTISET(
SELECT TO_NUMBER(
REGEXP_SUBSTR(
t.data,
'[^/=]+=(\d+)(/|$)',
1,
LEVEL,
NULL,
1
)
)
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.data, '[^/=]+=(\d+)(/|$)' )
)
AS INTLIST
);
This has the added bonus that INTLIST(123, 143, 163) can be passed as a bind parameter (depending on the client program you are using and the Oracle driver) so that you can simply change how many and what numbers you want to filter for (and that the order of the values does not matter).
Also, if you want it to contain at least those values then you can change INTLIST( ... ) = to INTLIST( ... ) SUBMULTISET OF.
I have a table from where I need to get only some part of record with comma after one part of record.
for example I have
ABCD [1000-1987] BCD[101928-876] adgs[10987-786]
I want to get the record like :
1000-1987,101928-876,10987-786
Can you please help me out to get the record as mentioned.
If you don't use 11g and do not want to use wm_concat:
WITH
my_data AS (
SELECT 'ABCD [1000-1987] BCD[101928-876] adgs[10987-786]' AS val FROM dual
)
SELECT
ltrim(
MAX(
sys_connect_by_path(
rtrim(ltrim(regexp_substr(val, '\[[0-9-]*\]', 1, level, NULL), '['), ']'),
',')
),
',') AS val_part
FROM my_data
CONNECT BY regexp_substr(val, '\[[0-9-]*\]', 1, level, NULL) IS NOT NULL
;
If using wm_concat is ok for you:
WITH
my_data AS (
SELECT 'ABCD [1000-1987] BCD[101928-876] adgs[10987-786]' AS val FROM dual
)
SELECT
wm_concat(rtrim(ltrim(regexp_substr(val, '\[[0-9-]*\]', 1, level, NULL), '['), ']')) AS val_part
FROM my_data
CONNECT BY regexp_substr(val, '\[[0-9-]*\]', 1, level, NULL) IS NOT NULL
;
If you use 11g:
WITH
my_data AS (
SELECT 'ABCD [1000-1987] BCD[101928-876] adgs[10987-786]' AS val FROM dual
)
SELECT
listagg(regexp_substr(val, '[a-b ]*\[([0-9-]*)\] ?', 1, level, 'i', 1), ',') WITHIN GROUP (ORDER BY 1) AS val_part
FROM my_data
CONNECT BY regexp_substr(val, '[a-b ]*\[([0-9-]*)\] ?', 1, level, 'i', 1) IS NOT NULL
;
Read more about string aggregation techniques: Tim Hall about aggregation techniques
Read more about regexp_substr: regexp_substr - Oracle Documentation - 10g
Read more about regexp_substr: regexp_substr - Oracle Documentation - 11g
You don't have to split and then aggregate it. You can use regexp_replace to keep only those characters within square brackets, then replace the square brackets by comma.
WITH my_data
AS (SELECT 'ABCD [1000-1987] BCD[101928-876] adgs[10987-786]' AS val
FROM DUAL)
SELECT RTRIM (
REPLACE (
REGEXP_REPLACE (val, '(\[)(.*?\])|(.)', '\2'),
']', ','),
',')
FROM my_data;
I have a string like this '102/103/104/106'
Now if i pass 102 as input then output should be the next field that is 103. if 103 then output should be 104 and if 106 then output should be null(as for last field I don't have any further expression). I can do this using procedure by splitting the string into arrays and comparing. But can I do this through sql statement something like this
select '102/103/104/106' from dual where [expression 102 or 103].
Thanks!!
You can do it in pure SQL with something like this:
--convert your string into rows
with vals as (
select
substr('102/103/104/106',
instr('102/103/104/106', '/', 1, level)-3,
3
) col,
level lvl
from dual
connect by level <= length('102/103/104/106')-length(replace('102/103/104/106', '/'))+1
)
select *
from (
select col,
lead(col) over (order by lvl) next_val -- find the next value in the list
from vals
)
where col = :val;
Basically, convert your string into rows by parsing it. Then use the analytic lead to find the "next" value.
-- p_whole_string = '102/103/104/106'
-- p_prev = '102'
select
regexp_substr(p_whole_string, '(^|/)' || p_prev || '/([^/]+)', 1, 1, null, 2)
as next
from dual;
Added NVL to return last value if 106 is entered:
SELECT NVL(REGEXP_SUBSTR('102/103/104/106', '(^|/)' || '106' || '/([^/]+)', 1, 1, null, 2), REGEXP_SUBSTR('102/103/104/106', '[^/]+$')) as nxt
FROM dual
/
works for Oracle form 10 up.
SELECT
REGEXP_SUBSTR(
REGEXP_SUBSTR('102/103/104/106', '(^|/)102/[^/]+'), -- returns 102/103
'[^/]+',1,2) val -- takes second part
FROM DUAL;
with parameters looks like this:
-- p_string_to_search = '102/103/104/106'
-- p_string_to_match = '102'
SELECT
REGEXP_SUBSTR(
REGEXP_SUBSTR(p_string_to_search, '(^|/)' || p_string_to_match ||'/[^/]+'), -- returns 102/103
'[^/]+',1,2) val -- takes second part
FROM DUAL;