Oracle complex string replacement - sql

I've got the following table
mytable
type | id | name | formula
"simple" | 1 | "COUNT" | "<1>"
"simple" | 2 | "DISTINCT" | "<2>"
"simple" | 3 | "mycol" | "<3>"
"complex" | 4 | null | "<1>(<2> <3>)"
Now I would like to read this table and add an additional column which replaces the formula string.
For id 4 I would need: "COUNT(DISTINCT mycol)"
Any idea how I can do that?

In Oracle 11 it may looks like this:
select
type, id, name, formula, value
from
mytable
left join (
select
id_complex,
listagg(decode(pos, 2, name, part)) within group (order by occ, pos) as value
from
(
select
id_complex, occ, pos,
regexp_replace(pair, '^(.*?)(<.*?>)$', '\'||pos) as part
from
(
select
id as id_complex,
occ,
regexp_substr(formula||'<>', '.*?<.*?>', 1, occ) as pair
from
(
select level as occ from dual
connect by level <= (select max(length(formula)) from mytable)
),
mytable
where type = 'complex'
),
(select level as pos from dual connect by level <= 2)
)
left join mytable on part = formula and type = 'simple'
group by id_complex
) on id = id_complex
order by id
SQL Fiddle

Related

Transforming columns to row values

Have a table in Google Bigquery like this with 1 id column (customers) and 3 store-name columns:
id |PA|PB|Mall|
----|--|--|----|
3699|1 |1 | 1 |
1017| |1 | 1 |
9991|1 | | |
My objective is to have the option to select customers (id's) who visited for example:
ONLY PA
PA and PB
PA and Mall
PA, PB and Mall
One alternative output could be:
id |Store |
----|--------- |
3699|PA+PB+Mall|
1017|PB+Mall |
9991|PA |
However this would not give me counts of all stopping by PA regardless of other stores visited. In the example above that count would have been 2 (3699 and 9991).
A second alternative output could be:
id |Store|
----|-----|
3699|PA |
3699|PB |
3699|Mall |
1017|PB |
1017|Mall |
9991|PA |
However, this would not allow me (i think) to select/filter those who has visited for example BOTH PA and Mall (only 3699)
A third alternative output could be a combo:
id |Store| Multiple store|
----|-----|---------------|
3699|PA | PA+PB+Mall |
3699|PB | PA+PB+Mall |
3699|Mall | PA+PB+Mall |
1017|PB | PB+Mall |
1017|Mall | PB+Mall |
9991|PA | |
What option is the best and is there any other alternatives to achieve my objective? I believe alternative 3 could be best, but not sure how to achieve it.
It depends what you want. For instance, the third would simply be:
select t.*,
string_agg(store, '+') over (partition by id)
from t;
The second would be:
select id, string_agg(store, '+')
from t
group by id;
For the third option, you may try unpivoting your current table, then applying STRING_AGG to get the computed column containing all stores for each id:
WITH cte AS (
SELECT id, CASE WHEN PA = 1 THEN 'PA' END AS Store
FROM yourTable
UNION ALL
SELECT id, CASE WHEN PB = 1 THEN 'PB' END
FROM yourTable
UNION ALL
SELECT id, CASE WHEN Mall = 1 THEN 'Mall' END
FROM yourTable
)
SELECT id, Store,
STRING_AGG(store, '+') OVER (PARTITION BY id) All_Stores
FROM cte
WHERE Store IS NOT NULL
ORDER BY id, Store;
Consider below approaches to all three options
Assuming input data is filled with nulls when it is empty in question's sample
with `project.dataset.table` as (
select 3699 id, 1 PA, 1 PB, 1 Mall union all
select 1017, null, 1, 1 union all
select 9991, 1, null, null
)
Option #1
select id, string_agg(key, '+') as Store
from `project.dataset.table` t,
unnest(split(translate(to_json_string(t), '{}"', ''))) kv,
unnest([struct(split(kv,':')[offset(0)] as key, split(kv,':')[offset(1)] as value)])
where key !='id'
and value != 'null'
group by id
with output
Option #2
select id, key as Store
from `project.dataset.table` t,
unnest(split(translate(to_json_string(t), '{}"', ''))) kv,
unnest([struct(split(kv,':')[offset(0)] as key, split(kv,':')[offset(1)] as value)])
where key !='id'
and value != 'null'
with output
Option #3
select id, key as Store,
string_agg(key, '+') over(partition by id) as Multiple_Store
from `project.dataset.table` t,
unnest(split(translate(to_json_string(t), '{}"', ''))) kv,
unnest([struct(split(kv,':')[offset(0)] as key, split(kv,':')[offset(1)] as value)])
where key !='id'
and value != 'null'
with output

SQL Tree Query - WITH RECURSIVE & UNION. How to sort by a second value

I have a category table with a tree like structure (see image below). For a fast single query, I have the following SQL to get the full tree.
WITH RECURSIVE search_tree(id, name, path, position) AS
(
SELECT id, name, ARRAY[id], position
FROM table_name
WHERE id = id
UNION ALL
SELECT table_name.id, table_name.name, path || table_name.id,
table_name.position
FROM search_tree
JOIN table_name ON table_name.parent_id = search_tree.id
WHERE NOT table_name.id = ANY(path)
)
SELECT id, name, path, position
FROM search_tree
ORDER BY path
This query result in the following table
id | name | path | position
----+--------------+---------+-------------
1 | Cat Pictures | {1}. |. 0
2 | Funny | {1,2}. |. 0
3 | LOLCats | {1,2,3} |. 1
4 | Animated | {1,2,4} |. 2
5 | Classic | {1,2,5} |. 0
6 | Renaissance | {1,6} |. 1
So the ordering according to path works well. But what I require is to have the order according to the column position, if the path-level is the same level (like id 2 & 4, and 3, 4, 5).
Hence the order of ID would want is
ids: 1, 6, 2, 5, 3, 4
How to I change my SQL statement to reflect that order?
It can be achieved this way https://www.db-fiddle.com/f/rpFiPjKSwHW88C4cp6o9Rm/0
with recursive search_tree(id, parentPath, name) as (
select id, cast(row_number() over(order by pos) as text), name
from objects
where parent_id = 0
union all
select o.id, concat(search_tree.parentPath, ',', cast(row_number() over(order by o.pos) as text)), o.name
from search_tree
join objects as o on search_tree.id = o.parent_id
)
select *
from search_tree
order by parentPath;

Oracle SQL Get unique symbols from table

I have table with descriptions of smth. For example:
My_Table
id description
================
1 ABC
2 ABB
3 OPAC
4 APEЧ
I need to get all unique symbols from all "description" columns.
Result should look like that:
symbol
================
A
B
C
O
P
E
Ч
And it shoud work for all languages, so, as I see, regular expressions cant help.
Please help me. Thanks.
with cte (c,description_suffix) as
(
select substr(description,1,1)
,substr(description,2)
from mytable
where description is not null
union all
select substr(description_suffix,1,1)
,substr(description_suffix,2)
from cte
where description_suffix is not null
)
select c
,count(*) as cnt
from cte
group by c
order by c
or
with cte(n) as
(
select level
from dual
connect by level <= (select max(length(description)) from mytable)
)
select substr(t.description,c.n,1) as c
,count(*) as cnt
from mytable t
join cte c
on c.n <= length(description)
group by substr(t.description,c.n,1)
order by c
+---+-----+
| C | CNT |
+---+-----+
| A | 4 |
| B | 3 |
| C | 2 |
| E | 1 |
| O | 1 |
| P | 2 |
| Ч | 1 |
+---+-----+
Create a numbers table and populate it with all the relevant ids you'd need (in this case 1..maxlength of string)
SELECT DISTINCT
locate(your_table.description, numbers.id) AS symbol
FROM
your_table
INNER JOIN
numbers
ON numbers.id >= 1
AND numbers.id <= CHAR_LENGTH(your_table.description)
SELECT DISTINCT(SUBSTR(ll,LEVEL,1)) OP --Here DISTINCT(SUBSTR(ll,LEVEL,1)) is used to get all distinct character/numeric in vertical as per requirment
FROM
(
SELECT LISTAGG(DES,'')
WITHIN GROUP (ORDER BY ID) ll
FROM My_Table --Here listagg is used to convert all values under description(des) column into a single value and there is no space in between
)
CONNECT BY LEVEL <= LENGTH(ll);

SELECT First Group

Problem Definition
I have an SQL query that looks like:
SELECT *
FROM table
WHERE criteria = 1
ORDER BY group;
Result
I get:
group | value | criteria
------------------------
A | 0 | 1
A | 1 | 1
B | 2 | 1
B | 3 | 1
Expected Result
However, I would like to limit the results to only the first group (in this instance, A). ie,
group | value | criteria
------------------------
A | 0 | 1
A | 1 | 1
What I've tried
Group By
SELECT *
FROM table
WHERE criteria = 1
GROUP BY group;
I can aggregate the groups using a GROUP BY clause, but that would give me:
group | value
-------------
A | 0
B | 2
or some aggregate function of EACH group. However, I don't want to aggregate the rows!
Subquery
I can also specify the group by subquery:
SELECT *
FROM table
WHERE criteria = 1 AND
group = (
SELECT group
FROM table
WHERE criteria = 1
ORDER BY group ASC
LIMIT 1
);
This works, but as always, subqueries are messy. Particularly, this one requires specifying my WHERE clause for criteria twice. Surely there must be a cleaner way to do this.
You can try following query:-
SELECT *
FROM table
WHERE criteria = 1
AND group = (SELECT MIN(group) FROM table)
ORDER BY value;
If your database supports the WITH clause, try this. It's similar to using a subquery, but you only need to specify the criteria input once. It's also easier to understand what's going on.
with main_query as (
select *
from table
where criteria = 1
order by group, value
),
with min_group as (
select min(group) from main_query
)
select *
from main_query
where group in (select group from min_group);
-- this where clause should be fast since there will only be 1 record in min_group
Use DENSE_RANK()
DECLARE #yourTbl AS TABLE (
[group] NVARCHAR(50),
value INT,
criteria INT
)
INSERT INTO #yourTbl VALUES ( 'A', 0, 1 )
INSERT INTO #yourTbl VALUES ( 'A', 1, 1 )
INSERT INTO #yourTbl VALUES ( 'B', 2, 1 )
INSERT INTO #yourTbl VALUES ( 'B', 3, 1 )
;WITH cte AS
(
SELECT i.* ,
DENSE_RANK() OVER (ORDER BY i.[group]) AS gn
FROM #yourTbl AS i
WHERE i.criteria = 1
)
SELECT *
FROM cte
WHERE gn = 1
group | value | criteria
------------------------
A | 0 | 1
A | 1 | 1

Getting distinct result with Oracle SQL

I have the following data structure
ID | REFID | NAME
1 | 100 | A
2 | 101 | B
3 | 101 | C
With
SELECT DISTINCT REFID, ID, NAME
FROM my_table
ORDER BY ID
I would like to have the following result:
1 | 100 | A
2 | 101 | B
Colum NAME and ID should contain the MIN or FIRST value.
But actually I get stuck at using MIN/FIRST here.
I welcome every tipps :-)
select id,
refid,
name
from (select id,
refid,
name,
row_number() over(partition by refid order by name) as rn
from my_table)
where rn = 1
order by id
You can use a subquery to do this.
WITH Q AS
( SELECT MIN(NAME) AS NAME, REFID FROM T GROUP BY REFID )
SELECT T.ID, T.REFID, T.NAME
FROM T
JOIN Q
ON (T.NAME = Q.NAME)
Also, note that SQL tables have no order. So there's no "First" value.