value substitution/replacement in a string - sql

I have a string x-y+z. The values for x, y and z will be stored in a table. Say
x 10
y 15
z 20
This string needs to be changed like 10-15+20.
Anyway I can achieve this using plsql or sql?

using simple Pivot we can do
DECLARE #Table1 TABLE
( name varchar(1), amount int )
;
INSERT INTO #Table1
( name , amount )
VALUES
('x', 10),
('y', 15),
('Z', 25);
Script
Select CAST([X] AS VARCHAR) +'-'+CAST([Y] AS VARCHAR)+'+'+CAST([Z] AS VARCHAR) from (
select * from #Table1)T
PIVOT (MAX(amount) FOR name in ([X],[y],[z]))p

An approach could be the following, assuming a table like this:
create table stringToNumbers(str varchar2(16), num number);
insert into stringToNumbers values ('x', 10);
insert into stringToNumbers values ('y', 20);
insert into stringToNumbers values ('zz', 30);
insert into stringToNumbers values ('w', 40);
First tokenize your input string with something like this:
SQL> with test as (select 'x+y-zz+w' as string from dual)
2 SELECT 'operand' as typ, level as lev, regexp_substr(string, '[+-]+', 1, level) as token
3 FROM test
4 CONNECT BY regexp_instr(string, '[a-z]+', 1, level+1) > 0
5 UNION ALL
6 SELECT 'value', level, regexp_substr(string, '[^+-]+', 1, level) as token
7 FROM test
8 CONNECT BY regexp_instr(string, '[+-]', 1, level - 1) > 0
9 order by lev asc, typ desc;
TYP LEV TOKEN
------- ---------- --------------------------------
value 1 x
operand 1 +
value 2 y
operand 2 -
value 3 zz
operand 3 +
value 4 w
In the example I used lowercase literals and only +/- signs; you can easily edit it to handle something more complex; also, I assume the input string is well-formed.
Then you can join your decoding table to the tokenized string, building the concatenation:
SQL> select listagg(nvl(to_char(num), token)) within group (order by lev asc, typ desc)
2 from (
3 with test as (select 'x+y-zz+w' as string from dual)
4 SELECT 'operand' as typ, level as lev, regexp_substr(string, '[+-]+', 1, level) as token
5 FROM test
6 CONNECT BY regexp_instr(string, '[a-z]+', 1, level+1) > 0
7 UNION ALL
8 SELECT 'value', level, regexp_substr(string, '[^+-]+', 1, level) as token
9 FROM test
10 CONNECT BY regexp_instr(string, '[+-]', 1, level - 1) > 0
11 order by lev asc, typ desc
12 ) tokens
13 LEFT OUTER JOIN stringToNumbers on (str = token);
LISTAGG(NVL(TO_CHAR(NUM),TOKEN))WITHINGROUP(ORDERBYLEVASC,TYPDESC)
--------------------------------------------------------------------------------
10+20-30+40
This assumes that every literal in you input string has a corrensponding value in table. You can even handle the case of strings with no corrensponding number, for example assigning 0:
SQL> select listagg(
2 case
3 when typ = 'operand' then token
4 else to_char(nvl(num, 0))
5 end
6 ) within group (order by lev asc, typ desc)
7 from (
8 with test as (select 'x+y-zz+w-UNKNOWN' as string from dual)
9 SELECT
.. ...
16 ) tokens
17 LEFT OUTER JOIN stringToNumbers on (str = token);
LISTAGG(CASEWHENTYP='OPERAND'THENTOKENELSETO_CHAR(NVL(NUM,0))END)WITHINGROUP(ORD
--------------------------------------------------------------------------------
10+20-30+40-0

Create a function like this:
create table ttt1
( name varchar(1), amount int )
;
INSERT INTO ttt1 VALUES ('x', 10);
INSERT INTO ttt1 VALUES ('y', 15);
INSERT INTO ttt1 VALUES ('z', 25);
CREATE OR REPLACE FUNCTION replace_vars (in_formula VARCHAR2)
RETURN VARCHAR2
IS
f VARCHAR2 (2000) := UPPER (in_formula);
BEGIN
FOR c1 IN ( SELECT UPPER (name) name, amount
FROM ttt1
ORDER BY name DESC)
LOOP
f := REPLACE (f, c1.name, c1.amount);
END LOOP;
return f;
END;
select replace_vars('x-y+z') from dual

Here's another way to approach the problem that attempts to do it all in SQL. While not necessarily the most flexible or fastest, maybe you can get some ideas from another way to approach the problem. It also shows a way to execute the final formula to get the answer. See the comments below.
Assumes all variables are present in the variable table.
-- First build the table that holds the values. You won't need to do
-- this if you already have them in a table.
with val_tbl(x, y, z) as (
select '10', '15', '20' from dual
),
-- Table to hold the formula.
formula_tbl(formula) as (
select 'x-y+z' from dual
),
-- This table is built from a query that reads the formula a character at a time.
-- When a variable is found using the case statement, it is queried in the value
-- table and it's value is returned. Otherwise the operator is returned. This
-- results in a row for each character in the formula.
new_formula_tbl(id, new_formula) as (
select level, case regexp_substr(formula, '(.|$)', 1, level, NULL, 1)
when 'x' then
(select x from val_tbl)
when 'y' then
(select y from val_tbl)
when 'z' then
(select z from val_tbl)
else regexp_substr(formula, '(.|$)', 1, level, NULL, 1)
end
from formula_tbl
connect by level <= regexp_count(formula, '.')
)
-- select id, new_formula from new_formula_tbl;
-- This puts the rows back into a single string. Order by id (level) to keep operands
-- and operators in the right order.
select listagg(new_formula) within group (order by id) formula
from new_formula_tbl;
FORMULA
----------
10-15+20
Additionally you can get the result of the formula by passing the listagg() call to the following xmlquery() function:
select xmlquery(replace( listagg(new_formula) within group (order by id), '/', ' div ')
returning content).getNumberVal() as result
from new_formula_tbl;
RESULT
----------
15

Related

Multiple Sequence in SQL Oracle / Varchar2 ID with letters and digits

I am trying to 'generate' a Varchar ID that contains letters and digits too with a sequence, but I think there will be more sequences than one.
What I am looking forward to is something like this 'DJ_Digit_Digit_Letter_Letter_Letter'. An example would be DJ00WVX/DJ01HYZ/DJ99ZZZ. This is also my primary key so it would be good if I could not encounter any primary key errors.
I thought about working in ascii so I will generate numbers between 65 and 90 (A-Z) and Insert them into the column. Same for the numbers in ascii 45 to 57 or something like that. I don't know how I could use more sequences on the same column like 'letter_seq.nextvalue,digit_seq.nextvalue', i know that this example is wrong, but it was easier for me to explain it. Maybe all my thinking is wrong
In conclusion I need to get something that starts with DJ followed by 2 digits and 3 letters, all wrapped up in a primary key. It's ok to get like 20-30 unique entries, I think.
Thank you for your help!
You can create an INVISIBLE IDENTITY column and then create your letter-digit-letter sequence as a function generated from the invisible column:
CREATE TABLE table_name (
id_seq_value INT
INVISIBLE
GENERATED ALWAYS AS IDENTITY( START WITH 0 INCREMENT BY 1 MINVALUE 0 MAXVALUE 175799 )
NOT NULL
CONSTRAINT table_name__id_seq_value__u UNIQUE,
id VARCHAR2(7)
GENERATED ALWAYS AS (
CAST(
'DJ'
|| TO_CHAR( FLOOR( id_seq_value / POWER( 26, 3 ) ), 'FM00' )
|| CHR( 65 + MOD( FLOOR( id_seq_value / POWER(26, 2) ), 26 ) )
|| CHR( 65 + MOD( FLOOR( id_seq_value / POWER(26, 1) ), 26 ) )
|| CHR( 65 + MOD( id_seq_value, 26 ) )
AS VARCHAR2(7)
)
)
CONSTRAINT table_name__id__pk PRIMARY KEY,
name VARCHAR2(50)
);
Then you can do:
INSERT INTO table_name ( name ) VALUES ( 'Item1' );
INSERT INTO table_name ( name ) VALUES ( 'Item2' );
Then:
SELECT * FROM table_name;
Outputs:
ID | NAME
:------ | :----
DJ00AAA | Item1
DJ00AAB | Item2
(If you want to increment the digits and the letters together then change the IDENTITY to INCREMENT BY 17577 (263+1).)
db<>fiddle here
To me, it looks like this:
SQL> create table test (id varchar2(10));
Table created.
SQL> create sequence seq1;
Sequence created.
SQL> insert into test (id)
2 with
3 a as (select chr(64 + level) la from dual
4 connect by level <= 26),
5 b as (select chr(64 + level) lb from dual
6 connect by level <= 26),
7 c as (select chr(64 + level) lc from dual
8 connect by level <= 26)
9 select 'DJ' || lpad(seq1.nextval, 2, '0')
10 || la || lb || lc id
11 from a cross join b cross join c;
17576 rows created.
SQL>
Several sample values:
SQL> with temp as
2 (select id,
3 row_number() Over (order by id) rna,
4 row_Number() over (order by id desc) rnd
5 from test
6 )
7 select id
8 from temp
9 where rna <= 5
10 or rnd <= 5
11 order by id;
ID
----------
DJ01AAA
DJ02AAB
DJ03AAC
DJ04AAD
DJ05AAE
DJ99OUK
DJ99OUL
DJ99OUM
DJ99OUN
DJ99OUO
10 rows selected.
SQL>
However, if
It's ok to get like 20-30 unique entries, I think.
means that you need to generate at most 30 values, well, you'd easily create them manually; why would you develop any software solution for that? If you started typing, you'd be over by now.

How to store fetched bulk values into single variable and insert all values it on one variable using pl/sql

Note
Somebody Tell me how to store Multiples rows value for a single column store into a single variable and behalf of that variable returned values store in table on one single click how to do such scenario using pl/sql.
Like Such Query Return single column with Multi rows i want to store it into some var and insert all values on single click process.It will generate error image of error has attached
DECLARE
l_respondent_id VARCHAR(100);
BEGIN
FOR c IN (
SELECT
s.gr_number
FROM
student s
LEFT JOIN class_time ct ON ct.class_id = s.class_id
AND instr(s.class_time, ct.class_time) > 0
WHERE
upper(TRIM(ct.class_id)) = upper(TRIM(:app_user))
AND s.gr_number IS NOT NULL
AND is_active_flg = 'Y'
) LOOP
l_respondent_id := c.gr_number;
BEGIN
SELECT
s.gr_number
INTO l_respondent_id
FROM
student s
LEFT JOIN class_time ct ON ct.class_id = s.class_id
AND instr(s.class_time, ct.class_time) > 0
WHERE
upper(TRIM(ct.class_id)) = upper(TRIM(:app_user))
AND s.gr_number IS NOT NULL
AND is_active_flg = 'Y'
AND s.gr_number = c.gr_number;
EXCEPTION
WHEN no_data_found THEN
l_respondent_id := NULL;
END;
IF l_respondent_id IS NULL THEN
INSERT INTO student_class_attend ( gr_number ) VALUES ( c.gr_number ) RETURNING gr_number INTO l_respondent_id;
END IF;
END LOOP;
END;
You can probably use a nested table for this, together with a BULK COLLECT and a FORALL. Simplified example:
Tables and data
create table students( id primary key, fname, lname )
as
select 1, 'fname_1', 'lname_1' from dual union all
select 2, 'fname_2', 'lname_2' from dual union all
select 3, 'fname_3', 'lname_3' from dual union all
select 4, 'fname_4', 'lname_4' from dual union all
select 5, 'fname_5', 'lname_5' from dual union all
select 6, 'fname_6', 'lname_6' from dual ;
-- empty
create table attendance( studentid number references students, when_ date ) ;
Anonymous block
declare
-- This will allow you to store several studentIDs:
type studentid_t is table of students.id%type index by pls_integer ;
-- The current attendance list. Notice the TYPE.
attendancelist studentid_t ;
begin
-- Your query (including all the necessary joins and conditions) here.
-- In this case, we will pick up 3 of the 6 IDs.
select id bulk collect into attendancelist
from students
where mod( id, 2 ) = 0 ;
-- Write a _single_ DML statement after FORALL (which is not a loop - see docs)
forall i in attendancelist.first .. attendancelist.last
insert into attendance( studentid, when_ )
values( attendancelist( i ) , sysdate ) ; -- use whatever you need here
end ;
/
Result (after executing the anonymous block)
SQL> select * from attendance ;
STUDENTID WHEN_
---------- ---------
2 07-JUN-20
4 07-JUN-20
6 07-JUN-20
DBfiddle here.

How to secure table for avoid duplicate data

I cant resolve the problem how secure my table to avoid duplicate combination of attributes_positions. The best way to show you what I mean is the following image
column id_combination represents number of combination. Combination consists of attributes_positions. So Combination is sequence of attributes_positions.
And now I would secure table from insert exaclty the same sequence of attributes_positions.
Of course if already inserted combination contains one additional attributes_positions or one less than inserting combination is ok
image I show the different bettwen duplicate and not duplicate combination.
Is there a some way how I can do that?? Meaby something like 'before update'. But how to implement for this example. I`m not so pretty good with advanced sql.
The database where I trying to secure table is postgresql 9.4
I will be grateful for help
-- The data
CREATE TABLE theset (
set_id INTEGER NOT NULL PRIMARY KEY
, set_name text UNIQUE
);
INSERT INTO theset(set_id, set_name) VALUES
( 1, 'one'), ( 2, 'two'), ( 3, 'three'), ( 4, 'four');
CREATE TABLE theitem (
item_id integer NOT NULL PRIMARY KEY
, item_name text UNIQUE
);
INSERT INTO theitem(item_id, item_name) VALUES
( 1, 'one'), ( 2, 'two'), ( 3, 'three'), ( 4, 'four'), ( 5, 'five');
CREATE TABLE set_item (
set_id integer NOT NULL REFERENCES theset (set_id)
, item_id integer NOT NULL REFERENCES theitem(item_id)
, PRIMARY KEY (set_id,item_id)
);
-- swapped index is indicated for junction tables
CREATE UNIQUE INDEX ON set_item(item_id, set_id);
INSERT INTO set_item(set_id,item_id) VALUES
(1,1), (1,2), (1,3), (1,4),
(2,1), (2,2), (2,3), -- (2,4),
(3,1), (3,2), (3,3), (3,4), (3,5),
(4,1), (4,2), (4,4);
CREATE FUNCTION set_item_unique_set( ) RETURNS TRIGGER AS
$func$
BEGIN
IF EXISTS ( -- other set
SELECT * FROM theset oth
-- WHERE oth.set_id <> NEW.set_id -- only for insert/update
WHERE TG_OP = 'DELETE' AND oth.set_id <> OLD.set_id
OR TG_OP <> 'DELETE' AND oth.set_id <> NEW.set_id
-- count (common) members in the two sets
-- items not in common will have count=1
AND NOT EXISTS (
SELECT item_id FROM set_item x1
WHERE (x1.set_id = NEW.set_id OR x1.set_id = oth.set_id )
GROUP BY item_id
HAVING COUNT(*) = 1
)
) THEN
RAISE EXCEPTION 'Not unique set';
RETURN NULL;
ELSE
RETURN NEW;
END IF;
END;
$func$ LANGUAGE 'plpgsql'
;
CREATE CONSTRAINT TRIGGER check_item_set_unique
AFTER UPDATE OR INSERT OR DELETE
-- BEFORE UPDATE OR INSERT
ON set_item
FOR EACH ROW
EXECUTE PROCEDURE set_item_unique_set()
;
-- Test it
INSERT INTO set_item(set_id,item_id) VALUES(4,5); -- success
INSERT INTO set_item(set_id,item_id) VALUES(2,4); -- failure
DELETE FROM set_item WHERE set_id=1 AND item_id= 4; -- failure
Note: There should also be a trigger for the DELETE case.
UPDATE: added handling of DELETE
(the handling of deletes is not perfect; imagine the case where the last element from a set is removed)
My answer assumes that the target is without dupes, and that we want to insert a new set - which happens to be a duplicate. I choose the group of 4 with the id_comb of 1.
You would have to put the group of 4 into a staging table. Then, you have to pivot both staging and target horizontally - so that you get 5 columns named attr_pos1 to attr_pos5 (the biggest group in your example is 5). To pivot, you need a sequence number, which we get by using ROW_NUMBER(). That's for both tables, staging and target. Then, you pivot both. Then, you try to join pivoted staging and target on all 5 attr_pos# columns, and count the rows. If you get 0, you have no duplicates. If you get 1, you have duplicates.
Here's the whole scenario:
WITH
-- input section: a) target table, no dupes
target(id_comb,attr_pos) AS (
SELECT 2,1
UNION ALL SELECT 2,2
UNION ALL SELECT 2,3
UNION ALL SELECT 2,4
UNION ALL SELECT 3,1
UNION ALL SELECT 3,2\
UNION ALL SELECT 3,3
UNION ALL SELECT 3,4
UNION ALL SELECT 3,5
UNION ALL SELECT 4,1
UNION ALL SELECT 4,2
UNION ALL SELECT 4,3
)
,
-- input section: b) staging, input, would be a dupe
staging(id_comb,attr_pos) AS (
SELECT 1,1
UNION ALL SELECT 1,2
UNION ALL SELECT 1,3
UNION ALL SELECT 1,4
)
,
-- query section:
-- add sequence numbers to stage and target
target_s AS (
SELECT
ROW_NUMBER() OVER(PARTITION BY id_comb ORDER BY attr_pos) AS seq
, *
FROM target
)
,
staging_s AS (
SELECT
ROW_NUMBER() OVER(PARTITION BY id_comb ORDER BY attr_pos) AS seq
, *
FROM staging
)
,
-- horizontally pivot target, NULLS as -1 for later join
target_h AS (
SELECT
id_comb
, IFNULL(MAX(CASE seq WHEN 1 THEN attr_pos END),-1) AS attr_pos1
, IFNULL(MAX(CASE seq WHEN 2 THEN attr_pos END),-1) AS attr_pos2
, IFNULL(MAX(CASE seq WHEN 3 THEN attr_pos END),-1) AS attr_pos3
, IFNULL(MAX(CASE seq WHEN 4 THEN attr_pos END),-1) AS attr_pos4
, IFNULL(MAX(CASE seq WHEN 5 THEN attr_pos END),-1) AS attr_pos5
FROM target_s
GROUP BY id_comb ORDER BY id_comb
)
,
-- horizontally pivot staging, NULLS as -1 for later join
staging_h AS (
SELECT
id_comb
, IFNULL(MAX(CASE seq WHEN 1 THEN attr_pos END),-1) AS attr_pos1
, IFNULL(MAX(CASE seq WHEN 2 THEN attr_pos END),-1) AS attr_pos2
, IFNULL(MAX(CASE seq WHEN 3 THEN attr_pos END),-1) AS attr_pos3
, IFNULL(MAX(CASE seq WHEN 4 THEN attr_pos END),-1) AS attr_pos4
, IFNULL(MAX(CASE seq WHEN 5 THEN attr_pos END),-1) AS attr_pos5
FROM staging_s
GROUP BY id_comb ORDER BY id_comb
)
SELECT
COUNT(*)
FROM target_h
JOIN staging_h USING (
attr_pos1
, attr_pos2
, attr_pos3
, attr_pos4
, attr_pos5
);
Hope this helps ----
Marco
Interesting but not very useful solution by #wildplasser. I create script to insert sample data:
WITH param AS (
SELECT 8 AS max
), maxarray AS (
SELECT array_agg(i) as ma FROM (SELECT generate_series(1, max) as i FROM param) as i
), pre AS (
SELECT
*
FROM (
SELECT
*, CASE WHEN (id >> mbit) & 1 = 1 THEN ma[mbit + 1] END AS item_id
FROM (
SELECT *,
generate_series(0, array_upper(ma, 1) - 1) as mbit
FROM (
SELECT *,
generate_series(1,(2^max - 1)::int8) AS id
FROM param, maxarray
) AS pre1
) AS pre2
) AS pre3
WHERE item_id IS NOT NULL
), ins_item AS (
INSERT INTO theitem (item_id, item_name) SELECT i, i::text FROM generate_series(1, (SELECT max FROM param)) as i RETURNING *
), ins_set AS (
INSERT INTO theset (set_id, set_name)
SELECT id, id::text FROM generate_series(1, (SELECT 2^max - 1 FROM param)::int8) as id
RETURNING *
), ins_set_item AS (
INSERT INTO set_item (set_id, item_id)
SELECT id, item_id FROM pre WHERE (SELECT count(*) FROM ins_item) > 0 AND (SELECT count(*) FROM ins_set) > 0
RETURNING *
)
SELECT
'sets', count(*)
FROM ins_set
UNION ALL
SELECT
'items', count(*)
FROM ins_item
UNION ALL
SELECT
'sets_items', count(*)
FROM ins_set_item
;
When I call it with 8 (1024 - 2^8 rows for set_item) it run 21 seconds. It is very bad. When I off trigger it took less then 1 milliseconds.
My proposal
It is very interesting to use arrays in this case. Unfortunatelly PostgreSQL does not support foreighn key for arrays, but it may be done by TRIGGERs. I remove set_item table and add items int[] field for theset:
-- The data
CREATE TABLE theitem (
item_id integer NOT NULL PRIMARY KEY
, item_name text UNIQUE
);
CREATE TABLE theset (
set_id INTEGER NOT NULL PRIMARY KEY
, set_name text UNIQUE
, items integer[] UNIQUE NOT NULL
);
CREATE INDEX i1 ON theset USING gin (items);
CREATE OR REPLACE FUNCTION check_item_CU() RETURNS TRIGGER AS $sql$
BEGIN
IF (SELECT count(*) > 0 FROM unnest(NEW.items) AS u LEFT JOIN theitem ON (item_id = u) WHERE item_id IS NULL) THEN
RETURN NULL;
END IF;
NEW.items = ARRAY(SELECT unnest(NEW.items) ORDER BY 1);
RETURN NEW;
END;
$sql$ LANGUAGE plpgsql;
CREATE TRIGGER check_item_CU BEFORE INSERT OR UPDATE ON theset FOR EACH ROW EXECUTE PROCEDURE check_item_CU();
CREATE OR REPLACE FUNCTION check_item_UD() RETURNS TRIGGER AS $sql$
BEGIN
IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE' AND NEW.item_id != OLD.item_id) AND (SELECT count(*) > 0 FROM theset WHERE OLD.item_id = ANY(items)) THEN
RAISE EXCEPTION 'item_id % still used', OLD.item_id;
RETURN NULL;
END IF;
RETURN NEW;
END;
$sql$ LANGUAGE plpgsql;
CREATE TRIGGER check_item_UD BEFORE DELETE OR UPDATE ON theitem FOR EACH ROW EXECUTE PROCEDURE check_item_UD();
WITH param AS (
SELECT 10 AS max
), maxarray AS (
SELECT array_agg(i) as ma FROM (SELECT generate_series(1, max) as i FROM param) as i
), pre AS (
SELECT
*
FROM (
SELECT
*, CASE WHEN (id >> mbit) & 1 = 1 THEN ma[mbit + 1] END AS item_id
FROM (
SELECT *,
generate_series(0, array_upper(ma, 1) - 1) as mbit
FROM (
SELECT *,
generate_series(1,(2^max - 1)::int8) AS id
FROM param, maxarray
) AS pre1
) AS pre2
) AS pre3
WHERE item_id IS NOT NULL
), pre_arr AS (
SELECT id, array_agg(item_id) AS items
FROM pre
GROUP BY 1
), ins_item AS (
INSERT INTO theitem (item_id, item_name) SELECT i, i::text FROM generate_series(1, (SELECT max FROM param)) as i RETURNING *
), ins_set AS (
INSERT INTO theset (set_id, set_name, items)
SELECT id, id::text, items FROM pre_arr WHERE (SELECT count(*) FROM ins_item) > 0
RETURNING *
)
SELECT
'sets', count(*)
FROM ins_set
UNION ALL
SELECT
'items', count(*)
FROM ins_item
;
This variant run less than 1ms

Common Table Expression to traverse down hierarchy

The Structure
I have 2 tables that link to each other. One is a set of values and a nullable foreign key that points to the Id of the other table, which contains 2 foreign keys back to the other table.
HierarchicalTable
Id LeftId RightId SomeValue
1 1 2 some value
2 3 4 top level in tree
3 5 6 incorrect hierarchy 1
4 7 8 incorrect result top level
IntermediateTable
Id SomeValue HierarchicalTableId
1 some value NULL
2 value NULL
3 NULL 1
4 value NULL
5 incorrect result 1 NULL
6 incorrect result 3 NULL
7 incorrect result 3 NULL
8 NULL 3
Each table points down the hierarchy. Here is this structure graphed out for the Hierarchical Table records 1 & 2 and their IntermediateTable values:
(H : HierarchicalTable, I : IntermediateTable)
H-2
/ \
I-3 I-4
/
H-1
/ \
I-1 I-2
The Problem
I need to be able to send in an Id for a given HierarchicalTable and get all the HierarchicalTable records below it. So, for the structure above, if I pass 1 into a query, I should just get H-1 (and from that, I can load the related IntermediateTable values). If I pass 2, I should get H-2 and H-1 (and, again, use those to load the relevant IntermediateTable values).
The Attempts
I've tried using a CTE, but there are a few main things that are different from the examples I've seen:
In my structure, the objects point down to their children, instead of up to their parent
I have the Id of the top object, not the Id of the bottom object.
My hierarchy is split across 2 tables. This shouldn't be a big issue once I understand the algorithm to find the results I need, but this could be causing additional confusion for me.
If I run this query:
declare #TargetId bigint = 2
;
with test as (
select h.*
from dbo.hierarchicaltable h
inner join dbo.intermediatetable i
on (h.leftid = i.id or h.rightid = i.id)
union all
select h.*
from dbo.hierarchicaltable h
where h.id = #TargetId
)
select distinct *
from test
I get all 4 records in the HierarchicalTable, instead of just records 1 & 2. I'm not sure if what I want is possible to do with a CTE.
Try this:
I'm build entire tree with both tables, then filter (only hierarchicaltable records).
DECLARE #HierarchicalTable TABLE(
Id INT,
LeftId INT,
RightId INT,
SomeValue VARCHAR(MAX)
)
INSERT INTO #HierarchicalTable
VALUES
(1, 1, 2, 'some value '),
(2, 3, 4, 'top level in tree '),
(3, 5, 6, 'incorrect hierarchy 1 '),
(4, 7, 8, 'incorrect result top level')
DECLARE #IntermediateTable TABLE(
Id INT,
SomeValue VARCHAR(MAX),
HierarchicalTableId INT
)
INSERT INTO #IntermediateTable
VALUES
(1, 'some value' ,NULL ),
(2, 'value ' ,NULL ),
(3, NULL ,1 ),
(4, 'value ' ,NULL ),
(5, 'incorrect result 1' ,NULL ),
(6, 'incorrect result 3' ,NULL ),
(7, 'incorrect result 3' ,NULL ),
(8, NULL ,3 )
DECLARE #TargetId INT = 2;
WITH CTE AS (
SELECT Id AS ResultId, LeftId, RightId, NULL AS HierarchicalTableId
FROM #HierarchicalTable
WHERE Id = #TargetId
UNION ALL
SELECT C.Id AS ResultId, C.LeftId, C.RightId, NULL AS HierarchicalTableId
FROM #HierarchicalTable C
INNER JOIN CTE P ON P.HierarchicalTableId = C.Id
UNION ALL
SELECT NULL AS ResultId, NULL AS LeftId, NULL AS RightId, C.HierarchicalTableId
FROM #IntermediateTable C
INNER JOIN CTE P ON P.LeftId = C.Id OR P.RightId = C.Id
)
SELECT *
FROM CTE
WHERE ResultId IS NOT NULL

How to insert different values from same row in multiple rows of a new table

I have the A table:
id bigint,
a integer,
b integer,
c integer,
date date
and I have to put every single value in a new table following by the date, in this way:
B table:
id bigint,
type integer,
date date
For example, if in my A table I had a row like this:
id a b c date
13 5 4 7 2014-11-09
I would like to put this values in B table like this:
id type date
1 5 2014-11-09,
2 4 2014-11-09,
3 7 2014-11-09
Any suggestions?
Unpivot first the data using UNION ALL and then assign a new id using ROW_NUMBER:
SELECT
ROW_NUMBER() OVER(ORDER BY id, col) AS id,
type,
date
FROM (
SELECT id, 'a' AS col, a AS type, date FROM tableA UNION ALL
SELECT id, 'b' AS col, b AS type, date FROM tableA UNION ALL
SELECT id, 'c' AS col, c AS type, date FROM tableA
) t
If the new id is auto generated, you do not need the ROW_NUMBER at all.
SELECT a AS type, date FROM tableA UNION ALL
SELECT b AS type, date FROM tableA UNION ALL
SELECT c AS type, date FROM tableA
INSERT INTO A
( id ,
a ,
b ,
c ,
date )
VALUES ( 13,
5 ,
4 ,
7 ,
2014-11-09
)
ALTER TRIGGER NewTrigger ON A
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #id AS NVARCHAR(50)
DECLARE #type AS NVARCHAR(50)
DECLARE #date AS INT
SELECT #Date = INSERTED.Date
FROM INSERTED
INSERT INTO B
( Date,
Type,
id
)
VALUES ( #Date,
4,
1
)
END
GO
SELECT * A
SELECT * FROM B
In Oracle you can try using REGEXP to simulate the sqame . Hope this helps.
SELECT DISTINCT LEVEL,
TRIM(regexp_substr(a.colk,'[^,]+', 1, level)) TYPE,
SYSDATE
FROM
(SELECT LEVEL,
COL1
||','
||COL2
||','
||COL3 colk,
SYSDATE
FROM
(SELECT 13 AS ID,5 COL1,4 AS COL2,7 AS COL3,sysdate AS dt FROM DUAL
)
CONNECT BY LEVEL <= REGEXP_COUNT(COL1
||','
||COL2
||','
||COL3,',')+1
)a
CONNECT BY regexp_substr(a.colk, '[^,]+', 1, level) IS NOT NULL;
Contest for shortest code :) My variant is:
insert into b(type, date)
select unnest(array[a,b,c]), date from a;
Assuming that the id column in the b table is autoincremented.