So, I have currently have two tables, one containing the fixed part of each object, and the other containing a variable number of properties of each object as key/value pairs. I would like to convert these properties to a CLOB field in the main table in JSON map format.
create table test_a (id integer, properties clob);
create table test_b (id integer, a_id integer, key char(30), value char(30));
insert into test_a values(1,'');
insert into test_a values(2,'');
insert into test_a values(3,'');
insert into test_b values (1, 1, 'k1', 'v1');
insert into test_b values (2, 1, 'k2', 'v2');
insert into test_b values (3, 2, 'k3', 'v3');
insert into test_b values (4, 2, 'k4', 'v4');
insert into test_b values (5, 2, 'k5', 'v5');
insert into test_b values (6, 2, 'k6', 'v6');
I can build the JSON I want via the following query:
WITH PROPS AS
(SELECT '"'
||trim(KEY)
||'":"'
||trim(value)
||'"' json,
test_b.*
FROM test_b
)
SELECT test_a.id,
'{'
||
(SELECT listagg(json, ',') within GROUP (
ORDER BY props.key)
FROM PROPS
WHERE PROPS.A_ID = test_a.id
)
|| '}'
FROM TEST_A
INNER JOIN PROPS
ON TEST_A.ID = PROPS.A_ID
GROUP BY TEST_A.ID ;
And I get the desired result
1 "{""k1"":""v1"",""k2"":""v2""}"
2 "{""k3"":""v3"",""k4"":""v4"",""k5"":""v5"",""k6"":""v6""}"
But when I try to use this result to insert into the master table, I get errors
UPDATE TEST_A SET PROPERTIES = (
WITH PROPS AS
(SELECT '"'
||trim(key)
||'":"'
||trim(value)
||'"' json,
test_b.*
FROM test_b
)
SELECT '{'
||
(SELECT listagg(json, ',') within GROUP (order by props.key) FROM PROPS
WHERE PROPS.A_ID = test_a.id)
|| '}' from TEST_A INNER JOIN PROPS ON TEST_A.ID = PROPS.A_ID GROUP BY TEST_A.ID
) ;
Error report -
SQL Error: ORA-01427: single-row subquery returns more than one row
01427. 00000 - "single-row subquery returns more than one row"
Any ideas?
You need a correlated subquery rather than a group by. The entire outer query doesn't look right, so I think you need something like:
UPDATE TEST_A
SET PROPERTIES = (
WITH PROPS AS (
SELECT '"'||trim(key)||'":"'||trim(value)||'"' as json,
test_b.*
FROM test_b
)
SELECT '{' || listagg(json, ',') within GROUP (order by props.key) || '}'
FROM PROPS
WHERE PROPS.A_ID = test_a.id
) ;
Related
How to print different output within LISTAGG() depending on number of aggregated elements?
Is it possible to get number of aggreated elements without additional COUNT(*) query?
There is an example DDL:
create table shepherds (
SHEPHERD_ID NUMBER(19),
SHEPHERD_NAME VARCHAR2(50 CHAR)
);
create table sheeps (
SHEEP_ID VARCHAR2(10 CHAR),
SHEEP_NAME VARCHAR2(50 CHAR),
SHEEP_SHEPHERD_ID NUMBER(19)
);
-- insert shepherds
insert into shepherds VALUES (111, 'Asher');
insert into shepherds VALUES (222, 'Joseph');
insert into shepherds VALUES (333, 'Nicodemus');
-- first shepherd (one sheep)
insert into sheeps VALUES ('A', 'Mark', 111);
-- second shepherd (two sheeps)
insert into sheeps VALUES ('A', 'Andres', 222);
insert into sheeps VALUES ('B', 'Jeffrey', 222);
-- third shepherd (three sheeps)
insert into sheeps VALUES ('B', 'Jeffrey', 333);
insert into sheeps VALUES ('A', 'Andres', 333);
insert into sheeps VALUES ('D', 'Andres', 333);
Now I want to display all shepherds with new-line separated sheep names in the following way:
SELECT
SHEPHERD_NAME,
(SELECT
listagg(SHEEP_ID || ': ' || SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
FROM SHEEPS
WHERE SHEEP_SHEPHERD_ID = SHEPHERD_ID)
FROM SHEPHERDS;
The result is: http://sqlfiddle.com/#!4/881a7/3
However, I want to hide sheep's ID letter for those shepherds who have only one sheep.
I tried the following:
SELECT
SHEPHERD_NAME,
(SELECT
listagg(
CASE WHEN COUNT(*) > 1 THEN SHEEP_ID || ': ' ELSE '' END
|| SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
FROM SHEEPS
WHERE SHEEP_SHEPHERD_ID = SHEPHERD_ID)
FROM SHEPHERDS;
However, I get error:
ORA-00978: nested group function without GROUP BY
http://sqlfiddle.com/#!4/881a7/7
Is it possible to return different string from LISTAGG() if there is only one element to aggregate?
How to detect number of aggregated elements without slowing down query performance in Oracle 11g or higher?
A conditional expression in the subquery should do what you want:
SELECT sh.SHEPHERD_NAME,
(SELECT (CASE WHEN COUNT(*) = 1 THEN MAX(s.SHEEP_NAME)
ELSE LISTAGG(s.SHEEP_ID || ': ' || s.SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY s.SHEEP_ID)
END) as SHEEPS
FROM SHEEPS s
WHERE s.SHEEP_SHEPHERD_ID = sh.SHEPHERD_ID
) as SHEEPS
FROM SHEPHERDS sh;
Here is a db<>fiddle.
The solution without a subquery use a simple GROUP BY, COUNT(*) = 1 to distinct the sheep count and two different LISTAGG statements
SELECT
s.SHEPHERD_NAME,
case when count(*) = 1 then
listagg(SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
else
listagg(SHEEP_ID || ': ' || SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID) end as SHEEPS
FROM SHEPHERDS s
JOIN SHEEPS sh on s.SHEPHERD_ID = sh.SHEEP_SHEPHERD_ID
GROUP BY s.SHEPHERD_NAME /* add SHEPHERD_ID in GROUP BY if the name is not unique */
returns
SHEPHERD_NAME, SHEEPS
Asher Mark
Joseph A: Andres
B: Jeffrey
Nicodemus A: Andres
B: Jeffrey
D: Andres
I have a table similar this
CREATE TABLE [dbo].[Test](
[harfno] INT,
[harf] NCHAR(1) NULL
)
and below values row count and harfno is variable
INSERT INTO [dbo].[Test]([harfno],[harf]) VALUES (1,'a')
INSERT INTO [dbo].[Test]([harfno],[harf]) VALUES (2,'b')
INSERT INTO [dbo].[Test]([harfno],[harf]) VALUES (3,'c')
INSERT INTO [dbo].[Test]([harfno],[harf]) VALUES (3,'d')
INSERT INTO [dbo].[Test]([harfno],[harf]) VALUES (4,'e')
INSERT INTO [dbo].[Test]([harfno],[harf]) VALUES (5,'f')
INSERT INTO [dbo].[Test]([harfno],[harf]) VALUES (5,'g')
INSERT INTO [dbo].[Test]([harfno],[harf]) VALUES (5,'h')
I need output like this: all word with special character list in each position ,position is harfno in each harfno maybe I have more than on char
word
abcef
abceg
abceh
abdef
abdeg
abdeh
Naive approach:
SELECT t1.harf + t2.harf+ t3.harf+ t4.harf+ t5.harf AS word
FROM Test t1, Test t2, Test t3, Test t4, Test t5
WHERE t1.harfno=1
AND t2.harfno=2
AND t3.harfno=3
AND t4.harfno=4
AND t5.harfno=5;
DBFidlde Demo
EDIT:
Recursive CTE:
WITH cte(harf, harfno) AS (
SELECT CAST(harf AS NVARCHAR(MAX)), 1 AS harfno
FROM Test
WHERE harfno=1
UNION ALL
SELECT c.harf + t.harf,t.harfno
FROM Test t
JOIN cte c
ON t.harfno = c.harfno+1
)
SELECT *
FROM cte
WHERE LEN(harf)= (SELECT MAX(harfno) FROM cte)
ORDER BY harf
--OPTION(MAXRECURSION nn)
DBFiddle Demo
v is defined as follows: create or replace type v is table of number and emp is a table which contains a column of type v.
I want to select the lines where v.count is 3, but I will get a compilation error. Is it because v.count is PL/SQL code?
I tried putting the code inside an anonymous block but it still didn't work.
Is using cursors the only solution?
SELECT *
FROM emp
WHERE V.COUNT = 3;
Thanks.
I think you're looking for cardinality():
CARDINALITY returns the number of elements in a nested table. The return type is NUMBER. If the nested table is empty, or is a null collection, then CARDINALITY returns NULL.
So you can do:
SELECT *
FROM emp
WHERE cardinality(V) = 3;
Quick demo:
create or replace type v is table of number
/
create table emp (id number, v v)
nested table v store as v_tab;
insert into emp (id, v) values (1, v(1));
insert into emp (id, v) values (2, v(1,2));
insert into emp (id, v) values (3, v(1,2,3));
insert into emp (id, v) values (4, v(1,2,3,4));
column v format a30
set feedback 1
SELECT *
FROM emp
WHERE cardinality(V) = 3;
ID V
---------- ------------------------------
3 V(1, 2, 3)
1 row selected.
I like Alex's cardinality answer, here is another approach:
create or replace type num_type as table of number;
create table table_with_num_type
(
ids num_type,
val varchar2(100)
)
nested table ids store as ids_tab ;
insert into table_with_num_type(ids, val) values (num_type(1,2,3), 'TEST1');
insert into table_with_num_type(ids, val) values (num_type(4,5,6,7), 'TEST2');
commit;
select t.val, count(t2.column_value) as num_count
from table_with_num_type t, table(t.ids) t2
group by t.val
having count(t2.column_value) = 3;
Result:
VAL NUM_COUNT
TEST1 3
My script is as below
CREATE TABLE #t (Id int, Name varchar(10))
INSERT INTO #t VALUES (1, 'A')
INSERT INTO #t VALUES (1, 'B')
INSERT INTO #t VALUES (1, 'C')
INSERT INTO #t VALUES (1, 'D')
INSERT INTO #t VALUES (2, 'E')
SELECT COUNT(0)FROM (SELECT COUNT(0) FROM #t GROUP BY Id) a
but I am getting an error
Msg 8155, Level 16, State 2, Line 5
No column name was specified for column 1 of 'A'.
When you use a subquery, all the columns need to given names:
SELECT COUNT(0)
FROM (SELECT COUNT(0) as cnt FROM #t GROUP BY Id
) a;
However, a simpler way to write this is:
SELECT COUNT(DISTINCT id)
FROM #t;
Actually, this isn't exactly the same. Your version will count NULL values but this does not. The exact equivalent is:
SELECT COUNT(DISTINCT id) + MAX(CASE WHEN id IS NULL THEN 1 ELSE 0 END)
FROM #t;
I have a sql table that has two columns id and name. I have list of names about 20 and I need to write a query that checks if name exists before insert.
Is there a better way of doing this rather then just having the below query 20 times but with different names (I need do this in t-sql):
IF NOT EXISTS(SELECT*
FROM mytable
WHERE name = 'Dan')
BEGIN
INSERT INTO mytable
(name)
VALUES ('dan')
END
INSERT INTO MyTable (Name)
SELECT NewNames.Name
FROM ( VALUES ('Name1'), ('Name2'), ('Name3') ) AS NewNames (Name)
WHERE NOT EXISTS ( SELECT 1
FROM MyTable AS MT
WHERE MT.Name = NewNames.Name );
I think you could use a merge statement:
MERGE INTO myTable AS Target
USING (VALUES ('name1'),('name2'),('...')) AS source (NAME)
ON Target.NAME = Source.NAME
WHEN NOT MATCHED BY TARGET THEN
INSERT (NAME) VALUES (name)
You can filter values with NOT EXISTS
INSERT INTO myTable (
Name
)
SELECT DISTINCT
Name
FROM (
VALUES ('Name 1'),
('Name 2')
) AS NewNames(Name)
WHERE
NOT EXISTS (SELECT 1 FROM TargetTable WHERE myTable.Name = NewNames.Name)
If your new names are in another table, you can change the select query in the above one.
Please note, that the DISTINCT keyword is necessary to filter out the duplications in the source data.
I would do this using insert:
with names as (
select 'Dan' as name union all
select 'name2' union all
. . .
)
insert into myTable(name)
select distinct name
from myTable
where not exists (select 1 from mytable t2 where t2.name = t.name);
Note: you may want to create a unique index on mytable(name) so the database does the checking for duplicates.
untested so there might be some minor errors:
merge into mytable x
using (
values ('name1')
, ('name2')
, ...
, ('namen')
) as y (name)
on x.name = y.name
when not matched then
insert (name)
values (y.name)
INSERT INTO MyTable (Name)
SELECT Name FROM
(
VALUES ('Name 1'),
('Name 2')
) AS Names(Name)
WHERE Name NOT IN
(
SELECT Name FROM MyTable
)
INSERT IGNORE INTO myTable (column1, column2) VALUES (val1, val2),(val3,val4),(val5,val6);
INSERT IGNORE will allow skip on duplicate values