Group and sort by associated table attribute - sql

There are two tables, Group sections and Groups.
I want to group and sort groups by group sections.
Group Sections:
Id | Name | Priority
1 | Football | 2
2 | Basketball | 1
3 | Tennis | 3
Groups:
Id | section_id | Name
1 | 1 | Barcelona
2 | NULL | Noname
3 | 1 | Real Madrid
4 | 2 | Cedevita
5 | 3 | Ljubljana
What i got for now in sql:
SELECT group_sections.id, group_sections.priority AS priority, group_sections.name, groups.*
FROM groups
LEFT OUTER JOIN group_sections ON group_sections.group_id = groups.id
GROUP BY group_sections.id, groups.id
ORDER BY group_sections.priority ASC
What I want to get:
Football => [<Group id: 1>, <Group id: 3], Basketball => [<Group id: 2>], Tennis => [<Group id: 3]
How can I get this in rails active record?
#groups.joins(:group_sections).group('group_sections.id', 'groups.id').order('group_sections.priority ASC')
This query is not working for me. Any ideas?

you can try string_agg
select string_agg(name || ' => [' || groupnames ,', ') groupnames
from(
SELECT b.name, string_agg(a.name,', ') || ']' groupnames
FROM groups1 a
JOIN group_sections b ON b.id = a.sectionid
group by a.sectionid, b.name, b.priority
order by b.priority) x

Postgresql has useful function for it named string_agg. Here example for your situation:
with
gs as (
select 1 as id, 'Football' as name, 2 as Priority union all
select 2 as id, 'Basketball' as name, 1 as Priority union all
select 3 as id, 'Tennis' as name, 3 as Priority
),
g as (
select 1 as id, 1 as section_id, 'Barcelona' as Name union all
select 2 as id, null as section_id, 'Noname' as Name union all
select 3 as id, 1 as section_id, 'Real Madrid' as Name union all
select 4 as id, 2 as section_id, 'Cedevita ' as Name
)
select
gs.id, gs.name, gs.Priority,
string_agg(g.id::text,',') as id_of_groups
from g
left join gs
on g.section_id = gs.id
group by gs.id, gs.name, gs.Priority
order by gs.Priority

Related

Postgres group by empty string question to include empty string in output

I have following table in Postgres
| phone | group | spec |
| 1 | 1 | 'Lock' |
| 1 | 2 | 'Full' |
| 1 | 3 | 'Face' |
| 2 | 1 | 'Lock' |
| 2 | 3 | 'Face' |
| 3 | 2 | 'Scan' |
Tried this
SELECT phone, string_agg(spec, ', ')
FROM mytable
GROUP BY phone;
Need this ouput for each phone where there is empty string for missing group.
| phone | spec
| 1 | Lock, Full, Face
| 2 | Lock, '' , Face
| 3 | '', Scan ,''
You need a CTE which returns all possible combinations of phone and group and a left join to the table so you can group by phone:
with cte as (
select *
from (
select distinct phone from mytable
) m cross join (
select distinct "group" from mytable
) g
)
select c.phone, string_agg(coalesce(t.spec, ''''''), ',') spec
from cte c left join mytable t
on t.phone = c.phone and t."group" = c."group"
group by c.phone
See the demo.
Results:
| phone | spec |
| ----- | -------------- |
| 1 | Lock,Full,Face |
| 2 | Lock,'',Face |
| 3 | '',Scan,'' |
You can use conditional aggregation:
select phone,
(max(case when group = 1 then spec else '''''' end) || ', ' ||
max(case when group = 2 then spec else '''''' end) || ', ' ||
max(case when group = 3 then spec else '''''' end)
) as specs
from mytable t
group by phone;
Alternatively, you can general all the groups using generate_series() and then aggregation:
select p.phone,
string_agg(coalesce(t.spec, ''''''), ', ') as specs
from (select distinct phone from mytable) p cross join
generate_series(1, 3, 1) gs(grp) left join
mytable t
on t.phone = p.phone and t.group = gs.grp
group by p.phone
You can consider using a self - (RIGHT/LEFT)JOIN with all three distinct groups (which's stated within the subquery just after RIGHT JOIN keywords ) and a correlated query for your table :
WITH mytable1 AS
(
SELECT distinct t1.phone, t2."group",
( SELECT spec FROM mytable WHERE phone = t1.phone AND "group"=t2."group" )
FROM mytable t1
RIGHT JOIN ( SELECT distinct "group" FROM mytable ) t2
ON t2."group" = coalesce(t2."group",t1."group")
)
SELECT phone, string_agg(coalesce(spec,''''''), ', ') as spec
FROM mytable1
GROUP BY phone;
Demo

Removing group of results if total is 0

I am using the following table to create a stacked bar chart - its quite a bit larger than this:
ID | Name | foodEaten | total
1 | Sam | Burger | 3
1 | Sam | Pizza | 1
1 | Sam | Kebab | 0
1 | Sam | Cheesecake| 3
1 | Sam | Sandwich | 5
2 | Jeff | Burger | 0
2 | Jeff | Pizza | 0
2 | Jeff | Kebab | 0
2 | Jeff | Cheesecake| 0
2 | Jeff | Sandwich | 0
I need to find a way to remove results like Jeff. Where the entire total for what he ate is 0. I can't think of the easiest way to achieve this. I've tried grouping the entire result by Id and creating a total, but its just not happening.
If the person has eaten a total of 0 food, then he needs to be excluded. But if he hasn't, and he hasn't eaten any kebabs, as shown in my above table, this needs to be included in the result!
So the output needed is:
ID | Name | foodEaten | total
1 | Sam | Burger | 3
1 | Sam | Pizza | 1
1 | Sam | Kebab | 0
1 | Sam | Cheesecake| 3
1 | Sam | Sandwich | 5
Assuming that you want the data as it appears, and not the aggregate out and then exclude:
WITH CTE AS (
SELECT ID,
[Name],
foodEaten,
total,
SUM(total) OVER (PARTITION BY [Name]) AS nameTotal
FROM YourTable)
SELECT ID,
[Name],
foodEaten,
total
FROM CTE
WHERE nameTotal > 0;
select id, name, foodEaten, sum(total) as total from <table> group by ID having sum(total) > 0
Does this work for you?
You can try below -
select id,name
from tablename a
group by id,name
having sum(total)>0
OR
DEMO
select * from tablename a
where not exists (select 1 from tablename b where a.id=b.id group by id,name
having sum(total)=0)
Try this
;WITH CTE (ID , Name , foodEaten , total)
AS
(
SELECT 1 , 'Sam' , 'Burger' , 3 UNION ALL
SELECT 1 , 'Sam' , 'Pizza' , 1 UNION ALL
SELECT 1 , 'Sam' , 'Kebab' , 2 UNION ALL
SELECT 1 , 'Sam' , 'Cheesecake', 3 UNION ALL
SELECT 1 , 'Sam' , 'Sandwich' , 5 UNION ALL
SELECT 2 , 'Jeff' , 'Burger' , 0 UNION ALL
SELECT 2 , 'Jeff' , 'Pizza' , 0 UNION ALL
SELECT 2 , 'Jeff' , 'Kebab' , 0 UNION ALL
SELECT 2 , 'Jeff' , 'Cheesecake', 0 UNION ALL
SELECT 2 , 'Jeff' , 'Sandwich' , 0
)
SELECT ID , Name ,SUM( total) AS Grandtotal
FROM CTE
GROUP BY ID , Name
HAVING SUM( total) >0
Result
ID Name Grandtotal
----------------------
1 Sam 14
Using DELETE with HAVING SUM(total) = 0 will remove the group of result which their total is 0
DELETE FROM TableName
WHERE ID IN (SELECT Id FROM TableName GROUP BY ID HAVING SUM(total) = 0)
or if you want to remvoe and select only the records which has sum of total is zero, then
SELECT * FROM TableName
WHERE ID NOT IN (SELECT Id FROM TableName GROUP BY ID HAVING SUM(total) = 0)
Assuming total is never negative, then probably the most efficient method is to use exists:
select t.*
from t
where exists (select 1
from t t2
where t2.name = t.name and
t2.total > 0
);
In particular, this can take advantage of an index on (name, total).

joining table with multiple rows and same column name

table 1
id | name | gender
1 | ABC | M
2 | CDE | M
3 | FGH | M
table 2
id | name | gender
4 | BAC | F
5 | DCE | F
6 | GFH | F
how to make output in oracle database like this :
id | name | gender
1 | ABC | M
2 | CDE | M
3 | FGH | M
4 | BAC | F
5 | DCE | F
6 | GFH | F
Use UNION [ALL]:
select * from table1
union all
select * from table2;
P.S. If there exists any duplicated row for individual SELECT statements, UNION would remove duplicates, but UNION ALL concatenates rows even they are duplicates.
If you really need to "join" 2 tables:
with a as (
select 1 id, 'ABC' name, 'M' gender from dual union all
select 2 id, 'CDE' name, 'M' gender from dual union all
select 3 id, 'FGH' name, 'M' gender from dual ),
b as (
select 4 id, 'BAC' name, 'F' gender from dual union all
select 5 id, 'DCE' name, 'F' gender from dual union all
select 6 id, 'GFH' name, 'F' gender from dual )
select coalesce(a.id, b.id) id,
coalesce(a.name, b.name) name,
coalesce(a.gender, b.gender) gender
from a
full join b
on a.id = b.id
/* if name, gender not in pk */
-- and a.name = b.name
-- and a.gender = b.gender
;
In this case all duplicated "ID"s will be removed. And first not null value of "name", "gender" columns will be returned becouse of coalesce function.
You can even use greatest, least and ets, instead of coalesce..
p.s. Be careful if you don't have PK on table!

SQL statement to conditionally selecting records based on the previous record

I have 2 tables as below
Table 1 : Animal (ID is a primary key)
ID |Animal
----------
1 |Dog
2 |Cat
3 |Fish
4 |Bird
5 |Elephant
Table 2: Pet (ID here is foreign keys to the Animal table)
ID | Animal | Name
----------
1 | Dog | Annie
1 | Dog | Buckie
2 | Cat | Conner
2 | Cat | Kitten
3 | Fish| Lala
I want to write a SQL statement to append a row with "Fish" right after wherever a specific pet "Dog" appears without breaking the order.
Expected result should be:
ID | Animal | Name
----------
1 | Dog | Annie
3 | Fish| NULL
1 | Dog | Buckie
3 | Fish| NULL
2 | Cat | Conner
2 | Cat | Kitten
3 | Fish| Lala
I'm not too sure about Oracle11g but I think it has ROW_NUMBER.
You could add a row number to the original table,
and then union a fish table with corresponding row numbers.
For example
WITH Tablex AS (
SELECT ROW_NUMBER() OVER(ORDER BY ID, Name) AS ref_id, *
FROM your_table
)
SELECT ID, Animal, Name
FROM (SELECT *
FROM Tablex
UNION ALL
SELECT *
FROM
(SELECT ref_id, 3 AS ID, 'Fish' AS Animal, NULL AS Name
FROM TableX
WHERE Animal = 'Dog'
) x
) X
ORDER BY ref_id, id
As commented above, the order of rows depends only on the ORDER BY clause and the order may not be actually incorporated in the table (if you put it INTO something).
Try this
select tn.Id
, case when tt.rn = 0 then tn.Animal else 'Fish' end Animal
, case when tt.rn = 0 then tn.Name else NULL end Name
, tn.rn+tt.rn rn
from (
select ID, Animal, Name, 2 * row_number() over (order by id, name) as rn
from pet
) tn
join (
select 0 rn from dual union
select 1 from dual
) tt on tt.rn <= case Animal when 'Dog' then 1 else 0 end
order by tn.rn+tt.rn;
with Q as (
select ID, Animal, Name,
row_number() over (order by id, name) rnum
from Pet
)
select ID, Animal, Name, rnum
from Q
union all
select 3, 'Fish', NULL, rnum+0.5
from Q
where ID=1 and name in('Annie','Buckie')
order by rnum

Query for missing elements

I have a table with the following structure:
timestamp | name | value
0 | john | 5
1 | NULL | 3
8 | NULL | 12
12 | john | 3
33 | NULL | 4
54 | pete | 1
180 | NULL | 4
400 | john | 3
401 | NULL | 4
592 | anna | 2
Now what I am looking for is a query that will give me the sum of the values for each name, and treats the nulls in between (orderd by the timestamp) as the first non-null name down the list, as if the table were as follows:
timestamp | name | value
0 | john | 5
1 | john | 3
8 | john | 12
12 | john | 3
33 | pete | 4
54 | pete | 1
180 | john | 4
400 | john | 3
401 | anna | 4
592 | anna | 2
and I would query SUM(value), name from this table group by name. I have thought and tried, but I can't come up with a proper solution. I have looked at recursive common table expressions, and think the answer may lie in there, but I haven't been able to properly understand those.
These tables are just examples, and I don't know the timestamp values in advance.
Could someone give me a hand? Help would be very much appreciated.
With Inputs As
(
Select 0 As [timestamp], 'john' As Name, 5 As value
Union All Select 1, NULL, 3
Union All Select 8, NULL, 12
Union All Select 12, 'john', 3
Union All Select 33, NULL, 4
Union All Select 54, 'pete', 1
Union All Select 180, NULL, 4
Union All Select 400, 'john', 3
Union All Select 401, NULL, 4
Union All Select 592, 'anna', 2
)
, NamedInputs As
(
Select I.timestamp
, Coalesce (I.Name
, (
Select I3.Name
From Inputs As I3
Where I3.timestamp = (
Select Max(I2.timestamp)
From Inputs As I2
Where I2.timestamp < I.timestamp
And I2.Name Is not Null
)
)) As name
, I.value
From Inputs As I
)
Select NI.name, Sum(NI.Value) As Total
From NamedInputs As NI
Group By NI.name
Btw, what would be orders of magnitude faster than any query would be to first correct the data. I.e., update the name column to have the proper value, make it non-nullable and then run a simple Group By to get your totals.
Additional Solution
Select Coalesce(I.Name, I2.Name), Sum(I.value) As Total
From Inputs As I
Left Join (
Select I1.timestamp, MAX(I2.Timestamp) As LastNameTimestamp
From Inputs As I1
Left Join Inputs As I2
On I2.timestamp < I1.timestamp
And I2.Name Is Not Null
Group By I1.timestamp
) As Z
On Z.timestamp = I.timestamp
Left Join Inputs As I2
On I2.timestamp = Z.LastNameTimestamp
Group By Coalesce(I.Name, I2.Name)
You don't need CTE, just a simple subquery.
select t.timestamp, ISNULL(t.name, (
select top(1) i.name
from inputs i
where i.timestamp < t.timestamp
and i.name is not null
order by i.timestamp desc
)), t.value
from inputs t
And summing from here
select name, SUM(value) as totalValue
from
(
select t.timestamp, ISNULL(t.name, (
select top(1) i.name
from inputs i
where i.timestamp < t.timestamp
and i.name is not null
order by i.timestamp desc
)) as name, t.value
from inputs t
) N
group by name
I hope I'm not going to be embarassed by offering you this little recursive CTE query of mine as a solution to your problem.
;WITH
numbered_table AS (
SELECT
timestamp, name, value,
rownum = ROW_NUMBER() OVER (ORDER BY timestamp)
FROM your_table
),
filled_table AS (
SELECT
timestamp,
name,
value
FROM numbered_table
WHERE rownum = 1
UNION ALL
SELECT
nt.timestamp,
name = ISNULL(nt.name, ft.name),
nt.value
FROM numbered_table nt
INNER JOIN filled_table ft ON nt.rownum = ft.rownum + 1
)
SELECT *
FROM filled_table
/* or go ahead aggregating instead */