Oracle perform a count for each row on a table - sql

I have a doubt regarding a query in oracle SQL.
I have a groups table, that I can query as:
SELECT *
FROM groups g
WHERE g.owner = 123;
This would give something like:
groupId
owner
1
123
2
123
3
123
4
123
I also have another table of administrators, that I can query as:
SELECT *
FROM admins a
ORDER BY groupId;
This would get administrators as:
adminId
userName
groupId
1
myadmin1
1
2
myAdmin2
1
3
myAdmin3
1
4
myAdmin4
2
5
myAdmin5
3
6
myAdmin6
3
That basically means that a group can have multiple administrators. I would like to count the number of administrators for each group. A result such as:
groupId
owner
adminCount
1
123
3
2
123
1
3
123
2
4
123
0
However, I cannot make a count of each administrator in the table and then make a join, as it is a table with a lot of rows.
I would like to perform the count query
SELECT count(*)
FROM admins a
WHERE groupId = 1;
for each row of the groups query, such that I get the desired result without performing a count of each administrator in the table, just the ones that belong to the groups from a specific owner.
Does someone know how can I count it without counting all the rows in the administrators table?
Thanks

The easiest and most readable variant is to use outer apply (or lateral(+)):
select *
from groups g
outer apply (
select count(*) as adminCount
from admins a
where a.groupId=g.groupId
);
Or you can get the same results using subqueries (moreover, in fact Oracle optimizer can decide to transform outer-apply/lateral to this variant, since it has "lateral view decorrelation" transformation):
select g.groupId,g.owner, nvl(a.adminCount,0) as adminCount
from groups g
left join (
select x.groupId, count(*) as adminCount
from admins x
group by x.groupId
) a
on a.groupId=g.groupId;
or even group-by with join:
select g.groupId,g.owner, count(a.groupId) as adminCount
from groups g
left join admins a
on g.groupId=a.groupId
group by g.groupId,g.owner
https://dbfiddle.uk/a-Q_abg8

You could use analytic function COUNT() OVER() ...
Select Distinct
g.GROUP_ID,
g.OWNER,
Count(a.ADMIN_ID) OVER(Partition By g.GROUP_ID) "COUNT_ADMINS"
From groups g
Left Join admins a ON(a.GROUP_ID = g.GROUP_ID)
Where g.OWNER = 123
Order By g.GROUP_ID
... this requires the Distinct keyword which could be performance costly with big datasets. I don't expect that user groups and admins are that big.
WIth your sample data:
WITH
groups (GROUP_ID, OWNER) AS
(
Select 1, 123 From Dual Union ALL
Select 2, 123 From Dual Union ALL
Select 3, 123 From Dual Union ALL
Select 4, 123 From Dual
),
admins (ADMIN_ID, ADMIN_USER_NAMAE, GROUP_ID) AS
(
Select 1, 'myadmin1', 1 From Dual Union All
Select 2, 'myadmin2', 1 From Dual Union All
Select 3, 'myadmin3', 1 From Dual Union All
Select 4, 'myadmin4', 2 From Dual Union All
Select 5, 'myadmin5', 3 From Dual Union All
Select 6, 'myadmin6', 3 From Dual
)
... the result is
GROUP_ID
OWNER
COUNT_ADMINS
1
123
3
2
123
1
3
123
2
4
123
0

Related

Find rows with a group values from other table

id
role
Group_ID
1
A
1
2
B
1
3
A
2
4
D
2
5
A
3
6
B
3
7
C
3
8
C
4
...
User_id
role
user1
A
user1
B
user2
C
user2
D
user3
A
user3
D
user4
C
user5
A
user5
B
user5
C
user5
D
...
I have 2 tables Table1 and Table2 as shown above.
My requirement is to get the User_ID from the table2 which has all the roles from a group. Additionally, only those groups need to be checked which has at least 2 roles. If a group_ID has only 1 role then it should not be considered
For example, this is how the result will look like from above 2 tables
user1 has both the roles from group 1 (A,B) -> therefore it is in the results.
user3 has both the roles from group 2 (A,D) -> therefore it is in the results.
user5 has all the roles from group 1(A,B), 2(A,D) and 3(A,B,C) -> therefore it is in the results.
User2 has role C and D which is not a group, hence not shown in the result
User4 has role C which is a group (Group_ID = 4), but the group should have at least 2 roles, hence not shown in the result
User_id
Group_ID
user1
1
user3
2
user5
1
user5
2
user5
3
....
Select Table2.USER_ID,Table1.GROUP_ID
from Table2,
Table1
Where Table2.ROLE = Table1.ROLE
group by Table1.GROUP_ID,Table2.USER_ID
With the above query, I am able to get the records with user_id assigned any of the role. However, I want to get the User_ID from the table2 which has all the roles from a group.
Any help is much appreciated. I will also make sure to accept the answer leading me to the solution
A direct translation of the requirement into SQL (Oracle style)
with g as (
select group_id, count(*) role_cnt from table1 group by group_id having count(*) > 1),
u as (
select u.user_id, g.group_id, count(*) usergroup_role_cnt from table1 g, table2 u
where u.role=g.role
group by u.user_id, g.group_id)
select u.user_id, g.group_id from g, u
where u.group_id=g.group_id
and u.usergroup_role_cnt=g.role_cnt;
Edit:
There has been concern about query performance using "group by" when tables are "very large". I don't know how "very large" is defined. Anyway, I did a test on Oracle Clould Free Tier ATP database 19c, using tables with tens of thousand rows. Here is the result.
select count(*) from table1;
COUNT(*)
--------
289327
Elapsed: 00:00:00.007
1 rows selected.
select count(*) from table2;
COUNT(*)
--------
1445328
Elapsed: 00:00:00.024
1 rows selected.
with g as (
select group_id, count(*) role_cnt from table1 group by group_id having count(*) > 1),
u as (
select u.user_id, g.group_id, count(*) usergroup_role_cnt from table1 g, table2 u
where u.role=g.role
group by u.user_id, g.group_id)
select u.user_id, g.group_id from g, u
where u.group_id=g.group_id
and u.usergroup_role_cnt=g.role_cnt;
USER_ID GROUP_ID
------- --------
user99 994
user97 462
user97 199
user87 35
user99 462
user87 179
user99 199
user98 199
user96 199
user87 813
user87 941
user96 994
user97 994
user96 462
user98 462
user98 994
Elapsed: 00:00:04.770
16 rows selected.
select a2.user_id, a1.group_id
from (
select user_id, cast(collect(role) as role_list) as user_roles
from table2
group by user_id
) a2
inner join
(
select group_id, cast(collect(role) as role_list) as group_roles
from table1
group by group_id
) a1
on a1.group_roles submultiset of a2.user_roles
order by user_id, group_id
;
USER_ID GROUP_ID
------- --------
user87 35
user87 179
user87 813
user87 941
user96 199
user96 462
user96 994
user97 199
user97 462
user97 994
user98 199
user98 462
user98 994
user99 199
user99 462
user99 994
Elapsed: 00:01:35.395
16 rows selected.
select q2.user_id, q1.group_id
from (select distinct user_id from table2) q2
cross join
(select distinct group_id from table1) q1
where not exists
(
select role
from table1
where group_id = q1.group_id
and role not in
(
select role
from table2
where user_id = q2.user_id
)
)
order by user_id, group_id
;
(Manually cancelled after 10 min)
Data is generated quite randomly and is somewhat skewed. But in reality, skewed data is not avoidable. Statistics should minimize the impact. (Both tables were analyzed and have no indexes)
Here is a solution that will work in Oracle Database; adapt it for SQL Server (if that is possible; I don't know that dialect).
Test data (others may use this too):
create table table1(id number, role varchar2(10), group_id number);
insert into table1 (id, role, group_id)
select 1, 'A', 1 from dual union all
select 2, 'B', 1 from dual union all
select 3, 'A', 2 from dual union all
select 4, 'D', 2 from dual union all
select 5, 'A', 3 from dual union all
select 6, 'B', 3 from dual union all
select 7, 'C', 3 from dual
;
create table table2 (user_id varchar2(20), role varchar2(10));
insert into table2 (user_id, role)
select 'user1', 'A' from dual union all
select 'user1', 'B' from dual union all
select 'user2', 'C' from dual union all
select 'user2', 'D' from dual union all
select 'user3', 'A' from dual union all
select 'user3', 'D' from dual union all
select 'user4', 'C' from dual union all
select 'user5', 'A' from dual union all
select 'user5', 'B' from dual union all
select 'user5', 'C' from dual union all
select 'user5', 'D' from dual
;
commit;
Create user-defined data type (collection of strings representing roles):
create or replace type role_list as table of varchar2(10);
/
Query and output:
select a2.user_id, a1.group_id
from (
select user_id, cast(collect(role) as role_list) as user_roles
from table2
group by user_id
) a2
inner join
(
select group_id, cast(collect(role) as role_list) as group_roles
from table1
group by group_id
) a1
on a1.group_roles submultiset of a2.user_roles
order by user_id, group_id
;
USER_ID GROUP_ID
------------ ----------
user1 1
user3 2
user5 1
user5 2
user5 3
The strategy is pretty obvious, and should be easy to read directly from the code. Group the roles by group_id in the first table and by user_id in the second. Identify all the pairs (user, group) where all roles for the group are found in the role list of the user - that is exactly what the submultiset comparison operator does.
A more rudimentary query (harder to follow and maintain, and likely slower), but perhaps helpful as it is likely to work with very few changes - if any - in pretty much all SQL dialects, might look like this. Assuming role can't be null in table2 (to make the query slightly simpler):
select q2.user_id, q1.group_id
from (select distinct user_id from table2) q2
cross join
(select distinct group_id from table1) q1
where not exists
(
select role
from table1
where group_id = q1.group_id
and role not in
(
select role
from table2
where user_id = q2.user_id
)
)
order by user_id, group_id
;

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)

Multiple repeated structures in Bigquery

Following up on this- Bigquery multiple unnest in a single select
We are using bigquery as our warehousing solution and are trying to push the limit by trying to consolidate. A simple example is client tracking. Client generates revenue, has several touch points on our site, and independently maintains several accounts with us. For a business user wanting to do behavior analysis on clients, they want to track visits, revenue generated and how their accounts impacT retention, we are trying to evaluate if a nested structure would work for us
Below is an example. I have 3 tables.
Clients (C)
C_Key| C_Name
-----|------
1 | ABC
2 | DEF
Accounts (A)
A_Key | C_Key
11 | 1
12 | 1
21 | 2
22 | 2
23 | 2
Revenue (R)
R_Key | C_Key | Revenue
-------|---------|----------
11 | 1 | $10
12 | 1 | $20
21 | 2 | $10
I used array_agg to combine these three into a single nested table that looks like below:
{Client,
Accounts:
[{
}],
Revenue:
[{
}]
}
I want to be able to use multiple unnests in a single query like below
Select client, Count Distinct(Accounts) and SUM(Revenue) from <single nested
table>, unnest accounts, unnest revenue
The expected output are 2 rows,
1,2,$30
2,3,$10
However, having multiple unnests in the same query results in a cross join.
The actual output is
1,2,$60
2,3,$30
Below is for BigQuery Standard SQL
First let's clarify creation of single nested table
I hope you did something like :
#standardSQL
WITH clients AS (
SELECT 1 AS c_key, 'abc' AS c_name UNION ALL
SELECT 2, 'def'
), accounts AS (
SELECT 11 AS a_key, 1 AS c_key UNION ALL
SELECT 12, 1 UNION ALL
SELECT 21, 2 UNION ALL
SELECT 22, 2 UNION ALL
SELECT 23, 2
), revenue AS (
SELECT 11 AS r_key, 1 AS c_key, 10 AS revenue UNION ALL
SELECT 12, 1, 20 UNION ALL
SELECT 21, 2, 10
), single_nested_table AS (
SELECT x.c_key, x.c_name, accounts, revenue
FROM (
SELECT c.c_key, c_name, ARRAY_AGG(a) AS accounts --, array_agg(r) as revenue
FROM clients AS c
LEFT JOIN accounts AS a ON a.c_key = c.c_key
GROUP BY c.c_key, c_name
) x
JOIN (
SELECT c.c_key, c_name, ARRAY_AGG(r) AS revenue
FROM clients AS c
LEFT JOIN revenue AS r ON r.c_key = c.c_key
GROUP BY c.c_key, c_name
) y
ON x.c_key = y.c_key
)
SELECT *
FROM single_nested_table
which creates table as
Row c_key c_name accounts.a_key accounts.c_key revenue.r_key revenue.c_key revenue.revenue
1 1 abc 11 1 11 1 10
12 1 12 1 20
2 2 def 21 2 21 2 10
22 2
23 2
Not that important what exactly query you used to create that table - but do important to clear the structure / schema!
So now, back to your question
#standardSQL
WITH clients AS (
SELECT 1 AS c_key, 'abc' AS c_name UNION ALL
SELECT 2, 'def'
), accounts AS (
SELECT 11 AS a_key, 1 AS c_key UNION ALL
SELECT 12, 1 UNION ALL
SELECT 21, 2 UNION ALL
SELECT 22, 2 UNION ALL
SELECT 23, 2
), revenue AS (
SELECT 11 AS r_key, 1 AS c_key, 10 AS revenue UNION ALL
SELECT 12, 1, 20 UNION ALL
SELECT 21, 2, 10
), single_nested_table AS (
SELECT x.c_key, x.c_name, accounts, revenue
FROM (
SELECT c.c_key, c_name, ARRAY_AGG(a) AS accounts --, array_agg(r) as revenue
FROM clients AS c
LEFT JOIN accounts AS a ON a.c_key = c.c_key
GROUP BY c.c_key, c_name
) x
JOIN (
SELECT c.c_key, c_name, ARRAY_AGG(r) AS revenue
FROM clients AS c
LEFT JOIN revenue AS r ON r.c_key = c.c_key
GROUP BY c.c_key, c_name
) y
ON x.c_key = y.c_key
)
SELECT
c_key, c_name,
ARRAY_LENGTH(accounts) AS distinct_accounts,
(SELECT SUM(revenue) FROM UNNEST(revenue)) AS revenue
FROM single_nested_table
this gives what you asked for:
Row c_key c_name distinct_accounts revenue
1 1 abc 2 30
2 2 def 3 10

fetch record as in order passed for IN condition in oracle

I want to fetch the records in order passed for IN condition.
select * from table where id in(6,3,7,1);
is returning the rows as
id name
1 abc
3 xy
6 ab
7 ac
but I want to display the records in same orders as ids passed in condition in Oracle
id name
6 ab
3 xy
7 ac
1 abc
Please help me in fetching the records in same order as in condition ids in oracle. The values in IN condition may change dynamically.
You can do this with a case statement in the order by clause or using a join.
select *
from table
where id in(6,3,7,1)
order by (case id when 6 then 1 when 3 then 2 when 7 then 3 when 1 then 4 end);
Or:
with ids as (
select 6 as id, 1 as ordering from dual union all
select 3 as id, 2 as ordering from dual union all
select 7 as id, 3 as ordering from dual union all
select 1 as id, 4 as ordering from dual
)
select *
from table t join
ids
on t.ids = ids.id
order by ids.ordering;
Note that you don't need the in in this case, because the join does the filtering.
you can use trick
select * from table where id in(6,3,7,1) order by case when id = 6 then 1
id = 3 then 2
id = 7 then 3
id = 1 then 4
end

How to do select count(*) group by and select * at same time?

For example, I have table:
ID | Value
1 hi
1 yo
2 foo
2 bar
2 hehe
3 ha
6 gaga
I want my query to get ID, Value; meanwhile the returned set should be in the order of frequency count of each ID.
I tried the query below but don't know how to get the ID and Value column at the same time:
SELECT COUNT(*) FROM TABLE group by ID order by COUNT(*) desc;
The count number doesn't matter to me, I just need the data to be in such order.
Desire Result:
ID | Value
2 foo
2 bar
2 hehe
1 hi
1 yo
3 ha
6 gaga
As you can see because ID:2 appears most times(3 times), it's first on the list,
then ID:1(2 times) etc.
you can try this -
select id, value, count(*) over (partition by id) freq_count
from
(
select 2 as ID, 'foo' as value
from dual
union all
select 2, 'bar'
from dual
union all
select 2, 'hehe'
from dual
union all
select 1 , 'hi'
from dual
union all
select 1 , 'yo'
from dual
union all
select 3 , 'ha'
from dual
union all
select 6 , 'gaga'
from dual
)
order by 3 desc;
select t.id, t.value
from TABLE t
inner join
(
SELECT id, count(*) as cnt
FROM TABLE
group by ID
)
x on x.id = t.id
order by x.cnt desc
How about something like
SELECT t.ID,
t.Value,
c.Cnt
FROM TABLE t INNER JOIN
(
SELECT ID,
COUNT(*) Cnt
FROM TABLE
GROUP BY ID
) c ON t.ID = c.ID
ORDER BY c.Cnt DESC
SQL Fiddle DEMO
I see the question is already answered, but since the most obvious and most simple solution is missing, I'm posting it anyway. It doesn't use self joins nor subqueries:
SQL> create table t (id,value)
2 as
3 select 1, 'hi' from dual union all
4 select 1, 'yo' from dual union all
5 select 2, 'foo' from dual union all
6 select 2, 'bar' from dual union all
7 select 2, 'hehe' from dual union all
8 select 3, 'ha' from dual union all
9 select 6, 'gaga' from dual
10 /
Table created.
SQL> select id
2 , value
3 from t
4 order by count(*) over (partition by id) desc
5 /
ID VALU
---------- ----
2 bar
2 hehe
2 foo
1 yo
1 hi
6 gaga
3 ha
7 rows selected.