SQL : Subquery without FROM clause - sql

This is the table structure
CREATE TABLE Book_Tag (id INT, Book_Id INT, tag varchar(20))
CREATE TABLE Book_Master (Book_Id INT, Book_title VARCHAR(50), price INT)
And the data looks like this :
INSERT INTO Book_Master
SELECT 1, 'Good Profit', 28 UNION ALL
SELECT 2, 'The Secret', 20 UNION ALL
SELECT 3, 'The One Minute Manager', 9 UNION ALL
SELECT 4, 'The 7 Habits of Highly Effective People', 35 UNION ALL
SELECT null, 'Who Moved My Cheese?', 15 UNION ALL
SELECT null, 'Blink: The Power of Thinking Without Thinking', 40
INSERT INTO Book_Tag
SELECT 1, 1, 'Management' UNION ALL
SELECT 2, 1, 'Profit' UNION ALL
SELECT 3, 2, 'Mind' UNION ALL
SELECT 4, 3, 'Management' UNION ALL
SELECT 5, 3, 'Efficiency' UNION ALL
SELECT 6, 3, 'Success' UNION ALL
SELECT 7, 4, 'Success' UNION ALL
SELECT 8, null, 'Time' UNION ALL
SELECT 9, 6, 'SelfHelp' UNION ALL
SELECT 10, 6, 'Motivation' UNION ALL
SELECT 11, 8, 'Mind'
select * from Book_Master
Book_Id Book_title price
1 Good Profit 28
2 The Secret 20
3 The One Minute Manager 9
4 The 7 Habits of Highly Effective People 35
NULL Who Moved My Cheese? 15
NULL Blink: The Power of Thinking Without Thinking 40
select * from Book_Tag
id Book_Id tag
1 1 Management
2 1 Profit
3 2 Mind
4 3 Management
5 3 Efficiency
6 3 Success
7 4 Success
8 NULL Time
9 6 SelfHelp
10 6 Motivation
11 8 Mind
I dont know why the following is working and also why the result is that.
select BT.* from Book_Tag BT
where BT.Book_Id in (select id)
id Book_Id tag
1 1 Management
or this
select BT.* from Book_Tag BT
where BT.Book_Id not in (select id)
id Book_Id tag
2 1 Profit
3 2 Mind
4 3 Management
5 3 Efficiency
6 3 Success
7 4 Success
9 6 SelfHelp
10 6 Motivation
11 8 Mind

Your first query:
select BT.*
from Book_Tag BT
where BT.Book_Id in (select id);
It is the same as:
select BT.*
from Book_Tag BT
where BT.Book_Id = BT.id;
That is why you get
1 1 Management
Keep in mind that NULL is not equal NULL or anything else.
In second example you have:
select BT.*
from Book_Tag BT
where BT.Book_Id not in (select id);
Which is the same as:
select BT.*
from Book_Tag BT
where BT.Book_Id <> BT.id;
Note that there is no
8 NULL Time row.
EDIT:
But,In the Subquery shouldnt we be specifying the table from which the id is coming.
From MSDN:
The general rule is that column names in a statement are implicitly qualified by the table referenced in the FROM clause at the same level. If a column does not exist in the table referenced in the FROM clause of a subquery, it is implicitly qualified by the table referenced in the FROM clause of the outer query.
and
If a column is referenced in a subquery that does not exist in the table referenced by the subquery's FROM clause, but exists in a table referenced by the outer query's FROM clause, the query executes without error. SQL Server implicitly qualifies the column in the subquery with the table name in the outer query.

You're asking here "where BT.Book_Id in (select id)", is saying where Book_ID = ID. So in this case it prints that out. Book_ID and ID are both 1. Then not, would be when they aren't equal, which is everything else from that table.

Related

how to select set of records is ID is present in one of them

Here is the table where ORGID/USERID makes unique combination:
ORGID USERID
1 1
1 2
1 3
1 4
2 1
2 5
2 6
2 7
3 9
3 10
3 11
I need to select all records (organizations and users) wherever USERID 1 is present. So USERID=1 is present in ORGID 1 and 2 so then select all users for these organizations including user 1 itself, i.e.
ORGID USERID
1 1
1 2
1 3
1 4
2 1
2 5
2 6
2 7
Is it possible to do it with one SQL query rather than SELECT *.. WHERE USERID IN (SELECT...
You could use exists:
select *
from mytable t
where exists (select 1 from mytable t1 where t1.orgid = t.orgid and t1.userid = 1)
Another option is window functions. In Postgres:
select *
from (
select t.*,
bool_or(userid = 1) over(partition by orgid) has_user_1
from mytable t
) t
where has_user_1
Or a more generic approach, that uses portable expressions:
select *
from (
select t.*,
max(case when userid = 1 then 1 else 0 end) over(partition by orgid) has_user_1
from mytable t
) t
where has_user_1 = 1
Yes, you can do it with a single select statement - no in or exists conditions, no analytic or aggregate functions in a subquery, etc. Why you want to do it that way is not clear; in any case, it is possible that the solution below is also more efficient than the alternatives. You will have to test on your real-life data to see if that is true.
The solution below has two potential disadvantages: it only works in Oracle (it uses a proprietary extension of SQL, the match_recognize clause); and it only works in Oracle 12.1 or higher.
with
my_table(orgid, userid) as (
select 1, 1 from dual union all
select 1, 2 from dual union all
select 1, 3 from dual union all
select 1, 4 from dual union all
select 2, 1 from dual union all
select 2, 5 from dual union all
select 2, 6 from dual union all
select 2, 7 from dual union all
select 3, 9 from dual union all
select 3, 10 from dual union all
select 3, 11 from dual
)
-- End of SIMULATED data (for testing), not part of the solution.
-- In real life you don't need the WITH clause; reference your actual table.
select *
from my_table
match_recognize(
partition by orgid
all rows per match
pattern (x* a x*)
define a as userid = 1
);
Output:
ORGID USERID
---------- ----------
1 1
1 2
1 3
1 4
2 1
2 5
2 7
2 6
You can use exists:
select ou.*
from orguser ou
where exists (select 1
from orguser ou ou2
where ou2.orgid = ou.orgid and ou2.userid = 1
);
Apart from Exists and windows function, you can use IN as follows:
select *
from your_table t
where t.orgid in (select t1.orgid from your_table t1 where t1.userid = 1)

Oracle Finding a string match from multiple database tables

This is somewhat a complex problem to describe, but I'll try to explain it with an example. I thought I would have been able to use the Oracle Instr function to accomplish this, but it does not accept queries as parameters.
Here is a simplification of my data:
Table1
Person Qualities
Joe 5,6,7,8,9
Mary 7,8,10,15,20
Bob 7,8,9,10,11,12
Table2
Id Desc
5 Nice
6 Tall
7 Short
Table3
Id Desc
8 Angry
9 Sad
10 Fun
Table4
Id Desc
11 Boring
12 Happy
15 Cool
20 Mad
Here is somewhat of a query to give an idea of what I'm trying to accomplish:
select * from table1
where instr (Qualities, select Id from table2, 1,1) <> 0
and instr (Qualities, select Id from table3, 1,1) <> 0
and instr (Qualities, select Id from table3, 1,1) <> 0
I'm trying to figure out which people have at least 1 quality from each of the 3 groups of qualities (tables 2,3, and 4)
So Joe would not be returned in the results because he does not have the quality from each of the 3 groups, but Mary and Joe would since they have at least 1 quality from each group.
We are running Oracle 12, thanks!
Here's one option:
SQL> with
2 table1 (person, qualities) as
3 (select 'Joe', '5,6,7,8,9' from dual union all
4 select 'Mary', '7,8,10,15,20' from dual union all
5 select 'Bob', '7,8,9,10,11,12' from dual
6 ),
7 table2 (id, descr) as
8 (select 5, 'Nice' from dual union all
9 select 6, 'Tall' from dual union all
10 select 7, 'Short' from dual
11 ),
12 table3 (id, descr) as
13 (select 8, 'Angry' from dual union all
14 select 9, 'Sad' from dual union all
15 select 10, 'Fun' from dual
16 ),
17 table4 (id, descr) as
18 (select 11, 'Boring' from dual union all
19 select 12, 'Happy' from dual union all
20 select 15, 'Cool' from dual union all
21 select 20, 'Mad' from dual
22 ),
23 t1new (person, id) as
24 (select person, regexp_substr(qualities, '[^,]+', 1, column_value) id
25 from table1 cross join table(cast(multiset(select level from dual
26 connect by level <= regexp_count(qualities, ',') + 1
27 ) as sys.odcinumberlist))
28 )
29 select a.person,
30 count(b.id) bid,
31 count(c.id) cid,
32 count(d.id) did
33 from t1new a left join table2 b on a.id = b.id
34 left join table3 c on a.id = c.id
35 left join table4 d on a.id = d.id
36 group by a.person
37 having ( count(b.id) > 0
38 and count(c.id) > 0
39 and count(d.id) > 0
40 );
PERS BID CID DID
---- ---------- ---------- ----------
Bob 1 3 2
Mary 1 2 2
SQL>
What does it do?
lines #1 - 22 represent your sample data
T1NEW CTE (lines #23 - 28) splits comma-separated qualities into rows, per every person
final select (lines #29 - 40) are outer joining t1new with each of "description" tables (table2/3/4) and counting how many qualities are contained in there for each of person's qualities (represented by rows from t1new)
having clause is here to return only desired persons; each of those counts have to be a positive number
Maybe this will help:
{1} Create a view that categorises all qualities and allows you to SELECT quality IDs and categories . {2} JOIN the view to TABLE1 and use a join condition that "splits" the CSV value stored in TABLE1.
{1} View
create or replace view allqualities
as
select 1 as category, id as qid, descr from table2
union
select 2, id, descr from table3
union
select 3, id, descr from table4
;
select * from allqualities order by category, qid ;
CATEGORY QID DESCR
---------- ---------- ------
1 5 Nice
1 6 Tall
1 7 Short
2 8 Angry
2 9 Sad
2 10 Fun
3 11 Boring
3 12 Happy
3 15 Cool
3 20 Mad
{2} Query
-- JOIN CONDITION:
-- {1} add a comma at the start and at the end of T1.qualities
-- {2} remove all blanks (spaces) from T1.qualities
-- {3} use LIKE and the qid (of allqualities), wrapped in commas
--
-- inline view: use UNIQUE, otherwise we may get counts > 3
--
select person
from (
select unique person, category
from table1 T1
join allqualities A
on ',' || replace( T1.qualities, ' ', '' ) || ',' like '%,' || A.qid || ',%'
)
group by person
having count(*) = ( select count( distinct category ) from allqualities )
;
-- result
PERSON
Bob
Mary
Tested w/ Oracle 18c and 11g. DBfiddle here.

Oracle SQL (Toad): Expand table

Suppose I have an SQL (Oracle Toad) table named "test", which has the following fields and entries (dates are in dd/mm/yyyy format):
id ref_date value
---------------------
1 01/01/2014 20
1 01/02/2014 25
1 01/06/2014 3
1 01/09/2014 6
2 01/04/2015 7
2 01/08/2015 43
2 01/09/2015 85
2 01/12/2015 4
I know from how the table has been created that, since there are value entries for id = 1 for February 2014 and June 2014, the values for March through May 2014 must be 0. The same applies to July and August 2014 for id = 1, and for May through July 2015 and October through November 2015 for id = 2.
Now, if I want to calculate, say, the median of the value column for a given id, I will not arrive at the correct result using the table as it stands - as I'm missing 5 zero entries for each id.
I would therefore like to create/use the following (potentially just temporary table)...
id ref_date value
---------------------
1 01/01/2014 20
1 01/02/2014 25
1 01/03/2014 0
1 01/04/2014 0
1 01/05/2014 0
1 01/06/2014 3
1 01/07/2014 0
1 01/08/2014 0
1 01/09/2014 6
2 01/04/2015 7
2 01/05/2015 0
2 01/06/2015 0
2 01/07/2015 0
2 01/08/2015 43
2 01/09/2015 85
2 01/10/2015 0
2 01/11/2015 0
2 01/12/2015 4
...on which I could then compute the median by id:
select id, median(value) as med_value from test group by id
How do I do this? Or would there be an alternative way?
Many thanks,
Mr Clueless
In this solution, I build a table with all the "needed dates" and value of 0 for all of them. Then, instead of a join, I do a union all, group by id and ref_date and ADD the values in each group. If the date had a row with a value in the original table, then that's the resulting value; and if it didn't, the value will be 0. This avoids a join. In almost all cases a union all + aggregate will be faster (sometimes much faster) than a join.
I added more input data for more thorough testing. In your original question, you have two id's, and for both of them you have four positive values. You are missing five values in each case, so there will be five zeros (0) which means the median is 0 in both cases. For id=3 (which I added) I have three positive values and three zeros; the median is half of the smallest positive number. For id=4 I have just one value, which then should be the median as well.
The solution includes, in particular, an answer to your specific question - how to create the temporary table (which most likely doesn't need to be a temporary table at all, but an inline view). With factored subqueries (in the WITH clause), the optimizer decides if to treat them as temporary tables or inline views; you can see what the optimizer decided if you look at the Explain Plan.
with
inputs ( id, ref_date, value ) as (
select 1, to_date('01/01/2014', 'dd/mm/yyyy'), 20 from dual union all
select 1, to_date('01/02/2014', 'dd/mm/yyyy'), 25 from dual union all
select 1, to_date('01/06/2014', 'dd/mm/yyyy'), 3 from dual union all
select 1, to_date('01/09/2014', 'dd/mm/yyyy'), 6 from dual union all
select 2, to_date('01/04/2015', 'dd/mm/yyyy'), 7 from dual union all
select 2, to_date('01/08/2015', 'dd/mm/yyyy'), 43 from dual union all
select 2, to_date('01/09/2015', 'dd/mm/yyyy'), 85 from dual union all
select 2, to_date('01/12/2015', 'dd/mm/yyyy'), 4 from dual union all
select 3, to_date('01/01/2016', 'dd/mm/yyyy'), 12 from dual union all
select 3, to_date('01/03/2016', 'dd/mm/yyyy'), 23 from dual union all
select 3, to_date('01/06/2016', 'dd/mm/yyyy'), 2 from dual union all
select 4, to_date('01/11/2014', 'dd/mm/yyyy'), 9 from dual
),
-- the "inputs" table constructed above is for testing only,
-- it is not part of the solution.
ranges ( id, min_date, max_date ) as (
select id, min(ref_date), max(ref_date)
from inputs
group by id
),
prep ( id, ref_date, value ) as (
select id, add_months(min_date, level - 1), 0
from ranges
connect by level <= 1 + months_between( max_date, min_date )
and prior id = id
and prior sys_guid() is not null
),
v ( id, ref_date, value ) as (
select id, ref_date, sum(value)
from ( select id, ref_date, value from prep union all
select id, ref_date, value from inputs
)
group by id, ref_date
)
select id, median(value) as median_value
from v
group by id
order by id -- ORDER BY is optional
;
ID MEDIAN_VALUE
-- ------------
1 0
2 0
3 1
4 9
If ref_date is date and is second
with int1 as (select id
, max(ref_date) as max_date
, min(ref_date) as min_date from test group by id )
, s(n) as (select level -1 from dual connect by level <= (select max(months_between(max_date, min_date)) from int1 ) )
select i.id
, add_months(i.min_date,s.n) as ref_date
, nvl(value,0) as value
from int1 i
join s on add_months(i.min_date,s.n) <= i.max_date
LEFT join test t on t.id = i.id and add_months(i.min_date,s.n) = t.ref_date
And with median
with int1 as (select id
, max(ref_date) as max_date
, min(ref_date) as min_date from test group by id )
, s(n) as (select level -1 from dual connect by level <= (select max(months_between(max_date, min_date)) from int1 ) )
select i.id
, MEDIAN(nvl(value,0)) as value
from int1 i
join s on add_months(i.min_date,s.n) <= i.max_date
LEFT join test t on t.id = i.id and add_months(i.min_date,s.n) = t.ref_date
group by i.id

How do I display Rows in a table where all values but the first one for a column is null

So I am trying to pull rows from a table where there are more than one version for an ID that has at least one person for the ID that is not null but the versions that come after it are null.
So, if i had a statement like:
select ID, version, person from table1
the output would be:
ID Version Person
-- ------- ------
1 1 Tom
1 2 null
1 3 null
2 1 null
2 2 null
2 3 null
3 1 Mary
3 2 Mary
4 1 Joseph
4 2 null
4 3 Samantha
The version number can have an infinite value and is not limited.
I want to pull ID 1 version 2/3, and ID 4 Version 2.
So in the case of ID 2 where the person is null for all three rows I don't need these rows. And in the case of ID 3 version 1 and 2 I don't need these rows because there is never a null value.
This is a very simple version of the table I am working with but the "real" table is a lot more complicated with a bunch of joins already in it.
The desired output would be:
ID Version Person
-- ------- ------
1 2 null
1 3 null
4 2 null
The result set that I am looking for is where in a previous version for the same ID there was a person listed but is now null.
You are seeking all rows where the person is not null and that id has null rows, and the not null person version is less than the null version for the same person id:
Edited predicate based on comment
with sample_data as
(select 1 id, 1 version, 'Tom' person from dual union all
select 1, 2, null from dual union all
select 1, 3, null from dual union all
select 2, 1, null from dual union all
select 2, 2, null from dual union all
select 2, 3, null from dual union all
select 3, 1, 'Mary' from dual union all
select 3, 2, 'Mary' from dual union all
select 4, 1, 'Joseph' from dual union all
select 4, 2, null from dual union all
select 4, 3, 'Samantha' from dual)
select *
from sample_data sd
where person is null
and exists
(select 1 from sample_data
where id = sd.id
and person is not null
and version < sd.version);
/* Old predicate
and id in
(select id from sample_data where person is not null);
*/
I think this query translates pretty nicely into what you asked for?
List all the rows (R) where the person is null, but only if a previous row (P) with a non-null name exists.
select *
from table1 r
where r.person is null
and exists(
select 'x'
from table1 p
where p.id = r.id
and p.version < r.version
and p.person is not null
);
I believe the below should work.
select ID, listagg(version, ', ') within group (order by version) as versions
from table1 t1
where 0 < (select count(*) from table1 t1A where t1A.ID = t1.ID and t1A.version is not null)
and 0 < (select count(*) from table1 t1B where t1B.ID = t1.ID and t1B.version is null)
and person is null
group by ID
This should do what you want:
select id, version, person
from
(
select id, version, person,
lag(person, 1) ignore nulls
over (partition by id
order by version) as x
from table1
) dt
where person is null
and x is not null

Recursive calculation to form a tree using sql

I am working on a simple problem and wanted to solve it using SQL. I am having 3 tables Category, Item & a relational table CategoryItem. I need to return count of items per category but the twist is Categories are arranged in Parent-Child relationships and the count of items in child categories should be added to the count in its parent Category. Please consider the sample data below and the expected resultset using SQL.
Id Name ParentCategoryId
1 Category1 Null
2 Category1.1 1
3 Category2.1 2
4 Category1.2 1
5 Category3.1 3
ID CateoryId ItemId
1 5 1
2 4 2
3 5 2
4 3 1
5 2 3
6 1 1
7 3 2
Result:
CategoryNAme Count
Category1 7
Category1.1 5
Category2.1 4
Category1.2 1
Category3.1 2
I can do it in my business layer but performance its not optimal because of size of data. I am hoping if I can do it in data layer, I would be able to improve performance greatly.
Thanks in Advance for your reply
your tables and sample data
create table #Category(Id int identity(1,1),Name Varchar(255),parentId int)
INSERT INTO #Category(Name,parentId) values
('Category1',null),('Category1.1',1),('Category2.1',2),
('Category1.2',1),('Category3.1',3)
create table #CategoryItem(Id int identity(1,1),categoryId int,itemId int)
INSERT INTO #CategoryItem(categoryId,itemId) values
(5,1),(4,2),(5,2),(3,1),(2,3),(1,1),(3,2)
create table #Item(Id int identity(1,1),Name varchar(255))
INSERT INTO #Item(Name) values('item1'),('item2'),('item3')
Checking for all childs of parent by Recursive Commom Table Expressions
;WITH CategorySearch(ID, parentId) AS
(
SELECT ID, ID AS ParentId FROM #Category
UNION ALL
SELECT CT.Id,CS.parentId FROM #Category CT
INNER JOIN CategorySearch CS ON CT.ParentId = CS.ID
)
select * from CategorySearch order by 1,2
Output: All child records against parent
ID parentId
1 1
2 1
3 1
4 1
5 1
2 2
3 2
5 2
3 3
5 3
4 4
5 5
Final query for your result, count all items for category and its children categories.
;WITH CategorySearch(ID, parentId) AS
(
SELECT ID, ID AS ParentId FROM #Category
UNION ALL
SELECT CT.Id,CS.parentId FROM #Category CT
INNER JOIN CategorySearch CS ON CT.ParentId = CS.ID
)
SELECT CA.Name AS CategoryName,count(itemId) CountItem
FROM #Category CA
INNER JOIN CategorySearch CS ON CS.ParentId = CA.id
INNER JOIN #CategoryItem MI ON MI.CategoryId =CS.ID
GROUP BY CA.Name
Output:
CategoryName CountItem
Category1 7
Category1.1 5
Category1.2 1
Category2.1 4
Category3.1 2
with help of CTE (common table expression) with recursion, you could achieve what you are looking for.
see Microsoft help for more details retated to recursive CTEs: CTE MS SQL 2008 +
hereby you could find complete example, with your sample data:
-- tables definition
SELECT 1 as id, 'cat1' as [name],NULL as id_parent
into cat
union
select 2, 'cat1.1', 1
union
select 3, 'cat2.1', 2
union
select 4, 'cat1.2', 1
union
select 5, 'cat3.1', 3
select 1 as id , 5 as id_cat, 1 as id_item
iNTO item
UNION
select 2, 4, 2
UNION
select 3, 5, 2
UNION
select 4, 3, 1
UNION
select 5, 2, 3
UNION
select 6, 1, 1
UNION
select 7, 3, 2
-- CTE to get desired result
with childs
as
(
select c.id, c.id_parent
from cat c
UNION ALL
select s.id, p.id_parent
from cat s JOIN childs p
ON (s.id_parent=p.id)
),
category_count
AS
(
SELECT c.id, c.name, count(i.id) as items
from cat c left outer join item i
on (c.id=i.id_cat)
GROUP BY c.id,c.name
),
pairs
AS
(
SELECT id, ISNULL(id_parent,id) as id_parent
FROM childs
)
select p.id_parent, n.name, sum(items)
from pairs p JOIN category_count cc
ON (p.id=cc.id)
join cat n ON (p.id_parent=n.id)
GROUP by p.id_parent ,n.name
ORDER by 1;