SQL Aggregation with SUM, GROUP BY and JOIN (many-to-many) - mysql

Here's an example Table layout:
TABLE_A: TABLE_B: TABLE_A_B:
id | a | b | c id | name a_id | b_id
--------------------- --------- -----------
1 | true | X | A 1 | A 1 | 1
2 | true | Z | null 2 | B 1 | 2
3 | false | X | null 3 | C 2 | 2
4 | true | Y | Q 4 | 1
5 | false | null | null 4 | 2
5 | 1
Possible Values:
TABLE_A.a: true, false
TABLE_A.b: X, Y, Z
TABLE_A.c: A, B, C, ... basically arbitrary
TABLE_B.name: A, B, C, ... basically arbitrary
What I want to achieve:
SELECT all rows from TABLE_A
SUM(where a = true),
SUM(where a = false),
SUM(where b = 'X'),
SUM(where b = 'Y'),
SUM(where b = 'Z'),
SUM(where b IS NULL),
and also get the SUMs for all distinct TABLE_A.c values.
and also get the SUMs for all those TABLE_A_B relations.
The result for the example Table above should look like:
aTrue | aFalse | bX | bY | bZ | bNull | cA | cQ | cNull | nameA | nameB | nameC
-------------------------------------------------------------------------------
3 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | 3 | 0
What I've done so far:
SELECT
SUM(CASE WHEN a = true THEN 1 ELSE 0 END) AS aTrue,
SUM(CASE WHEN b = false THEN 1 ELSE 0 END) AS aFalse,
SUM(CASE WHEN b = 'X' THEN 1 ELSE 0 END) AS bX,
...
FROM TABLE_A
What's my problem?
Selecting column TABLE_A.a and TABLE_A.b is easy, because there's a fixed number of possible values.
But I can't figure out how to count the distinct values of TABLE_A.c. And basically the same problem for the JOINed TABLE_B, because the number of values within TABLE_B is unknown and can change over time.
Thanks for your help! :)
EDIT1: New (preferred) SQL result structure:
column | value | sum
----------------------------
TABLE_A.a | true | 3
TABLE_A.a | false | 2
TABLE_A.b | X | 2
TABLE_A.b | Y | 1
TABLE_A.b | Z | 1
TABLE_A.b | null | 1
TABLE_A.c | A | 1
TABLE_A.c | Q | 1
TABLE_A.c | null | 3
TABLE_B.name | A | 3
TABLE_B.name | B | 3
TABLE_B.name | C | 0

From your original request of rows as a simulated pivot. By doing a SUM( logical condition ) basically returns 1 if true, 0 if false. So, since the column "a" is true or false, simple sum of "a" or NOT "a" (for the false counts -- NOT FALSE = TRUE). Similarly, your "b" column, so b='X' = true counted as 1, else 0.
In other sql engines, you might see it as SUM( case/when ).
Now, since your table counts don't rely on each other, they can be separate SUM() into their own sub-alias query references (pqA and pqB for pre-queryA and pre-queryB respectively). Since no group by, they will each result in a single row. With no join will create a Cartesian, but since 1:1 ratio, will only return a single record of all columns you want.
SELECT
pqA.*, pqB.*
from
( SELECT
SUM( ta.a ) aTrue,
SUM( NOT ta.a ) aFalse,
SUM( ta.b = 'X' ) bX,
SUM( ta.b = 'Y' ) bY,
SUM( ta.b = 'Z' ) bZ,
SUM( ta.b is null ) bNULL,
SUM( ta.c = 'A' ) cA,
SUM( ta.c = 'Q' ) cQ,
SUM( ta.c is null ) cNULL,
COUNT( distinct ta.c ) DistC
from
table_a ta ) pqA,
( SELECT
SUM( b.Name = 'A' ) nameA,
SUM( b.Name = 'B' ) nameB,
SUM( b.Name = 'C' ) nameC
from
table_a_b t_ab
join table_b b
ON t_ab.b_id = b.id ) pqB
This option gives your second (preferred) output
SELECT
MAX( 'TABLE_A.a ' ) as Basis,
CASE when a then 'true' else 'false' end Value,
COUNT(*) finalCnt
from
TABLE_A
group by
a
UNION ALL
SELECT
MAX( 'TABLE_A.b ' ) as Basis,
b Value,
COUNT(*) finalCnt
from
TABLE_A
group by
b
UNION ALL
SELECT
MAX( 'TABLE_A.c ' ) as Basis,
c Value,
COUNT(*) finalCnt
from
TABLE_A
group by
c
UNION ALL
SELECT
MAX( 'TABLE_B.name ' ) as Basis,
b.Name Value,
COUNT(*) finalCnt
from
table_a_b t_ab
join table_b b
ON t_ab.b_id = b.id
group by
b.Name

I think You will need to build dynamic query as you don't know possible values for column C in table A. So you can write store procedure where you can get list of distinct value for Column C in one variable and by using "Do WHILE" you can construct your dynamic query.
Please let me know if you need more help in detail
Dynamic SQL

Related

MySQL: Return a row if all joint table row have the value true

I've two tables as following.
Table A
Column Id | Column Name
1 | Test 1
2 | Test 2
The relationship is
Table B
Column Id | Column Delivered | Column TableA_Id
1 | true | 1
2 | true | 1
3 | true | 1
4 | true | 1
5 | false | 2
6 | true | 2
7 | true | 2
What I want is to return for example for table A the record with id 1 which has a relationship with table B where all the values on table B column Delivered as true.
You can use group by and having:
select tablea_id
from b
group by tablea_id
having sum( delivered <> 'true' ) = 0;
This doesn't use table a. If you want all the columns there, you can also use;
select a.*
from a
where not exists (select 1
from b
where b.tablea_id = a.id and b.delivered <> 'true'
);
The one difference with this query is that it will also return rows in a that have no rows in b at all.
You can use join and group by with having as follows:
select a.column_id, a.column_name
from a join b on a.column_id = b.tablea_id
group by a.column_id, a.column_name
having count( b.delivered <> 'true' ) = 0;

sql query to compare values row wise?

i have a table which contains some data as an example:
+----------+-----+------+
| order_id | poi | povi |
+----------+-----+------+
| 1 | A | a |
| 1 | B | b |
| 1 | C | c |
| 2 | A | a |
| 2 | B | b |
| 2 | C | c |
| 3 | A | a |
| 3 | B | b |
| 4 | C | c |
| 5 | A | a |
| 5 | B | b |
| 6 | C | c |
| 7 | A | a |
| 8 | B | b |
| 9 | C | c |
+----------+-----+------+
i have 3 set of values of poi and povi like {A,a},{B,b},{C,c}
i want to get the order_id which contains all three of them, like in the above case the output should be.(order_id which have poi and povi as {A,a} and {B,b} and {C,c} but the problem is that they are diffrent rows)
+----------+
| order_id |
+----------+
| 1 |
| 2 |
+----------+
any idea?
So many times people just getting started ask similar questions to those already asked and answered, including this common scenario. However, not being able to apply know answers to your scenario doesn't help you wrap your head around what is asked, or how the query works in their own scenario... That said, lets look at yours.
You want all DISTINCT orders that have ALL of the following A/a, B/b, C/c entries. Multiple ways to resolve, but the most common is with a where / group by / having.
Start with something simple, looking for any order that has A/a
select
yt.Order_id
from
YourTable yt
where
( yt.poi = 'A' AND yt.poiv = 'a' )
and you would get order 1, 2, 3, 5 and 7. That is simple...
Now, add in your other criteria
select
yt.Order_id
from
YourTable yt
where
( yt.poi = 'A' AND yt.poiv = 'a' )
OR ( yt.poi = 'B' AND yt.poiv = 'b' )
OR ( yt.poi = 'C' AND yt.poiv = 'c' )
and this will give you all rows, but not what you want, but you should be able to see the where criteria is checking for both parts of POI / POIV with an OR between each possible combination. You obviously can not have one record that has a POI of both "A" and "B", that is why the "OR" between each paired ( AND ) criteria. But again, this gives ALL rows. But it is also qualifying only the pieces. So lets add one next step... a group by via the order, but HAVING clause expecting 3 records...
select
ytA.Order_id
from
YourTable ytA
where
( yt.poi = 'A' AND yt.poiv = 'a' )
OR ( yt.poi = 'B' AND yt.poiv = 'b' )
OR ( yt.poi = 'C' AND yt.poiv = 'c' )
group by
yt.Order_id
HAVING
count(*) = 3
The count(*) is to count how many records qualified the WHERE clause and will only return those records that had 3 entries.
Now, what if someone has multiple orders of A/a, A/a, B/b... This COULD give a false answer returned value, but please confirm these queries to meet your needs.
Although accepted, here is another way I would have written the query... somewhat similar to another post below. The premise of this version of the query is to utilize an index and qualify at least 1 record found before trying to find ALL. In this case, it first qualifies for those with an A/a. If an order does not have that, it does not care about looking for a B/b, C/c. If it DOES, then the join qualifies to the next levels too
select
ytA.Order_id
from
YourTable ytA
JOIN YourTable ytB
on ytA.Order_id = ytB.Order_id
AND ytB.poi = 'B'
AND ytB.poiv = 'b'
JOIN YourTable ytC
on ytB.Order_id = ytC.Order_id
AND ytC.poi = 'C'
AND ytC.poiv = 'c'
where
ytA.poi = 'A'
AND ytA.poiv = 'a'
find the "intersection" of lists, each of which contains one set
select id
from
(select id from mytable where poi = 'A' and povi= 'a') t1
inner join
(select id from mytable where poi = 'B' and povi= 'b') t2
using(id)
inner join
(select id from mytable where poi = 'C' and povi= 'c') t3
using(id)
demo

SQL query to get all records in a certain status within a foreign key

I have a fairly big table (10,000+ records) that looks more or less like this:
| id | name | contract_no | status |
|----|-------|-------------|--------|
| 1 | name1 | 1022 | A |
| 2 | name2 | 1856 | B |
| 3 | name3 | 1322 | C |
| 4 | name4 | 1322 | C |
| 5 | name5 | 1322 | D |
contract_no is a foreign key which of course can appear in several records and each record will have a status of either A, B, C, D or E.
What I want is to get a list of all the contract numbers, where ALL the records referencing that contract are in status C, D, E, or a mix of those, but if any of the records are in status A or B, omit that contract number.
Is it possible to do this using a SQL query? Or should I better export the data and try to run this analysis using another language like Python or R?
Post aggregate filtering should do the trick
SELECT contract_no FROM t
GROUP BY contract_no
HAVING SUM(status='A')=0
AND SUM(status='B')=0
You can use group by with having to get such contract numbers.
select contract_number
from yourtable
group by contract_number
having count(distinct case when status in ('C','D','E') then status end) >= 1
and count(case when status = 'A' then 1 end) = 0
and count(case when status = 'B' then 1 end) = 0
Not that elegant as the other two answers, but more expressive:
SELECT DISTINCT contract_no
FROM the_table t1
WHERE NOT EXISTS (
SELECT *
FROM the_table t2
WHERE t2.contract_no = t1.contract_no
AND t2.status IN ('A', 'B')
)
Or
SELECT DISTINCT contract_no
FROM the_table
WHERE contract_no NOT IN (
SELECT contract_no
FROM the_table
AND status IN ('A', 'B')
)

MySQL Compare between 2 Tables then add results

I have a table with the next structure
Table A:
| Id | 1 | 2 | 3 |
|-----|---------|----------|-----------|
| 1 | 00:05:00| 00:10:00 | (null) |
| 2 | 00:10:00| (null) | (null) |
Table B
| Id | col |Expected |
|-----|---------|----------|
| 1 | 1 | 00:06:00 |
| 2 | 2 | 00:12:00 |
| 3 | 3 | 00:22:00 |
I am trying to make a sum depending on the actual value on the rows of table A in comparison to the expected on table B
Select
Id, (Select ??????? From (Select TiempoStd from B)as Stime) as Time
From
A
Basically i want to make a comparison between the 2 tables to see which one is greater and add that to the next one.
I cant manage to understand how to call a specific value under my temp table Stime.
i am not that familiar with SQL so thats why i cant get this, the logic is something like this. in where the question marks are on the Query
ADDTIME(IF(A.1>(Stime.Expected where col = 1),A.1,(Stime.Expected where col = 1)),
ADDTIME(IF(A.2>(Stime.Expected where col = 2),A.2,(Stime.Expected where col = 2)),
IF(A.3>(Stime.Expected where col = 3),A.3,(Stime.Expected where col = 3))
Stime.Expected where col = 3 is a bad syntax right? but i hope you get the point of the logic im trying to make here.
so the output would be like this
| Id | Time |
|-----|---------|
| 1 | 00:40:00|
| 2 | 00:44:00|
Use a UNION to convert table A to a table with separate rows for each column, then join this with table B.
select *
FROM TableB AS b
JOIN (SELECT Id, 1 AS col, a.1 AS Time
FROM TableA as a
UNION
SELECT Id, 2 AS col, a.2 AS Time
FROM TableA AS a
UNION
SELECT Id, 3 AS col, a.3 AS Time
FROM TableA AS a) AS a
ON a.Id = b.Id AND a.col = b.col
This made the trick, i got help from some friends and got to it, gives the desired result.
SELECT
A.Id,
addtime(
addtime(
ifnull(Case when A.1 > E.Expected1 Then A.1 else E.expected1 end,'00:00:00') ,
ifnull(Case when A.2 > E.Expected2 Then A.2 else E.expected2 end,'00:00:00')
), ifnull(Case when A.3 > E.Expected3 Then A.3 else E.expected3 end,'00:00:00')
) as Time
From TableA A, (
SELECT
sum(case when col=1 Then Expected else 0 end) as Expected1,
sum(case when col=2 Then Expected else 0 end) as Expected2,
sum(case when col=3 Then Expected else 0 end) as Expected3
From TableB ) as E

MySQL - Counting two things with different conditions

I want to count two things under different conditions in one query.
SELECT COUNT(*) AS count FROM table_name WHERE name = ?
and
SELECT COUNT(*) as count FROM table_name WHERE address = ? AND port = ?
I need to have a count for rows that have a certain address and certain port, and a SEPARATE count for rows that have a certain name.
I'm aware that I could do
SELECT (COUNT*) as count FROM table_name WHERE (address = ? AND port = ?) OR name = ?
However that is a single count, and I need them to be separate so I can display a more accurate message to the user.
How might I go about doing this? Help would be appreciated!
What about simply:
SELECT
SUM(IF(name = ?, 1, 0)) AS name_count,
SUM(IF(address = ? AND port = ?, 1, 0)) AS addr_count
FROM
table_name
SELECT SUM(CASE WHEN Name = ? THEN 1 ELSE 0 END) as name_match
, SUM(CASE WHEN Address = ? AND Port = ? THEN 1 ELSE 0 END) as address_match
FROM table_name
WHERE (address = ? AND port = ?) OR name = ?
SELECT
COUNT( CASE WHEN n1 = 'J' THEN 1 END ) AS t1,
COUNT( CASE WHEN n2 = 'C' THEN 1 END ) AS t2,
COUNT( CASE WHEN n3 = 'K' THEN 1 END ) AS t3
FROM test
Using COUNT(CASE...), you can get the count of two-column from single table, even when conditions for both are different (eg: Get count of J from n1 column and count of C from n2 column, and so on..)
Table: test
+----+----+----+----+
| id | n1 | n2 | n3 |
|----+----+----+----+
| 1 | J | C | K |
|----+----+----+----+
| 1 | J | C | F |
|----+----+----+----+
| 1 | J | K | C |
|----+----+----+----+
| 1 | K | K | C |
|----+----+----+----+
Result:
+----+----+----+
| t1 | t2 | t3 |
|----+----+----+
| 3 | 2 | 1 |
|----+----+----+
Might be easiest just to do a Union:
SELECT COUNT(*) AS count FROM table_name WHERE name = ?
GROUP BY name
UNION ALL
SELECT COUNT(*) as count FROM table_name WHERE address = ? AND port = ?
GROUP BY address, port