I have to match the following URLs by writing a query in Amazon Redshift:
<some url>/www.abc.com/a/<more url>
<some url>/www.abc.com/b/<more url>
<some url>/www.abc.com/c/<more url>
<some url>/www.abc.com/d/<more url>
Here, obviously the "/www.abc.com/" is constant, but the text after '/' can change. It can take one of the many values that I have a list of (a,b,c,d in this case). How do I match this part that comes immediately after "/www.abc.com/"?
I can think of the following:
select text,
case
when text ilike '%/www.abc.com/' || <what should go here?> || '/%' then 'URLType1'
when <some other condition> then 'URLType2'
end as URLType
from table
I have to maintain the CASE structure.
Any help would be much appreciated.
THe options are:
1) put the list of values into a subquery and then join to this list like this:
with
value_list as (
select 'a' as val union select 'b' union select 'c' union select 'd'
)
select text
from table
join value_list
on text ilike '%/www.abc.com/' || val || '/%'
2) use OR:
select text,
case
when text ilike '%/www.abc.com/a/%'
or text ilike '%/www.abc.com/b/%'
or text ilike '%/www.abc.com/c/%'
or text ilike '%/www.abc.com/d/%'
then 'URLType1'
when <some other condition>
then 'URLType2'
end as URLType
from table
3) Write a Python UDF that takes the url and the list and returns true or false like this:
CREATE OR REPLACE FUNCTION multi_ilike(str varchar(max),arr varchar(max))
RETURNS boolean
STABLE AS $$
if str==None or arr==None:
return None
arr = arr.split(',')
str = str.lower()
for i in arr:
if i.lower() in str:
return True
return False
$$ LANGUAGE plpythonu;
select multi_ilike('<some url>/www.abc.com/a/<more url>','/www.abc.com/a/,/www.abc.com/b/,/www.abc.com/c/,/www.abc.com/d/'); -- returns true
select multi_ilike('<some url>/www.abc.com/F/<more url>','/www.abc.com/a/,/www.abc.com/b/,/www.abc.com/c/,/www.abc.com/d/'); -- returns false
Related
I'm trying to check whether or not a string ends in a year.
Input:
file_paths
wowefnowinf/wefionwe/wefowoi/2012-02-03
weofnweofn/weoew/2022-03-04
ewpfowe/ewopfew
Desired Output:
wowefnowinf/wefionwe/wefowoi/
weofnweofn/weoew/
ewpfowe/ewopfew
I'm having trouble first detecting that the strings themselves end in a date-format. This is my query:
SELECT CASE WHEN 'wowefnowinf/wefionwe/wefowoi/2012-02-03' LIKE '%/####\-##\-##'
THEN TRUE
ELSE FALSE END AS result
FROM my_table;
I should be getting true for this, but my query returns false. Any help would be appreciated.
In Snowflake you can make use of regexp_like and split_part:
with dummy as (
select 'wowefnowinf/wefionwe/wefowoi/2012-02-03' as file_path
union all
select 'weofnweofn/weoew/2022-03-04'
union all
select 'ewpfowe/ewopfew'
)
select
file_path,
split_part(file_path, '/', -1) as splitted_file_path,
regexp_like(splitted_file_path, '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]') as ends_with_date,
iff(ends_with_date, trim(file_path, splitted_file_path), file_path) as trimmed_file_path
from dummy;
Output:
I have 3 tables.
Table A | Table B | Table C
----------|-------------|------------
name | name | name
phrase | phrase | phrase
field 1 | include | include
field 2 | exclude | exclude
field 3 | field 1 | field 1
Table A, B, C contains a lot of other columns too but i am interested in only Table B (include and exclude) and Table C (include and exclude).
I am trying to write a function which will take name and phrase of Table A as parameter and make a query on Table B and Table C to get include and exclude columns of both tables where Table B (name and phrase) and Table C (name and phrase) are equal to parameters name and phrase.
The include and exclude columns are boolean and i want to use the B.include, B.exclude, C.include and C.exclude to return a string.
What i have written so far is but i am not sure if it is right.
CREATE OR REPLACE FUNCTION createString(name text, phrase text) RETURNS table (descp text) AS
$$
BEGIN
select b.include, b.exclude, c.include, c.exclude
from (TableB b join TableC c on b.name = c.name and b.phrase = c.phrase)
where b.name = name and b.phrase = phrase;
IF b.include = true THEN
NEW := 'b included';
ELSEIF b.exclude = true THEN
NEW := 'b excluded';
ELSEIF c.exclude = true THEN
NEW := 'c excluded';
ELSEIF c.exclude = true THEN
NEW := 'c excluded';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
I am new to writing functions and don't really know if i am doing it right. Can someone please help me point in right direction.
I don't recommend that you write a function for this. Functions tend to be performance killers.
Instead, you can write a view, using left join to bring the tables together:
For the logic you have written, you seem to want:
select a.*,
(case when b.include then 'B included'
when b.exclude then 'B excluded'
when c.include then 'C included'
when c.exclude then 'C excluded'
end) as string
from a left join
b
on a.name = b.name and a.phrase = b.phrase left join
c
on a.name = c.name and a.phrase = c.phrase;
This returns the first match on the flag. I might expect that you want the strings concatenated together in some way, but that is not the pseudo-code you have written.
I am still learning and what i wrote in original question was learn and trial code. I ended up writing something like this....
CREATE OR REPLACE FUNCTION createString(name text, phrase text) RETURNS TEXT AS
$$
DECLARE
moInclude boolean;
moExcluded boolean;
modaidInclude boolean;
modaidExclude boolean;
result text := 'None';
BEGIN
select mo.include, mo.exclude, modaid.include, modaid.exclude INTO moInclude, moExcluded, modaidInclude, modaidExclude
from (tableB AS mo JOIN tableC AS modaid on mo.text = modaid.text and mo.phrase = modaid.phrase)
where mo.text = text and mo.phrase = phrase;
IF moInclude = true AND modaidInclude = true THEN
result := 'INCLUDED';
ELSEIF (moExcluded = true) THEN
result := 'EXCLUDED';
ELSEIF ((moInclude = false AND modaidInclude = true AND modaidExclude = false) OR
(moInclude = false AND modaidInclude = false AND modaidExclude = true) OR
(moInclude = true AND modaidInclude = false AND modaidExclude = true) OR
(moInclude = true AND modaidInclude = false AND modaidExclude = false)) THEN
result := 'PARTIAL';
ELSE
result := 'NONE';
END IF;
RETURN result;
END;
$$ LANGUAGE plpgsql;
Calling this function when i am creating a view.
Thank you everyone for help.
#GordonLinoff is correct that this wold be better just using SQL instead of a function. But if you must have a function then you can create a SQL Function - function with language sql. The following converts your last function and reproduces it as a SQL Function.
Additionally since the columns Include and Exclude already defined as boolean, they don't need equal evacuation. That is "if include ..." will produce the same result as "if include = true". But for false values you need to negate the value so "if include = false" becomes simply "not include". I've used that, but it could be easily replaced.
-- create tables and gen test data
create table tableA (name text, phase text, field1 text, field2 text, field3 text) ;
create table TableB (name text, phase text, include boolean, exclude boolean, field1 text);
create table TableC (name text, phase text, include boolean, exclude boolean, field1 text);
insert into tableA (name,phase)
values ('n1','p1')
, ('n2','p1')
, ('n3','p1')
, ('n4','p1')
, ('n5','p1')
, ('n6',null);
insert into tableB(name, phase, include, exclude)
values ('n1','p1',true,true)
, ('n2','p1',true,false)
, ('n3','p1',false,true)
, ('n4','p1',false,false)
, ('n5','p1',false,false);
insert into tableC(name, phase, include, exclude)
values ('n1','p1',true,true)
, ('n2','p1',true,false)
, ('n3','p1',false,true)
, ('n4','p1',false,false)
, ('n5','p1',true,false);
-------------------------------------------------------------------------------------------------------------
create or replace function createString(name_in text, phase_in text)
returns text
language sql strict
as $$
select case when (mo.include and modaid.include) then 'INCLUDED'
when (mo.exclude) then 'EXCLUDED'
when ( (not mo.include and modaid.include and not modaid.exclude)
or (not mo.include and not modaid.include and modaid.exclude)
or (mo.include and not modaid.include and modaid.exclude)
or (mo.include and not modaid.include and not modaid.exclude)) then 'PARTIAL'
else 'NONE'
end
from tableb as mo
join tablec as modaid on ( mo.name = modaid.name
and mo.phase = modaid.phase
)
where mo.name = name_in
and mo.phase = phase_in
$$;
-------------------------------------------------------------------------------------------------------------
-- test
select name, phase, createString(name, phase)
from tableA;
I want to pivot a table based on a field which can contain "dynamic" values (not always known beforehand).
I can make it work by hard coding the values (which is undesirable):
SELECT *
FROM my_table
pivot(SUM(amount) FOR type_id IN (1,2,3,4,5,20,50,83,141,...));
But I can't make it work using a query to provide the values dynamically:
SELECT *
FROM my_table
pivot(SUM(amount) FOR type_id IN (SELECT id FROM types));
---
090150 (22000): Single-row subquery returns more than one row.
SELECT *
FROM my_table
pivot(SUM(amount) FOR type_id IN (SELECT ARRAY_AGG(id) FROM types));
---
001038 (22023): SQL compilation error:
Can not convert parameter 'my_table.type_id' of type [NUMBER(38,0)] into expected type [ARRAY]
Is there a way to accomplish this?
I don't think it's possible in native SQL, but I wrote an article and published some code showing how my team does this by generating the query from Python.
You can call the Python script directly, passing arguments similar to the options Excel gives you for pivot tables:
python generate_pivot_query.py \
--dbtype snowflake --database mydb \
--host myhost.url --port 5432 \
--user me --password myp4ssw0rd \
--base-columns customer_id \
--pivot-columns category \
--exclude-columns order_id \
--aggfunction-mappings amount=sum \
myschema orders
Or, if you're Airflow, you can use a CreatePivotTableOperator to create tasks directly.
I wrote a Snowflake stored procedure to get dynamics pivots inside Snowflake, check:
https://hoffa.medium.com/dynamic-pivots-in-sql-with-snowflake-c763933987c
3 steps:
Query
Call stored procedure call pivot_prev_results()
Find the results select * from table(result_scan(last_query_id(-2)))
The procedure:
create or replace procedure pivot_prev_results()
returns string
language javascript
execute as caller as
$$
var cols_query = `
select '\\''
|| listagg(distinct pivot_column, '\\',\\'') within group (order by pivot_column)
|| '\\''
from table(result_scan(last_query_id(-1)))
`;
var stmt1 = snowflake.createStatement({sqlText: cols_query});
var results1 = stmt1.execute();
results1.next();
var col_list = results1.getColumnValue(1);
pivot_query = `
select *
from (select * from table(result_scan(last_query_id(-2))))
pivot(max(pivot_value) for pivot_column in (${col_list}))
`
var stmt2 = snowflake.createStatement({sqlText: pivot_query});
stmt2.execute();
return `select * from table(result_scan('${stmt2.getQueryId()}'));\n select * from table(result_scan(last_query_id(-2)));`;
$$;
Inspired by my two predecessors, I created another stored proc that could be called to create even multi-grouped and multi-pivot emulated pivot queries:
create or replace procedure
test_pivot.public.get_full_pivot(
"source" varchar, // fully-qualified 'table/view_name' or full '(subquery)'
"row_headers" varchar, // comma-separated list of 1+ GROUP BY field names
"col_header1" varchar, // first (mandatory) PIVOT field name
"col_header2" varchar, // secondary (optional) PIVOT field name ('' if none)
"agg" varchar, // field name for the aggregate values
"aggf" varchar) // aggregate function (sum, avg, min, max, count...)
returns varchar
language javascript
as $$
// collect all distinct values for a column header field
function get_distinct_values(col_header) {
var vals = [];
if (col_header != '') {
var result = snowflake.execute(
{sqlText: `select distinct ${col_header}\n`
+ `from ${source}\n`
+ `order by ${col_header}`}); // nulls last!
while (result.next())
vals.push(result.getColumnValueAsString(1));
}
return vals;
}
var vals1 = get_distinct_values(col_header1);
var vals2 = get_distinct_values(col_header2);
// create and return the emulated pivot query, for one or two column header values
var query = `select ${row_headers}`;
if (vals2.length == 0)
for (const i in vals1) {
var cond1 = (vals1[i] == 'null'
? `${col_header1} is null` : `to_char(${col_header1})='${vals1[i]}'`);
query += `,\n ${aggf}(iff(${cond1}, ${agg}, null)) as "${vals1[i]}"`;
}
else
for (const i in vals1)
for (const j in vals2) {
var cond1 = (vals1[i] == 'null'
? `${col_header1} is null` : `to_char(${col_header1})='${vals1[i]}'`);
var cond2 = (vals2[j] == 'null'
? `${col_header2} is null` : `to_char(${col_header2})='${vals2[j]}'`);
query += `,\n ${aggf}(iff(${cond1} AND ${cond2}, ${agg}, null)) as "${vals1[i]}+${vals2[j]}"`;
}
query += `\nfrom ${source}\n`
+ `group by ${row_headers}\n`
+ `order by ${row_headers};`; // nulls last!
return query;
$$;
Call with:
call test_pivot.public.get_full_pivot(
'test_pivot.public.demographics',
'country, education', 'status', 'gender', 'sales', 'sum');
to generate the following SQL:
select country, education,
sum(iff(to_char(status)='divorced' AND to_char(gender)='F', sales, null)) as "divorced+F",
sum(iff(to_char(status)='divorced' AND to_char(gender)='M', sales, null)) as "divorced+M",
sum(iff(to_char(status)='married' AND to_char(gender)='F', sales, null)) as "married+F",
sum(iff(to_char(status)='married' AND to_char(gender)='M', sales, null)) as "married+M",
sum(iff(to_char(status)='single' AND to_char(gender)='F', sales, null)) as "single+F",
sum(iff(to_char(status)='single' AND to_char(gender)='M', sales, null)) as "single+M",
sum(iff(status is null AND to_char(gender)='F', sales, null)) as "null+F",
sum(iff(status is null AND to_char(gender)='M', sales, null)) as "null+M"
from test_pivot.public.demographics
group by country, education
order by country, education;
Which may return a result structured like:
I have the following function
CREATE OR REPLACE FUNCTION match_custom_filter(filters text[], id text)
RETURNS boolean LANGUAGE plpgsql as $$
DECLARE
r boolean;
BEGIN
execute format(
'SELECT 1 FROM trackings t LEFT JOIN visitors v ON v.id = t.visitor_id
WHERE v.id = ''%s'' AND %s',
id,
array_to_string(filters, ') AND ('))
into r;
RETURN r;
END $$;
select v.*, array_agg(g.name) as groups from visitors v join groups g on match_custom_filter(g.formatted_custom_filters, v.id)
where v.id = 'cov4pisw00000sjctfyvwq126'
group by v.id
This works fine when the filters are not empty. But it is also possible that a filter is empty, in which case I will have an dangling AND with no right hand side.
Error:
ERROR: syntax error at end of input
LINE 2: ... WHERE v.id = 'cov4pisw00000sjctfyvwq126' AND
^
QUERY: SELECT 1 FROM trackings t LEFT JOIN visitors v ON v.id = t.visitor_id
WHERE v.id = 'cov4pisw00000sjctfyvwq126' AND
CONTEXT: PL/pgSQL function match_custom_filter(text[],text) line 5 at EXECUTE statement
What's the best way to handle this?
UPDATE:
Example of how I generate the array of string filters based off JSONB array of filter objects
def build_condition(%{"filter" => filter, "field" => field, "value" => value}) when field in #default_values do
case filter do
"greater_than" -> "#{field} > #{value}"
"less_than" -> "#{field} < #{value}"
"is" -> "#{field} = '#{value}'"
"is_not" -> "#{field} <> '#{value}'"
..
First, a warning. What you are doing here gives you in-stored-proc sql injection. I highly recommend you reconsider so you can properly parameterize.
Now, having said this, the obvious option is to declare a text variable and then pre-process it.
In your DECLARE block you add:
filterstring text;
then in your body, you add:
filterstring := array_to_string(filters, ') AND ('))
IF filterstring = '' or filterstring is null THEN
filterstring := 'TRUE';
END IF;
Then you use filterstring in place of the array_to_string call in the format() call.
Note that any time you assemble a query anywhere by string interpolation you have the possibility of sql injection.
To protect against SQL injection you will need to rethink your approach a little bit. Your best option is not to use format() for your query to the extent possible. So:
execute 'SELECT 1 FROM trackings t
LEFT JOIN visitors v ON v.id = t.visitor_id
WHERE v.id = $1'
USING id;
That causes planning and filling in the value to happen on two different points. That works well in the case of a simple parameter. However it doesn't work well in the case of the dynamic filters.
Instead of passing a one-dimensional array in, you could pass a two dimensional (nx3 array) with three elements per line. These would be column name, operator, and value. You can sanitize the column name by passing it through quote_ident and the value by passing it through quote_literal but sanitizing the operators is likely to be a problem so my recommendation would be to whitelist these and throw an exception if the operator is not found. Something like:
DECLARE
...
op TEXT;
allowed_ops TEXT[] := ARRAY['=', '<=', '>='];
BEGIN
...
IF not(op = ANY(allowed_ops)) THEN
RAISE EXCEPTION 'Illegal operator in function, %', op;
END IF;
...
END;
This is not going to be easy but it is doable.
Since you have your filters in the form of a jsonb array to begin with, you should use that as a function parameter instead of a text[]. For one thing, it will allow you to protect against SQL-injection.
CREATE OR REPLACE FUNCTION match_custom_filter(filters jsonb, id text)
RETURNS boolean LANGUAGE plpgsql AS $$
DECLARE
f text;
r boolean;
BEGIN
IF jsonb_array_length(filters) = 0 THEN
-- If no filters are specified then run a straight SQL query against trackings
PERFORM * FROM trackings WHERE visitor_id = quote_literal(id);
RETURN FOUND;
ELSE
-- Build the filters from the jsonb array
SELECT string_agg(
-- Concatenate the parts from a single json object into a filter
quote_ident(j->>'field') || -- avoid SQL injection on column name
CASE j->>'type'
WHEN 'greater_than' THEN ' > '
...
END ||
quote_literal(j->>'value'), -- avoid SQL injection on value
-- Aggregate individual filters with the AND operator
' AND ') INTO f
FROM jsonb_array_elements(filters) j;
-- Run a dynamic query with the filters
EXECUTE format('SELECT true FROM trackings t
LEFT JOIN visitors v ON v.id = t.visitor_id
WHERE v.id = %L AND %s LIMIT 1', id, f) INTO r;
RETURN r;
END IF;
END $$;
You should call this function passing in the jsonb array, like so:
SELECT v.*, array_agg(g.name) AS groups
FROM visitors v JOIN groups g ON match_custom_filter(g.group->'filter', v.id)
WHERE v.id = 'cov4pisw00000sjctfyvwq126'
GROUP BY v.id;
I have below SQL as a part of a view. In one of the schema I am getting "String Concatenation is too long" error and not able to execute the view.
Hence I tried the TO_CLOB() and now VIEW is not throwing ERROR, but it not returning the result as well it keep on running..
Please suggest....
Sql:
SELECT Iav.Item_Id Attr_Item_Id,
LISTAGG(La.Attribute_Name
||'|~|'
|| Lav.Attribute_Value
||' '
|| Lau.Attribute_Uom, '}~}') WITHIN GROUP (
ORDER BY ICA.DISP_SEQ,LA.ATTRIBUTE_NAME) AS ATTR
FROM Item_Attribute_Values Iav,
Loc_Attribute_Values Lav,
Loc_Attribute_Uoms Lau,
Loc_Attributes La,
(SELECT *
FROM Item_Classification Ic,
CATEGORY_ATTRIBUTES CA
WHERE IC.DEFAULT_CATEGORY='Y'
AND IC.TAXONOMY_TREE_ID =CA.TAXONOMY_TREE_ID
) ICA
WHERE IAV.ITEM_ID =ICA.ITEM_ID(+)
AND IAV.ATTRIBUTE_ID =ICA.ATTRIBUTE_ID(+)
AND Iav.Loc_Attribute_Id =La.Loc_Attribute_Id
AND La.Locale_Id =1
AND Iav.Loc_Attribute_Uom_Id =Lau.Loc_Attribute_Uom_Id(+)
AND Iav.Loc_Attribute_Value_Id=Lav.Loc_Attribute_Value_Id
GROUP BY Iav.Item_Id;
Error:
ORA-01489: result of string concatenation is too long
01489. 00000 - "result of string concatenation is too long"
*Cause: String concatenation result is more than the maximum size.
*Action: Make sure that the result is less than the maximum size.
You can use the COLLECT() function to aggregate the strings into a collection and then use a User-Defined function to concatenate the strings:
Oracle Setup:
CREATE TYPE stringlist IS TABLE OF VARCHAR2(4000);
/
CREATE FUNCTION concat_List(
strings IN stringlist,
delim IN VARCHAR2 DEFAULT ','
) RETURN CLOB DETERMINISTIC
IS
value CLOB;
i PLS_INTEGER;
BEGIN
IF strings IS NULL THEN
RETURN NULL;
END IF;
value := EMPTY_CLOB();
IF strings IS NOT EMPTY THEN
i := strings.FIRST;
LOOP
IF i > strings.FIRST AND delim IS NOT NULL THEN
value := value || delim;
END IF;
value := value || strings(i);
EXIT WHEN i = strings.LAST;
i := strings.NEXT(i);
END LOOP;
END IF;
RETURN value;
END;
/
Query:
SELECT Iav.Item_Id AS Attr_Item_Id,
CONCAT_LIST(
CAST(
COLLECT(
La.Attribute_Name || '|~|' || Lav.Attribute_Value ||' '|| Lau.Attribute_Uom
ORDER BY ICA.DISP_SEQ,LA.ATTRIBUTE_NAME
)
AS stringlist
),
'}~}'
) AS ATTR
FROM your_table
GROUP BY iav.item_id;
LISTAGG is limited to 4000 characters unfortunately. So you may want to use another approach to concatenate the values.
Anyway ...
It is strange to see LISTAGG which is a rather new feature combined with error-prone SQL1992 joins. I'd suggest you re-write this. Are the tables even properly joined? It looks strange that there seems to be no relation between Loc_Attributes and, say, Loc_Attribute_Values. Doesn't have Loc_Attribute_Values a Loc_Attribute_Id so an attribute value relates to an attribute? It would be hard to believe that there is no such relation.
Moreover: Is it guaranteed that your classification subquery doesn't return more than one record per attribute?
Here is your query re-written:
select
iav.item_id as attr_item_id,
listagg(la.attribute_name || '|~|' || lav.attribute_value || ' ' || lau.attribute_uom,
'}~}') within group (order by ica.disp_seq, la.attribute_name) as attr
from item_attribute_values iav
join loc_attribute_values lav
on lav.loc_attribute_value_id = iav.loc_attribute_value_id
and lav.loc_attribute_id = iav.loc_attribute_id -- <== maybe?
join loc_attributes la
on la.loc_attribute_id = lav.loc_attribute_id
and la.loc_attribute_id = lav.loc_attribute_id -- <== maybe?
and la.locale_id = 1
left join loc_attribute_uoms lau
on lau.loc_attribute_uom_id = iav.loc_attribute_uom_id
and lau.loc_attribute_id = iav.loc_attribute_id -- <== maybe?
left join
(
-- aggregation needed to get no more than one sortkey per item attribute?
select ic.item_id, ca.attribute_id, min (ca.disp_seq) as disp_seq
from item_classification ic
join category_attributes ca on ca.taxonomy_tree_id = ic.taxonomy_tree_id
where ic.default_category = 'y'
group by ic.item_id, ca.attribute_id
) ica on ica.item_id = iav.item_id and ica.attribute_id = iav.attribute_id
group by iav.item_id;
Well, you get the idea; check your keys and alter your join criteria where necessary. Maybe this gets rid of duplicates, so LISTAGG has to concatenate less attributes, and maybe the result even stays within 4000 characters.
Xquery approach.
Creating extra types or function isn't necessary.
with test_tab
as (select object_name
from all_objects
where rownum < 1000)
, aggregate_to_xml as (select xmlagg(xmlelement(val, object_name)) xmls from test_tab)
select xmlcast(xmlquery('for $row at $idx in ./*/text() return if($idx=1) then $row else concat(",",$row)'
passing aggregate_to_xml.xmls returning content) as Clob) as list_in_lob
from aggregate_to_xml;
I guess you need to write a small function to concatenate the strings into a CLOB, because even when you cast TO_CLOB() the LISTAGG at the end, this might not work.
HereĀ“s a sample-function that takes a SELECT-Statement (which MUST return only one string-column!) and a separator and returns the collected values as a CLOB:
CREATE OR REPLACE FUNCTION listAggCLob(p_stringSelect VARCHAR2
, p_separator VARCHAR2)
RETURN CLOB
AS
cur SYS_REFCURSOR;
s VARCHAR2(4000);
c CLOB;
i INTEGER;
BEGIN
dbms_lob.createtemporary(c, FALSE);
IF (p_stringSelect IS NOT NULL) THEN
OPEN cur FOR p_stringSelect;
LOOP
FETCH cur INTO s;
EXIT WHEN cur%NOTFOUND;
dbms_lob.append(c, s || p_separator);
END LOOP;
END IF;
i := length(c);
IF (i > 0) THEN
RETURN dbms_lob.substr(c,i-length(p_separator));
ELSE
RETURN NULL;
END IF;
END;
This function can be used f.e. like this:
WITH cat AS (
SELECT DISTINCT t1.category
FROM lookup t1
)
SELECT cat.category
, listAggCLob('select t2.name from lookup t2 where t2.category = ''' || cat.category || '''', '|') allcategorynames
FROM cat;