Combine multiple counts with group by including zeros - sql

I have 3 tables: Equipment, Sections and Users and i want to combine 3 tables grouped by section and show total equipment per section and total users per section including zeros
Example equipment table:
ID Device Name SectionID Device Type
1 Holly's computer 1 PC
2 John's mobile 2 mobile
3 Maggie's printer 3 printer
4 Jonathan's scanner 3 scanner
5 George's scanner 2 scanner
6 Dugans handheld 5 scanner
7 Main printer 5 printer
Example sections table:
ID Section
1 Finance
2 HR
3 IT
4 Manager
5 Storage
Example users table
ID User SectionID
1 John 3
2 George 2
3 Amanda 2
4 Andy 4
5 Katherine 1
I tried this:
SELECT
b.section AS GROUPED_SECTION,
COUNT(distinct a.sectionid) AS TOTAL_EQUIPMENT,
COUNT(distinct c.sectionid) AS TOTAL_USERS
FROM Equipment a
LEFT JOIN Section b ON a.sectionid=b.id
LEFT JOIN Users c on a.sectionid=c.sectionid
GROUP BY b.description
ORDER BY b.description
but something is not working correctly
I want to create a query that will have the following result:
SECTION TOTAL_EQUIPMENT TOTAL_USERS
------- --------------- ------------
Finance 1 1
IT 2 1
HR 2 2
Manager 0 1
Storage 2 0
-1st column presents distinct sections from Equipment table
-2nd column presents total equipment per section according to Equipment table
-3rd column presents total users under that section according to Users table

Using UNION ALL for SectionID on equipment and users tables. and make a grp column to split two different tables.
then do OUTER JOIN base on sections table, final use aggregate function get count.
Query 1:
SELECT s.Section,
COUNT(CASE WHEN grp = 1 THEN 1 END) TOTAL_EQUIPMENT,
COUNT(CASE WHEN grp = 2 THEN 1 END) TOTAL_USERS
FROM sections s
LEFT JOIN (
select SectionID,1 grp
from equipment
UNION ALL
select SectionID,2 grp
from users
) t1 on t1.SectionID = s.ID
GROUP BY s.Section
Results:
| Section | TOTAL_EQUIPMENT | TOTAL_USERS |
|---------|-----------------|-------------|
| Finance | 1 | 1 |
| HR | 2 | 2 |
| IT | 2 | 1 |
| Manager | 0 | 1 |
| Storage | 2 | 0 |

The query is straightforward with CTE, count each of the two child tables, then join them to parent table (section)
Live test: http://sqlfiddle.com/#!18/f9f99/4
with eq as
(
select sectionid, count(*) as total_equipment
from equipment
group by sectionid
)
, uq as
(
select sectionid, count(*) as total_users
from users
group by sectionid
)
select s.id, s.section,
total_equipment = isnull(eq.total_equipment, 0),
total_users = isnull(uq.total_users, 0)
from sections s
left join eq on s.id = eq.sectionid
left join uq on s.id = uq.sectionid
order by s.section
Output:
| id | section | total_equipment | total_users |
|----|---------|-----------------|-------------|
| 1 | Finance | 1 | 1 |
| 2 | HR | 2 | 2 |
| 3 | IT | 2 | 1 |
| 4 | Manager | 0 | 1 |
| 5 | Storage | 2 | 0 |

Related

TSQL - Picking up first match from a group of rows

I have a simple scenario wherein, a table stores data about which card(s) a users uses and if those cards are registered (exist) in the system. I've applied ROW_NUMBER to group them too
SELECT User, CardId, CardExists, ROW_NUMBER() OVER (PARTITION BY User) AS RowNum From dbo.CardsInfo
User | CardID | CardExists | RowNum
-------------------------------------
A | 1 | 0 | 1
A | 2 | 1 | 2
A | 3 | 1 | 3
---------------------------------
B | 4 | 0 | 1
B | 5 | 0 | 2
B | 6 | 0 | 3
B | 7 | 0 | 4
---------------------------------
C | 8 | 1 | 1
C | 9 | 0 | 2
C | 10 | 1 | 3
Now in the above, I need to filter out User cards based on the two rules below
If in the cards registered with a user, multiple cards exist in the system, then take first one. So, for user A, CardID 2 will be returned and for User C it'll return CardID = 8
Othwerwise, if no card is existing (registered) for the user in the system, then just take the first one. So, for user B, it should return CardID = 4
Thus, final returned set should be -
User | CardID | CardExists | RowNum
-------------------------------------
A | 2 | 1 | 2
---------------------------------
B | 4 | 0 | 1
---------------------------------
C | 8 | 1 | 1
How can I do this filteration in SQL?
Thanks
You can use:
SELECT ci.*
FROM (SELECT User, CardId, CardExists,
ROW_NUMBER() OVER (PARTITION BY User ORDER BY CardExists DESC, CardId) AS RowNum
FROM dbo.CardsInfo ci
) ci
WHERE seqnum = 1;
You can also do this with aggregation:
select user,
max(cardexists) as cardexists,
coalesce(min(case when cardexists = 1 then cardid end),
min(card(cardid)
) as cardid
from cardsinfo
group by user;
Or, if you have a separate users table:
select ci.*
from users u cross apply
(select top (1) ci.*
from cardinfo ci
where ci.user = u.user
order by ci.cardexists desc, cardid asc
) ci

Group By with MAX value from another column

Table FieldStudies is :
ID Name
---|-----------------------|
1 | Industrial Engineering|
2 | Civil Engineering |
3 | Architecture |
4 | Chemistry |
And table Eductionals is :
ID UserID Degree FieldStudy_ID
---|------|--------|------------|
1 | 100 | 3 | 4 |
2 | 101 | 2 | 2 |
3 | 101 | 3 | 2 |
4 | 101 | 4 | 3 |
5 | 103 | 3 | 4 |
6 | 103 | 4 | 2 |
I want to find the number of students in each FieldStudies , provided that the highest Degree is considered.
Output desired:
ID Name Count
---|-----------------------|--------|
1 | Industrial Engineering| 0 |
2 | Civil Engineering | 0 |
3 | Architecture | 1 |
4 | Chemistry | 2 |
I have tried:
select Temptable2.* , count(*) As CountField from
(select fs.*
from FieldStudies fs
left outer join
(select e.UserID , Max(e.Degree) As ID_Degree , e.FieldStudy_ID
from Eductionals e
group by e.UserID) Temptable
ON fs.ID = Temptable.FieldStudy_ID) Temptable2
group by Temptable2.ID
But I get the following error :
Column 'Eductionals.FieldStudy_ID' is invalid in the select list
because it is not contained in either an aggregate function or the
GROUP BY clause.
If I understand correctly, you want only the highest degree for each person. If so, you can use row_number() to whittle down the multiple rows for a given person and the rest is aggregation and join:
select fs.id, fs.Name, count(e.id)
from fieldstudies fs left join
(select e.*,
row_number() over (partition by userid order by degree desc) as seqnum
from educationals e
) e
on e.FieldStudy_ID = fs.id and seqnum = 1
group by fs.id, fs.Name
order by fs.id;

How to count records using an l-tree

I have two tables, tickets and categories. The categories table has 3 columns of interest: id, name and path. The data looks like this:
id | Name | Path
------------------
1 | ABC | 1
2 | DEF | 1.2
3 | GHI | 1.2.3
4 | JKL | 4
5 | MNO | 4.5
6 | PQR | 4.5.6
9 | STU | 4.5.9
Note that the path column is an l-tree. What this is meant to represent is that the category with id=2 is a subcategory of id=1 and that id=3 is a subcategory of id=2.
In my tickets table, there's a column called category_id which refers to the id column in my categories table. Each ticket can have up to one category assigned to it (category_id may be null).
I'm trying to count all the tickets for each category.
Suppose my tickets table looks like this:
ticket_id | ticket_title | category_id
1 | A | 1
2 | B | 2
3 | C | 3
4 | D | 5
5 | F | 5
6 | G | 6
7 | H | 9
I would like to output:
category_id | count
1 | 3
2 | 2
3 | 1
4 | 4
5 | 4
6 | 1
9 | 1
I've found that I can get all of the tickets which belong to a given category with the following query: select * from tickets where category_id in (select id from categories where path ~ '*.1.*'); (although now that I'm writing this question I'm not convinced this is correct).
I've also attempted to perform the ticket-count-by-category problem and I came up with:
SELECT
categories.id as cid,
COUNT(*) as tickets_count
FROM tickets
LEFT JOIN categories ON tickets.category_id = categories.id
GROUP BY cid;
which outputs the following:
c_id | count
1 | 1
2 | 1
3 | 1
5 | 2
6 | 1
9 | 1
I'm not very good at SQL. Is it possible to achieve what I want?
Try this:
WITH tickets_per_path AS (
SELECT
c.path AS path,
count(*) AS count
FROM tickets t INNER JOIN categories c ON (t.category_id = c.id)
GROUP BY c.path)
SELECT
c.id,
sum(tickets_per_path.count) AS count
FROM categories c LEFT JOIN tickets_per_path ON (c.path #> tickets_per_path.path)
GROUP BY c.id
ORDER BY c.id;
Which yields the following result:
id| count
1 | 3
2 | 2
3 | 1
4 | 4
5 | 4
6 | 1
9 | 1
It roughly works like this:
the WITH clause computes the number of tickets per path (without
including the count of tickets of descendent paths).
the second select clause joins the categories table with the precomputed tickets_per_path view, but instead of an equi-join on path, it
joins by testing whether a record in the left table (categories) is
an ancestor of the right side table (using #> operator). Then it
groups by category id and sums up the ticket counts by category
including the descendant counts.
You are close, but you need a more general join:
SELECT c.id as cid, COUNT(*) as tickets_count
FROM categories c LEFT JOIN
tickets t
ON t.category_id || '.' LIKE c.id || '.%'
GROUP BY c.id;
The '.' in the comparison is just so 1.100 doesn't match 1.1.

Find duplicate combinations

I need a query to find duplicate combinations in these tables:
AttributeValue:
id | name
------------------
1 | green
2 | blue
3 | red
4 | 100x200
5 | 150x200
Product:
id | name
----------------
1 | Produkt A
ProductAttribute:
id | id_product | price
--------------------------
1 | 1 | 100
2 | 1 | 200
3 | 1 | 100
4 | 1 | 200
5 | 1 | 100
6 | 1 | 200
7 | 1 | 100 -- duplicate combination
8 | 1 | 100 -- duplicate combination
ProductAttributeCombinations:
id_product_attribute | id_attribute
-------------------------------------
1 | 1
1 | 4
2 | 1
2 | 5
3 | 2
3 | 4
4 | 2
4 | 5
5 | 3
5 | 4
6 | 3
6 | 5
7 | 1
7 | 4
8 | 1
8 | 5
I need SQL that creates result like:
id_product | duplicate_attributes
----------------------------------
1 | {7,8}
If I understand correct, 7 is a duplicate of 1 and 8 is a duplicate of 2. As phrased, your question is a bit confusing, because 7 and 8 are not related to each other and the only table of interest is ProductAttributeCombinations.
If this is the case, then one method is to use string aggregation
with combos as (
select id_product_attribute,
string_agg(id_attribute::text, ',' order by id_attribute) as combo
from ProductAttributeCombinations pac
group by id_product_attribute
)
select *
from combos c
where exists (select 1
from combos c2
where c2.id_product_attribute > c.id_product_attribute and
c2.combo = c.combo
);
Your question leaves some room for interpretation. Here is my educated guess:
For each product, return an array of all instances with the same set of attributes as any other instance of the same product with smaller ID.
WITH combo AS (
SELECT id_product, id, array_agg(id_attribute) AS attributes
FROM (
SELECT pa.id_product, pa.id, pac.id_attribute
FROM ProductAttribute pa
JOIN PoductAttributeCombinations pac ON pac.id_product_attribute = pa.id
ORDER BY pa.id_product, pa.id, pac.id_attribute
) sub
GROUP BY 1, 2
)
SELECT id_product, array_agg(id) AS duplicate_attributes
FROM combo c
WHERE EXISTS (
SELECT 1
FROM combo
WHERE id_product = c.id_product
AND attributes = c.attributes
AND id < c.id
)
GROUP BY 1;
Sorting can be inlined into the aggregate function so we don't need a subquery for the sort (like #Gordon already provided). This is shorter, but also typically slower:
WITH combo AS (
SELECT pa.id_product, pa.id
, array_agg(pac.id_attribute ORDER BY pac.id_attribute) AS attributes
FROM ProductAttribute pa
JOIN PoductAttributeCombinations pac ON pac.id_product_attribute = pa.id
GROUP BY 1, 2
)
SELECT ...
This only returns products with duplicate instances.
SQL Fiddle.
Your table names are rather misleading / contradict the rest of your question. Your sample data is not very clear either, only featuring a single product. I assume there are many in your table.
It's also unclear whether you are using double-quoted table names preserving CaMeL-case spelling. I assume: no.

How to GROUP BY into separate columns

I have 3 tables:
The first one contains information about persons. The relevant column is the personID.
The second contains exercises a person can do. There are for example 3 exercises with an exID.
The third contains the points a person (personID) reached in an exercise (exID). So each row here stands for an examination a person has taken. But not everyone need to have taken every exam.
What I would like to have is a result with the columns personID, exam_one, exam_two, exam_three, ...(ongoing, depending on how many exams there are). And each row of the result should contain the personID and the points from the respective exam.
For exams not taken there should be NULL or something.
Example for table persons:
personID | Name | ...
-------------------
1 | Max |
2 | Peter |
Example for exercises table:
exID | exName | maxPoints | ...
-------------------------------
1 | exam1 | 20
2 | exam2 | 25
3 | exam3 | 20
Example for points table:
personID (fkey) | exID (fkey) | points
----------------------------------------
1 | 1 | 12.5
1 | 3 | 10
2 | 1 | 5
2 | 2 | 8.5
2 | 3 | 10
Wished result:
personId | exam1 | exam2 | exam3
------------------------------------
1 | 12.5 | NULL | 10
2 | 5 | 8.5 | 10
Is there a way to do this? I use PostgreSQL
You can use something like the following:
select p.personId,
sum(case when e.exname = 'exam1' then t.points end) Exam1,
sum(case when e.exname = 'exam2' then t.points end) Exam2,
sum(case when e.exname = 'exam3' then t.points end) Exam3
from persons p
left join points t
on p.personID = t.personID
left join exercises e
on t.exid = e.exid
group by p.personid
See SQL Fiddle with Demo