postgresql count rows with special columns - sql

my table looks like this:
table1:
ident
A
B
C
D
1
2
1
2
3
3
1
2
1
5
4
4
5
4
1
3
6
3
2
7
3
8
1
9
1
Now i need something like a analysis from that table.
It should look like:
table2:
name
just_name
A
3
B
1
C
1
D
0
the column just_name count the columns from table1 where there are no other entry in the other columns exept the ident column.
in the real table there are more than 4 columns so i better not work with a where for every other column. :)
thx

If you are ok with just putting the column names in column list then below query can get you your desired result. Though it's possible to make those part dynamic but if you know your column names and it's not changing dynamically this will be better approach. Please let me know if you wanna have hat part dynamic olso.
Schema:
create table mytable1(ident int, A int, B int, C int, D int);
insert into mytable1 values(1,null,2,1,null);
insert into mytable1 values(2,3,null,null,null);
insert into mytable1 values(3,1,2,1,5);
insert into mytable1 values(4,null,4,null,null);
insert into mytable1 values(5,4,1,null,3);
insert into mytable1 values(6,null,3,2,null);
insert into mytable1 values(7,null,null,3,null);
insert into mytable1 values(8,1,null,null,null);
insert into mytable1 values(9,1,null,null,null);
Query:
with cte as (SELECT
unnest(array['A', 'B', 'C','D']) AS Columns,
unnest(array[A, B, C,D]) AS Values,
row_number()over(order by 1)rn
FROM mytable1),
cte2 as (
select rn,max(cte.columns)col,count(*) from cte
where values is not null
group by rn
having count(*)=1)
select distinct columns as name,coalesce(just_name,0) from cte left join (select col,count(rn) just_name from cte2
group by col)t on cte.columns=t.col
Output:
name
coalesce
A
3
C
1
D
0
B
1
db<>fiddle here

I would do this as columns:
select count(*) filter (where A is not null and B is null and C is null and d is null),
count(*) filter (where A is null and B is not null and C is null and d is null),
count(*) filter (where A is null and B is null and C is not null and d is null),
count(*) filter (where A is null and B is null and C is null and d is not null)
from t;
You could also express this as:
select c.colname, count(*) filter (where c.num_vals = 1)
from t cross join lateral
(select colname, count(colval) over () as num_vals
from (values ('a', t.a), ('b', t.b), ('c', t.c), ('d', t.d)) v(colname, colval)
group by colname
) c
group by c.colname;
This returns the values in separate rows. And it is a bit easier to generalize.

Related

Distribute a header table value to another table several rows with the same id

I need help with SQL Server on how to distribute a header table value to another table several rows with the same id : -
Table A : -
Id | Value
---+--------
1 | 40
2 | 21
Table B : -
Id | Qty
---+-------
1 | 20
1 | 13
2 | 1
Result should be : -
Id | Value | Qty
--------------------
1 | 20 | 20
1 | 20 | 13
2 | 21 | 1
WITH TblA
AS (SELECT 1 AS id,
40 AS num
UNION
SELECT 2,
21),
TblB
AS (SELECT 1 AS id,
20 AS num
UNION
SELECT 1,
13
UNION
SELECT 2,
1)
SELECT *
FROM TblA a
INNER JOIN TblB b ON a.id = b.id;
This below logic will distribute the value equally to all rows.
SELECT *,
(SELECT Value FROM A WHERE A.id = B.id)/COUNT(*) OVER(PARTITION BY id) Value
FROM B
Output is-
Id Qty Value
1 13 20
1 20 20
2 1 21
But if you wants to distribute the same value for all rows, do this below-
SELECT *,
(SELECT Value FROM A WHERE A.id = B.id) Value
FROM B
OR
SELECT B.*,A.Value
FROM B
INNER JOIN A ON B.id= A.id
Output will be-
Id Qty Value
1 20 40
1 13 40
2 1 21
Substitute table variables #Header & #Detail with your tables and remove the insert statements. You could also do this with a sub query to your main query as proposed in some of the other answers but for larger tables it may become slow hence the table variable called #Counter to first gather the aggregated count based on the #Detail table variable.
declare #Header table
(
Id int identity(1, 1),
[Value] int
)
declare #Detail table
(
Id int identity(1, 1),
HeaderId int,
Qty int
)
declare #Counter table
(
HeaderId int,
[Count] int
)
insert into #Header values (40)
insert into #Header values (21)
insert into #Detail values (1, 20)
insert into #Detail values (1, 13)
insert into #Detail values (2, 1)
insert into #Counter
select d.HeaderId, count(d.HeaderId)
from #Detail d
group by d.HeaderId
select
h.Id,
h.[Value] / c.[Count] [Value],
d.Qty
from #Header h
join #Detail d
on h.Id = d.HeaderId
join #Counter c
on h.Id = c.HeaderId
Please try with this simple Join
SELECT ta.id,
ta.value / (SELECT ( Count(tmp.id) )
FROM tb AS tmp
WHERE tmp.id = ta.id
GROUP BY tmp.id) AS 'value',
tb.qty
FROM ta
INNER JOIN tb
ON ta.id = tb.id
Full example (check Image):
CREATE TABLE ta
(
id INT,
value INT
)
CREATE TABLE tb
(
id INT,
qty INT
)
INSERT INTO ta
VALUES (1,40),
(2,21)
SELECT *
FROM ta
INSERT INTO tb
VALUES (1,20),
(1,13),
(2,1)
SELECT *
FROM tb
SELECT *
FROM ta
SELECT *
FROM tb
SELECT ta.id,
ta.value / (SELECT ( Count(tmp.id) )
FROM tb AS tmp
WHERE tmp.id = ta.id
GROUP BY tmp.id) AS 'value',
tb.qty
FROM ta
INNER JOIN tb
ON ta.id = tb.id
Just use a window function to divide the value:
select tb.id, ta.value / count(*) over (partition by ta.id) as value, tb.qty
from ta join
tb
on ta.id = tb.id;
Note that because these are integers, you might want to avoid integer division by using:
select tb.id, ta.value * 1.0 / count(*) over (partition by ta.id) as value, tb.qty
Here is a db<>fiddle.

Subquery in select - non-grouped values in 'IN' clause

Assume the following simplified schema:
create table main_table
(
a number,
b number,
c number
);
create table other_table
(
c number,
d number
)
Now, what i want to achieve:
I have a query on main_table, that groups by a,b.
I need to use the "all values of c" in subquery in select clause to get some data from other tables.
I can't join to the other table unfortunately.
Pseudocode would be:
select mt.a,
mt.b,
(select /* some aggregated value */
from other_table ot
where ot.c in (all_values_of_c_within_group)
)
from main table mt
group by mt.a, mt.b
There are two ways i know it's possible to handle this:
Use join on other_table and then aggregate values from there - unfortunately i can't do it, because of how the real query is structured (3 nested views, 800 sloc, 30 values in group by - long story)
Use listagg and then 'delistagg' it with 'instr'. Pseudocode:
/*(...)*/
(select /* some_aggregated_value */
from other_table ot
where instr(',' || listagg(
to_char(mt.c), ',') within group (order by 1),
',' || ot.c) > 0
)
/*(...)*/
But that's just terrible code, and it automatically prevents using any potentially existing indexes on other_table.c.
Is there a syntax to properly get "all values of column within group?
It is unclear without some data and expected results what you are trying to achieve but I think you do what you want using collections:
SQL Fiddle
Oracle 11g R2 Schema Setup:
create table main_table( a, b, c ) AS
SELECT 1, 1, 1 FROM DUAL UNION ALL
SELECT 1, 1, 2 FROM DUAL UNION ALL
SELECT 1, 1, 3 FROM DUAL
/
create table other_table( c, d ) AS
SELECT 1, 4 FROM DUAL UNION ALL
SELECT 3, 6 FROM DUAL UNION ALL
SELECT 5, 8 FROM DUAL
/
CREATE TYPE number_table AS TABLE OF NUMBER
/
Query 1:
SELECT a,
b,
( SELECT LISTAGG( d, ',' ) WITHIN GROUP ( ORDER BY d )
FROM other_table
WHERE c MEMBER OF m.cs
) ds
FROM (
SELECT a,
b,
CAST( COLLECT( c ) AS number_table ) AS cs
FROM main_table
GROUP BY a, b
) m
Results:
| A | B | DS |
|---|---|-----|
| 1 | 1 | 4,6 |
Query 2: But it seems simpler to just use a LEFT OUTER JOIN:
SELECT a,
b,
LISTAGG( d, ',' ) WITHIN GROUP ( ORDER BY d ) ds
FROM main_table m
LEFT OUTER JOIN other_table o
ON ( m.c = o.c )
GROUP BY a, b
Results:
| A | B | DS |
|---|---|-----|
| 1 | 1 | 4,6 |
You may just be able to aggregate the subquery, e.g. with sum as the aggregate function:
select mt.a,
mt.b,
sum(
(select d
from other_table ot
where ot.c = mt.c)
) as sum_d
from main_table mt
group by mt.a, mt.b;
With some made-up data:
insert into main_table values (1, 2, 3);
insert into main_table values (1, 2, 4);
insert into main_table values (2, 3, 4);
insert into main_table values (2, 3, 5);
insert into main_table values (2, 3, 6);
insert into other_table values (3, 10);
insert into other_table values (4, 11);
insert into other_table values (5, 12);
insert into other_table values (6, 13);
that query gives:
A B SUM_D
---------- ---------- ----------
2 3 36
1 2 21
As you noted, with an extra row:
insert into main_table values (2, 3, 4);
that query counts a matching c's d value multiple times, so you get 47 instead of 36:
A B SUM_D
---------- ---------- ----------
2 3 47
1 2 21
You can add a distinct:
select mt.a,
mt.b,
sum(distinct
(select d
from other_table ot
where ot.c = mt.c)
) as sum_d
from main_table mt
group by mt.a, mt.b;
A B SUM_D
---------- ---------- ----------
1 2 21
2 3 36
This assumes that c, or at least the combination of c, d, is unique in other_table.
This should work, and should not impose the uniqueness requirements on other_table that Alex's answer does.
select mt.a,
mt.b,
(select sum(d) /* some aggregated value */
from other_table ot
where ot.c in ( SELECT mt2.c
FROM main_table mt2
WHERE mt2.a = mt.a AND mt2.b = mt.b
)
) agg
from main_table mt
group by mt.a, mt.b;
It has to go to main_table again for each group, but considering you already are accessing those records, we should be talking about extra logical I/O instead of extra physical I/O.
Using Alex Poole's test data (with the duplicate MAIN_TABLE row), I get this in 12c:
+---+---+-----+
| A | B | AGG |
+---+---+-----+
| 2 | 3 | 36 |
| 1 | 2 | 21 |
+---+---+-----+

Return results from a table match on exact number of rows

I have two tables A and B, that are in a many to many relationship in a third table. What A want to achieve is get the "repeating" A rows based on B. For example:
table A table B table A_B
---------- ---------- ----------
1 A 1 A
2 B 1 B
3 C 2 A
4 D 2 B
5 3 A
3 B
3 C
4 A
4 D
5 A
What I want is, when searching table A_B by lets say '1', to get only 2, although 3 has both A and B and 4 has A, same goes for 5 too, it matches A but only A so it should be ignored as well. I've tried some suggestions form similar questions with cross join but I had no luck. I am trying to achieve this with just selects and joins, without stored procedures or temporary tables. Any suggestions is welcomed, thank you.
Repeat all base table rows for EACH left join row match
I want my output to look like:
table A_B
----------
2 A
2 B
Or if possible it would be even better if it matches the A_id by which the search is being done
table A_B
----------
1 A
1 B
2 A
2 B
However, the B_id column is not as important so if it is only
table A_B
----------
2
or
table A_B
----------
1
2
is acceptable as well.
EDIT 1:
Until now this is what I've came up with, although a bit unclean but it gets the expected result
select
A_id
from
tableA_B
where
A_id in
(
select
A_id
from
tableA_B
group by
A_id
having
count (A_id) IN (
select
count (A_id)
from
tableA_B
where
A_id = 1
)
)
AND
B_id IN (
select
B_id
from
tableA_B
where
A_id = 1
)
group by
A_id
Basically process of elimination, step by step. It would be ideal if it took only one step.
EDIT 2:
I'm sorry I left out some important information, my B values can be repeated for instance
table A table B table A_B
---------- ---------- ----------
1 A 1 A
2 B 1 B
3 c 2 A
4 D 2 B
5 AB 3 A
6 3 B
3 C
4 A
4 D
5 A
6 AB
so using XML path may return incorrect values. Because in my case it will return 6 as well which is incorrect. I apologies for leaving out this information.
Other solution which use INTERSECT could be:
CREATE TABLE tableA_B (A_id INT, B_id VARCHAR(8))
GO
INSERT INTO tableA_B VALUES
(1,'A'),(1,'B'),(2,'A'),(2,'B'),(3,'A'),(3,'B'),(3,'C'),(4,'A'),(4,'D'),(5,'A')
GO
DECLARE #x INT = 1;
SELECT A_id FROM tableA_B ab1
LEFT JOIN (
SELECT B_id FROM tableA_B
WHERE A_id=#x
) ab2 ON ab1.B_id=ab2.B_id
GROUP BY ab1.A_id
HAVING COUNT(*)=(SELECT COUNT(*) FROM tableA_B WHERE A_id=#x)
INTERSECT
SELECT A_id FROM tableA_B ab1
JOIN (
SELECT B_id FROM tableA_B
WHERE A_id=#x
) ab2 ON ab1.B_id=ab2.B_id
GROUP BY ab1.A_id
HAVING COUNT(*)=(SELECT COUNT(*) FROM tableA_B WHERE A_id=#x)
DROP TABLE tableA_B
GO
Try this,
declare #A_B table(col int,col2 varchar(30))
insert into #A_B VALUES
(1 ,'A') ,(1 ,'B') ,(2 ,'A') ,(2 ,'B') ,(3 ,'A') ,(3 ,'B')
,(3 ,'C') ,(4 ,'A') ,(4 ,'D') ,(5 ,'A'),(6 ,'AB')
declare #i int=1
declare #007 char(1)='-'
;with CTE as
(
select col,col2
,(select #007+col2 from #A_B y
where col=x.col for xml path(''))ConcateCol
from #A_B x
--where col=#i
)
select col,col2
from cte c
where
exists(select * from cte c1
where col=#i and c.ConcateCol=c1.ConcateCol)
you can further maniplate to get whatever desire output
;With tableA(ID)
AS
(
Select 1 uNION ALL
Select 2 uNION ALL
Select 3 uNION ALL
Select 4
)
, tableB(VAL)
As
(
SELECT 'A' UNION ALL
SELECT 'B' UNION ALL
SELECT 'C' UNION ALL
SELECT 'D'
)
SELECT ID,VAL FROM
(
SELECT *,ROW_NUMBER()OVER(PARTITION BY ID ORDER BY ID)AS Seq FROM tableA
CROSS JOIN tableB
)Dt
WHERE ID In (SELECT Id From tableA where id in(1,2) ) AND Dt.Seq<3
OutPut
table A_B
----------
1 A
1 B
2 A
2 B

SQL Server: add a column and get other column names as its value

Here is a sample table
DECLARE #t TABLE(a INT,b INT,c INT);
INSERT #t VALUES(1,2,3),(9,8,7),(4,6,5);
SELECT *
, ( SELECT MAX(val)
FROM (VALUES (a)
, (b)
, (c)
) AS value(val)
) AS MaxVal
FROM #t;
Result:
A B C MAX
---------------
1 2 3 3
9 8 7 9
4 6 5 6
I would like to add a column Max_cols which will have other column names as its values. column names are respective to the values that column 'MAX' has in it ...(hope this makes sense).
The result should look like this.
A B C MAX Max_cols
--------------------------
1 2 3 3 C
9 8 7 9 A
4 6 5 6 B
You can't really do this dynamically (at least not with table variables); one solution would be a CASE expression that explicitly lists each potential column name. You haven't explained how you want to handle ties, though...
;WITH x AS (SELECT *,
(
SELECT MAX(val)
FROM (VALUES (a)
, (b)
, (c)
) AS value(val)
) AS MaxVal
FROM #t
) SELECT *, Max_cols = CASE MaxVal
WHEN a THEN 'A' WHEN b THEN 'B' WHEN c THEN 'C' END
FROM x;

SQL: Making a 'computation row'

I have a table that looks like this
TYPE | A | B | C | ... | Z
one | 4 | 4 | 4 | ... | 4
two | 3 | 2 | 2 | ... | 1
And I wanted to insert a row with a computation (row one minus row two):
TYPE | A | B | C | ... | Z
one | 4 | 4 | 4 | ... | 4
two | 3 | 2 | 2 | ... | 1
delta| 1 | 2 | 2 | ... | 3
I was thinking of a SQL command that looks like
(select A from table where type=one) - (select A from table where type=two)
Down side is, it's too long and I also have to do that for all the columns (A-Z) and that's quite a lot.
I'm sure there's a more elegant way of doing this.
PS:
The sequence of my code looks like this btw:
// I'm inserting the data from a RawTable to a TempTable
INSERT one
INSERT two
INSERT delta
INSERT three
INSERT four
INSERT delta
...
INSERT onehundredone
INSERT onehundredtwo
INSERT delta
I have added an ID column with identity to your temp table. You can use that to figure out what rows should be grouped.
create table YourTable
(
ID int identity primary key,
[TYPE] varchar(20),
A int,
B int,
C int
)
insert into YourTable ([TYPE], A, B, C)
select 'one', 4, 4, 4 union all
select 'two', 3, 2, 2 union all
select 'three', 7, 4, 4 union all
select 'four', 3, 2, 2 union all
select 'five', 8, 4, 4 union all
select 'six', 3, 2, 2
select T.[TYPE], T.A, T.B, T.C
from
(
select
T.ID,
T.[TYPE],
T.A,
T.B,
T.C
from YourTable as T
union all
select
T2.ID,
'delta' as [TYPE],
T1.A-T2.A as A,
T1.B-T2.B as B,
T1.C-T2.C as C
from YourTable as T1
inner join YourTable as T2
on T1.ID = T2.ID-1 and
T2.ID % 2 = 0
) as T
order by T.ID, case T.[TYPE] when 'delta' then 1 else 0 end
Result:
TYPE A B C
-------------------- ----------- ----------- -----------
one 4 4 4
two 3 2 2
delta 1 2 2
three 7 4 4
four 3 2 2
delta 4 2 2
five 8 4 4
six 3 2 2
delta 5 2 2
Sorting on column C from first row in group:
select T.[TYPE], T.A, T.B, T.C
from
(
select
T1.ID,
T1.[TYPE],
case T1.ID % 2 when 1 then T1.C else T2.C end as Sortorder,
T1.A,
T1.B,
T1.C
from YourTable as T1
left outer join YourTable as T2
on T1.ID = T2.ID+1
union all
select
T2.ID,
'delta' as [TYPE],
T1.C as Sortorder,
T1.A-T2.A as A,
T1.B-T2.B as B,
T1.C-T2.C as C
from YourTable as T1
inner join YourTable as T2
on T1.ID = T2.ID-1 and
T2.ID % 2 = 0
) as T
order by T.Sortorder, T.ID, case T.[TYPE] when 'delta' then 1 else 0 end
I'm not aware of any way to do this "easily" (i.e. without having to specify every column), I can't come up with any way to do it easily, so I'll go on the record as saying that it can't be done. Easily.
The non-easy way would be to build dynamic code--something that loops through the database metadata, builds a string containing the statement(s) to execute your desired routine column by column, and then execute that string. You really want to avoid this whenever possible.
One shortcut, if you just need to build a procedure or function that does this (i.e. build once run many), you could copy the list of columns into a spreadsheet (Excel), build out the highly-repetitive statements using forumlas that reference the column names, and then copying the results back. (This is much simpler to do than it is to explain.)
I have no idea why you're doing this, but the way I'd approach it is:
insert into table
select 'delta',
t1.a - t2.a,
t1.b - t2.b
.....
from table t1,
table t2
where t1.type = 'one'
and t2.type = 'two'
You would have to run this query immediately after inserting "one" and "two", then re-run it after inserting "three" and "four". Nasty nasty nasty.
If you can re-name the columns in some way, or create a numerical column, you could run it in a single query.
When you replace one for 1, two for 2, and so on, then maybe this sql could work:
INSERT INTO PodMays
SELECT
"Delta", A.A-B.A, A.B-B.B, A.C-B.C, A.D-B.D, A.E-B.E
FROM
(
SELECT TOP 1
*
FROM
(SELECT TOP 2 * FROM PodMays WHERE Type <> "Delta" ORDER BY Type DESC)
ORDER BY
Type ASC
) AS A,
(
SELECT TOP 1
*
FROM
(SELECT TOP 2 * FROM PodMays WHERE Type <> "Delta" ORDER BY Type DESC)
ORDER BY
Type DESC
) AS B