Query to exclude negating value-pairs - sql

Create Table #Temp(Number Varchar(20), Category Varchar(20))
Insert Into #Temp
Select '123', '-A'
Union all
Select '123', 'A'
Union all
Select '123', 'A'
Union all
Select '123','B'
Union all
Select '123','-B'
Select * From #temp
result set
---------------------------
Number Category
123 -A
123 A
123 A
123 B
123 -B
123 C
123 -C
123 -C
---------------------------
From the above set of the data I need to query showing only one A when there are 2 -A and one A.
All i need is to have an output that cancels the -A and A where ever necessary, from the above example,
the query should return the below only
result set
---------------------------
Number Category
123 A
123 -C
---------------------------

This should do what you want:
select t.*
from (select t.*,
count(*) over (partition by replace(category, '-', ''), seqnum) as cnt_sc
from (select t.*,
row_number() over (partition by category order by category) as seqnum
from temp t
) t
) t
where cnt_sc = 1;
For a given category this enumerates the rows. It then counts the number for each enumeration, taking the "-" into account. It returns the rows that have only one enumeration -- they have no matches.
Note: This assumes that category has no hyphens except at the beginning.
EDIT:
If you know that there will be at most one such row, you can do:
select number,
(case when count(*) > sum(case when category like '-%' then 1 else 0)
then '-' + replace(category, '-', '')
else replace(category, '-', '')
end)
from t
group by number, replace(category, '-', '')
having count(*) <> 2 * sum(case when category like '-%' then 1 else 0 end)

Not the prettiest solution but perhaps someone else has a more elegant approach:
WITH cte as(
SELECT
SUM(CASE WHEN LEFT(Category, 1) = '-'
THEN -1
ELSE 1
END) as summed
, Right(Category,1) AS nuCat
FROM #Temp
GROUP BY Number, RIGHT(Category, 1)
)
SELECT CASE WHEN SUM(summed) > 0
THEN nuCat
ELSE '-' + nuCat
END AS DerivedCategory
FROM cte
GROUP BY nuCat
HAVING SUM(summed) <> 0
Using the CTE to turn the character strings into integers to SUM them. Then, when selecting from the CTE, the string is concatenated back with the integer "sign" from the summed values in the CTE.
The result:
DerivedCategory
---------------
A
-C

Related

Check whether an employee is present on three consecutive days

I have a table called tbl_A with the following schema:
After insert, I have the following data in tbl_A:
Now the question is how to write a query for the following scenario:
Put (1) in front of any employee who was present three days consecutively
Put (0) in front of employee who was not present three days consecutively
The output screen shoot:
I think we should use case statement, but I am not able to check three consecutive days from date. I hope I am helped in this
Thank you
select name, case when max(cons_days) >= 3 then 1 else 0 end as presence
from (
select name, count(*) as cons_days
from tbl_A, (values (0),(1),(2)) as a(dd)
group by name, adate + dd
)x
group by name
With a self-join on name and available = 'Y', we create an inner table with different combinations of dates for a given name and take a count of those entries in which the dates of the two instances of the table are less than 2 units apart i.e. for each value of a date adate, it will check for entries with its own value adate as well as adate + 1 and adate + 2. If all 3 entries are present, the count will be 3 and you will have a flag with value 1 for such names(this is done in the outer query). Try the below query:
SELECT Z.NAME,
CASE WHEN Z.CONSEQ_AVAIL >= 3 THEN 1 ELSE 0 END AS YOUR_FLAG
FROM
(
SELECT A.NAME,
SUM(CASE WHEN B.ADATE >= A.ADATE AND B.ADATE <= A.ADATE + 2 THEN 1 ELSE 0 END) AS CONSEQ_AVAIL
FROM
TABL_A A INNER JOIN TABL_A B
ON A.NAME = B.NAME AND A.AVAILABLE = 'Y' AND B.AVAILABLE = 'Y'
GROUP BY A.NAME
) Z;
Due to the complexity of the problem, I have not been able to test it out. If something is really wrong, please let me know and I will be happy to take down my answer.
--Below is My Approch
select Name,
Case WHen Max_Count>=3 Then 1 else 0 end as Presence
from
(
Select Name,MAx(Coun) as Max_Count
from
(
select Name, (count(*) over (partition by Name,Ref_Date)) as Coun from
(
select Name,adate + row_number() over (partition by Name order by Adate desc) as Ref_Date
from temp
where available='Y'
)
) group by Name
);
select name as employee , case when sum(diff) > =3 then 1 else 0 end as presence
from
(select id, name, Available,Adate, lead(Adate,1) over(order by name) as lead,
case when datediff(day, Adate,lead(Adate,1) over(order by name)) = 1 then 1 else 0 end as diff
from table_A
where Available = 'Y') A
group by name;

Calculation of occurrence of strings

I have a table with 3 columns, id, name and vote. They're populated with many registers. I need that return the register with the best balance of votes. The votes types are 'yes' and 'no'.
Yes -> Plus 1
No -> Minus 1
This column vote is a string column. I am using SQL SERVER.
Example:
It must return Ann for me
Use conditional Aggregation to tally the votes as Kannan suggests in his answer
If you really only want 1 record then you can do it like so:
SELECT TOP 1
name
,SUM(CASE WHEN vote = 'yes' THEN 1 ELSE -1 END) AS VoteTotal
FROM
#Table
GROUP BY
name
ORDER BY
VoteTotal DESC
This will not allow for ties but you can use this method which will rank the responses and give you results use RowNum to get only 1 result or RankNum to get ties.
;WITH cteVoteTotals AS (
SELECT
name
,SUM(CASE WHEN vote = 'yes' THEN 1 ELSE -1 END) AS VoteTotal
,ROW_NUMBER() OVER (PARTITION BY 1 ORDER BY SUM(CASE WHEN vote = 'yes' THEN 1 ELSE -1 END) DESC) as RowNum
,DENSE_RANK() OVER (PARTITION BY 1 ORDER BY SUM(CASE WHEN vote = 'yes' THEN 1 ELSE -1 END) DESC) as RankNum
FROM
#Table
GROUP BY
name
)
SELECT name, VoteTotal
FROM
cteVoteTotals
WHERE
RowNum = 1
--RankNum = 1 --if you want with ties use this line instead
Here is the test data used and in the future do NOT just put an image of your test data spend the 2 minutes to make a temp table or a table variable so that people you are asking for help do not have to!
DECLARE #Table AS TABLE (id INT, name VARCHAR(25), vote VARCHAR(4))
INSERT INTO #Table (id, name, vote)
VALUES (1, 'John','no'),(2, 'John','no'),(3, 'John','yes')
,(4, 'Ann','no'),(5, 'Ann','yes'),(6, 'Ann','yes')
,(9, 'Marie','no'),(8, 'Marie','no'),(7, 'Marie','yes')
,(10, 'Matt','no'),(11, 'Matt','yes'),(12, 'Matt','yes')
Use this code,
;with cte as (
select id, name, case when vote = 'yes' then 1 else -1 end as votenum from register
) select name, sum(votenum) from cte group by name
You can get max or minimum based out of this..
This one gives the 'yes' rate for each person:
SELECT Name, SUM(CASE WHEN Vote = 'Yes' THEN 1 ELSE 0 END)/COUNT(*) AS Rate
FROM My_Table
GROUP BY Name

How to take the total sum of all objects within an object?

I am trying to get the total sum of all parts in a container. The way I am doing now, sum(weight), will only grab the first weight of the first part in the container. I want to grab all part weights where the container number is the same. There are many different container numbers in the table. I want the statement to work with different container numbers, and only insert the value in the row of the first occurrence of the container number.
http://s33.postimg.org/3t63t83hr/sumweight.png
Each part has a weight in the above. I want to tally those weights for each container number and sum it up on the first row like shown.
,(case when mu.master_unit_no is null
then c.Gross_weight
when mu.master_unit_no is not null
then sum(c.Gross_weight)+mut.tare_weight
end)
as 'Weight in LBS'
Right now I have this query but it returns just the first part weight + the tare weight. I want to grab the sum of all the parts for the container.
/* I-Dashboards Shipping Report */
/* ROTW 11-21-2015 */
select
p.part_no AS 'Part_Number'
,p.name AS 'Description'
,c.serial_no as 'S#'
,c.quantity AS 'Qty'
,cp.customer_part_No as 'F_NUMBER'
--,cast(mut.length AS varchar) + 'X' + Cast(mut.width as varchar) + 'X' + Cast(mut.Height as varchar) as 'dim MU'
,(CASE when mut.length is null
then 0
else cast(mut.length as int) end) as 'M_LEN'
,(CASE when mut.width is null
then 0
else cast(mut.width as int) end) As 'M_WD'
,(CASE when mut.height is null
then 0
else cast(mut.Height as int) end) AS 'M_HT'
,cast(pct.cube_length AS INT) as 'S_LEN'
,cast(pct.cube_width AS INT) AS 'S_WD'
,cast(pct.cube_height AS INT) AS 'S_HT'
,mut.tare_Weight as 'M_Tare_lbs'
,c.Gross_weight as 'Net_Wt_lbs'
,mu.master_unit_no as 'M Number'
,g.Booking_No as 'Booking_HAWB_Num'
,concat(g.cargo_container_no, '-', g.dock_code) as 'Container_ID'
,g.outbound_scac_code AS 'Carrier'
,concat(cast(pct.cube_length as int), 'x', cast(pct.cube_width as int), 'x', cast(pct.cube_height as int)) as 'BOX_DIMS_INCHES'
,(case when row_number() over (partition by mu.master_unit_no order by mu.master_unit_no) = 1
then concat(cast(mut.length as int), 'x', cast(mut.width as int), 'x', cast(mut.Height as int))
when mu.master_unit_no is null
then ''
end)
as 'PALLET_DIMS_INCHES'
,(case when g.booking_container_type_key = 6 THEN
'DIRECT'
when g.booking_container_type_key = 5 THEN
'AIR'
else 'CEVA-Ocean'
end) as 'Shipment Type'
,CASE
--WHEN(ROW_NUMBER() OVER (PARTITION BY mu.master_unit_no ORDER BY mu.master_unit_no)) = 1
--then (select sum((pct.cube_length*0.0254)*(pct.cube_width*0.0254)* (pct.cube_height*0.0254))
--from part_v_container c where c.master_unit_key = mu.master_unit_key)
when mu.master_unit_no is null
then (pct.cube_length*0.0254)*(pct.cube_width*0.0254)* (pct.cube_height*0.0254)
end as 'CBM'
,select c.*, CASE
WHEN(ROW_NUMBER() OVER (PARTITION BY mu.master_unit_no ORDER BY mu.master_unit_no)) = 1
THEN **(**select SUM(c.Gross_weight)+mut.tare_weight
from part_v_container c where c.master_unit_no = mu.master_unit_no**)** END AS 'Total Weight'
from part_v_container c
I'm trying to take the total sum of all the parts gross weight in a m number + the tare weight for that m number and store is as total weight.
Like Siyual said, add tables to help better our understanding. Until then I believe I have most of what you want.
Your table probably looks something like...
part_id container_ id Weight
------- ------------- ------
1 a 5
2 a 5
3 b 99
4 a 3
5 c 99
And you probably want a result like (example using container_id = a)...
Weight
------
13
Try this...
SELECT SUM(Weight) FROM someTable WHERE container_id = someContainer
In the case of the result example I gave I would do...
SELECT SUM(Weight) FROM someTable WHERE container_id = 'a'
I am not fully sure on what you mean by your last part "only insert the value in the row of the first occurrence of the container number". Why would you want this specifically?
EDIT 1
The final result should not have multiple container_id though. I did the following...
My table...
SELECT t1.container_id, SUM(t1.weight) FROM table_1 t1 JOIN table_1 t2 ON t1.part_id = t2.part_id GROUP BY t1.container_id
Result was...
EDIT 2
It took me a while but I think I got it :)
Table:
Query:
SELECT t.*, CASE
WHEN(ROW_NUMBER() OVER (PARTITION BY t.Container ORDER BY t.Container)) = 1
THEN (SELECT SUM(t2.Weight) FROM table1 t2 WHERE t2.Container = t.Container)
ELSE 0 END AS 'Total Weight'
FROM table1 t GROUP BY t.Container, t.Part, t.Weight
Results:
EDIT 3
This was your original...
select c.*, CASE
WHEN(ROW_NUMBER() OVER (PARTITION BY mu.master_unit_no ORDER BY mu.master_unit_no)) = 1
THEN select SUM(c.Gross_weight)+mut.tare_weight
from part_v_container c where c.master_unit_no = mu.master_unit_no END AS 'Total Weight'
This is what I would change (surrounded by two asterix on both sides EX: ** A **)...
select c.*, CASE
WHEN(ROW_NUMBER() OVER (PARTITION BY mu.master_unit_no ORDER BY mu.master_unit_no)) = 1
THEN **(**select SUM(c.Gross_weight)+mut.tare_weight
from part_v_container c where c.master_unit_no = mu.master_unit_no**)** END AS 'Total Weight'
You need the parenthesis because the code doesn't know where the end belongs to otherwise. The parenthesis allows SQL to know that the end belongs to the case statement. I also am not sure where the mu. and mut. come from. It seems like they belong to a different table that you never reference here?
I am not sure if you added it but after 'Total Weight' you are missing
from someTable group by (all things that are in your select aka things that will be output need to be here...see my previous example for a better understanding)
If you want, on your original question you can post screen shots of exactly what your tables look like (or manually create it) so I can use the names you use accurately and make it more easily understandable by you :)
EDIT 4
/* I-Dashboards Shipping Report */
/* ROTW 11-21-2015 */
select
p.part_no AS 'Part_Number'
,p.name AS 'Description'
,c.serial_no as 'S#'
,c.quantity AS 'Qty'
,cp.customer_part_No as 'F_NUMBER'
--,cast(mut.length AS varchar) + 'X' + Cast(mut.width as varchar) + 'X' + Cast(mut.Height as varchar) as 'dim MU'
,(CASE when mut.length is null
then 0
else cast(mut.length as int) end) as 'M_LEN'
,(CASE when mut.width is null
then 0
else cast(mut.width as int) end) As 'M_WD'
,(CASE when mut.height is null
then 0
else cast(mut.Height as int) end) AS 'M_HT'
,cast(pct.cube_length AS INT) as 'S_LEN'
,cast(pct.cube_width AS INT) AS 'S_WD'
,cast(pct.cube_height AS INT) AS 'S_HT'
,mut.tare_Weight as 'M_Tare_lbs'
,c.Gross_weight as 'Net_Wt_lbs'
,mu.master_unit_no as 'M Number'
,g.Booking_No as 'Booking_HAWB_Num'
,concat(g.cargo_container_no, '-', g.dock_code) as 'Container_ID'
,g.outbound_scac_code AS 'Carrier'
,concat(cast(pct.cube_length as int), 'x', cast(pct.cube_width as int), 'x', cast(pct.cube_height as int)) as 'BOX_DIMS_INCHES'
,(case when row_number() over (partition by mu.master_unit_no order by mu.master_unit_no) = 1
then concat(cast(mut.length as int), 'x', cast(mut.width as int), 'x', cast(mut.Height as int))
when mu.master_unit_no is null
then ''
end)
as 'PALLET_DIMS_INCHES'
,(case when g.booking_container_type_key = 6 THEN
'DIRECT'
when g.booking_container_type_key = 5 THEN
'AIR'
else 'CEVA-Ocean'
end) as 'Shipment Type'
,(case when row_number() over (partition by mu.master_unit_no order by mu.master_unit_no) = 1
then (pct.cube_length*0.0254)*(pct.cube_width*0.0254)*(pct.cube_height*0.0254)
when mu.master_unit_no is null
then (pct.cube_length*0.0254)*(pct.cube_width*0.0254)* (pct.cube_height*0.0254)
end)
as 'CBM'
,CASE
WHEN(ROW_NUMBER() OVER (PARTITION BY mu.master_unit_no ORDER BY mu.master_unit_no)) = 1
THEN (SELECT SUM(c.Gross_weight) + mut.tare_weight
from part_v_container c where c.master_unit_no = mu.master_unit_no) END AS 'Total Weight'
from part_v_container c
So this should have fixed my part. I do have an extra comment though. You have all these different prefixs (p., c., mut., mu., g., pct.). Where do you reference all of these? I can see where you reference c (it is right after the final from). Even in my part you use mut. but I don't know how you reference it. For example, c is useable because of from part_v_container c. c represents part_v_container. You can look into joins to help you get the other tables in there. If you want you can edit your original question and add all your tables to it (whether they are actual or examples). I just need to know the different table names and column names. I don't care about the actual data. I wish I personally knew you because this would be much easier in real time xD
EDIT 5
Using this table...
I used this query...
;WITH mult AS (SELECT (m.length*0.0254)*(m.width*0.0254)*(m.height*0.0254) AS multiply, m.container FROM measurement m)
, sumMult AS (SELECT SUM((m.length*0.0254)*(m.width*0.0254)*(m.height*0.0254)) AS sumMultiply, m.container FROM measurement m GROUP BY m.container)
, combine AS (SELECT s.sumMultiply AS sumMultiply, m.multiply AS multiply, m.container FROM mult m JOIN sumMult s ON m.container = s.container)
SELECT c.container, CASE WHEN (ROW_NUMBER() OVER (PARTITION BY c.container ORDER BY c.container)) = 1
THEN (SELECT c.sumMultiply)
ELSE (SELECT c.multiply)
END AS 'Cubic Meters'
FROM combine c GROUP BY c.container, c.sumMultiply, c.multiply
It SUMS all of the volumes for all parts in a container and displays it only in the first row (first part). The rest of the parts have their volume.
I can't completely convert it for you. I trust, since you have done it successfully in my previous queries, that you can convert it properly. I tried to keep the names for the table and columns as bland and recognizable as much as I could. It appears to be working how you want it. Incase you don't know what the ;WITH mult.... is...you can think of it like a function. Put the entire with statement (that is, mult, sumMult, combine) before your gigantic query. You can see in my query that my ;WITH is comes first (above) my SELECT query that produces the actual results.

Grouping By different rows

I have returned rows which look like this:
2 - Eggs
3 - Bacon
4 - Bacon Smoked
I would like to group by '%Bacon%' so that my count is 2.
How can i do this is SQL?
I should see results like this:
Eggs - 1
Bacon - 2
How about the following (Demo):
SELECT 'Eggs' AS Category, COUNT(*) AS MyCount
FROM MyTable
WHERE MyField LIKE '%Eggs%'
UNION ALL
SELECT 'Bacon' AS Category, COUNT(*) AS MyCount
FROM MyTable
WHERE MyField LIKE '%Bacon%'
Not tested, but I think it should work
SELECT COUNT(*) as QTY, RS.FOOD_TYPE
FROM
(SELECT
Case patIndex ('%[ /-]%', LTrim (FOOD_TYPE))
When 0 Then LTrim (FOOD_TYPE)
Else substring (LTrim (FOOD_TYPE), 1, patIndex ('%[ /-]%', LTrim (FOOD_TYPE)) - 1)
End FOOD_TYPE
FROM YOUR_TABLE) RS
GROUP BY RS.FOOD_TYPE
Other solution:
SELECT
Eggs = SUM(CASE WHEN FoodColumn LIKE '%Eggs%' THEN 1 ELSE 0 END),
Bacon = SUM(CASE WHEN FoodColumn LIKE '%Bacon%' THEN 1 ELSE 0 END)
FROM Test
You can see demo here.
If you need to separate the result into two separate rows
SELECT *
FROM
(
SELECT
Eggs = SUM(CASE WHEN FoodColumn LIKE '%Eggs%' THEN 1 ELSE 0 END),
Bacon = SUM(CASE WHEN FoodColumn LIKE '%Bacon%' THEN 1 ELSE 0 END)
FROM Test
) AS Test
UNPIVOT
(
Quantity FOR Foods IN (Eggs, Bacon)
) AS Result
You can see demo here.
This is a very specific case.. can you provide more Data? Does this help in anyway?
with list (item) as (
select
item
from (
values
('Eggs'),
('Bacon'),
('Bacon Smoked')) list (item)
)
select
LEFT(item,
(case
when CHARINDEX(' ',item,1) = 0
then LEN(item)
else CHARINDEX(' ',item,1) end)
) filtered,
COUNT(*)
from list
group by
LEFT(item,
(case
when CHARINDEX(' ',item,1) = 0
then LEN(item)
else CHARINDEX(' ',item,1) end))
create table MyTable
(id int, FieldName varchar(50) )
insert into MyTable values (1, 'Eggs')
insert into MyTable values (2, 'Bacon')
insert into MyTable values (3, 'Bacon Smoked')
select count(FieldName), FieldName from (
select
case
when charindex('eggs', FieldName) > 0 then 'eggs'
when charindex('bacon', FieldName) > 0 then 'bacon'
end as FieldName
from MyTable) as myMyTablealias
group by FieldName
check it out

Looping in select query

I want to do something like this:
select id,
count(*) as total,
FOR temp IN SELECT DISTINCT somerow FROM mytable ORDER BY somerow LOOP
sum(case when somerow = temp then 1 else 0 end) temp,
END LOOP;
from mytable
group by id
order by id
I created working select:
select id,
count(*) as total,
sum(case when somerow = 'a' then 1 else 0 end) somerow_a,
sum(case when somerow = 'b' then 1 else 0 end) somerow_b,
sum(case when somerow = 'c' then 1 else 0 end) somerow_c,
sum(case when somerow = 'd' then 1 else 0 end) somerow_d,
sum(case when somerow = 'e' then 1 else 0 end) somerow_e,
sum(case when somerow = 'f' then 1 else 0 end) somerow_f,
sum(case when somerow = 'g' then 1 else 0 end) somerow_g,
sum(case when somerow = 'h' then 1 else 0 end) somerow_h,
sum(case when somerow = 'i' then 1 else 0 end) somerow_i,
sum(case when somerow = 'j' then 1 else 0 end) somerow_j,
sum(case when somerow = 'k' then 1 else 0 end) somerow_k
from mytable
group by id
order by id
this works, but it is 'static' - if some new value will be added to 'somerow' I will have to change sql manually to get all the values from somerow column, and that is why I'm wondering if it is possible to do something with for loop.
So what I want to get is this:
id somerow_a somerow_b ....
0 3 2 ....
1 2 10 ....
2 19 3 ....
. ... ...
. ... ...
. ... ...
So what I'd like to do is to count all the rows which has some specific letter in it and group it by id (this id isn't primary key, but it is repeating - for id there are about 80 different values possible).
http://sqlfiddle.com/#!15/18feb/2
Are arrays good for you? (SQL Fiddle)
select
id,
sum(totalcol) as total,
array_agg(somecol) as somecol,
array_agg(totalcol) as totalcol
from (
select id, somecol, count(*) as totalcol
from mytable
group by id, somecol
) s
group by id
;
id | total | somecol | totalcol
----+-------+---------+----------
1 | 6 | {b,a,c} | {2,1,3}
2 | 5 | {d,f} | {2,3}
In 9.2 it is possible to have a set of JSON objects (Fiddle)
select row_to_json(s)
from (
select
id,
sum(totalcol) as total,
array_agg(somecol) as somecol,
array_agg(totalcol) as totalcol
from (
select id, somecol, count(*) as totalcol
from mytable
group by id, somecol
) s
group by id
) s
;
row_to_json
---------------------------------------------------------------
{"id":1,"total":6,"somecol":["b","a","c"],"totalcol":[2,1,3]}
{"id":2,"total":5,"somecol":["d","f"],"totalcol":[2,3]}
In 9.3, with the addition of lateral, a single object (Fiddle)
select to_json(format('{%s}', (string_agg(j, ','))))
from (
select format('%s:%s', to_json(id), to_json(c)) as j
from
(
select
id,
sum(totalcol) as total_sum,
array_agg(somecol) as somecol_array,
array_agg(totalcol) as totalcol_array
from (
select id, somecol, count(*) as totalcol
from mytable
group by id, somecol
) s
group by id
) s
cross join lateral
(
select
total_sum as total,
somecol_array as somecol,
totalcol_array as totalcol
) c
) s
;
to_json
---------------------------------------------------------------------------------------------------------------------------------------
"{1:{\"total\":6,\"somecol\":[\"b\",\"a\",\"c\"],\"totalcol\":[2,1,3]},2:{\"total\":5,\"somecol\":[\"d\",\"f\"],\"totalcol\":[2,3]}}"
In 9.2 it is also possible to have a single object in a more convoluted way using subqueries in instead of lateral
SQL is very rigid about the return type. It demands to know what to return beforehand.
For a completely dynamic number of resulting values, you can only use arrays like #Clodoaldo posted. Effectively a static return type, you do not get individual columns for each value.
If you know the number of columns at call time ("semi-dynamic"), you can create a function taking (and returning) polymorphic parameters. Closely related answer with lots of details:
Dynamic alternative to pivot with CASE and GROUP BY
(You also find a related answer with arrays from #Clodoaldo there.)
Your remaining option is to use two round-trips to the server. The first to determine the the actual query with the actual return type. The second to execute the query based on the first call.
Else, you have to go with a static query. While doing that, I see two nicer options for what you have right now:
1. Simpler expression
select id
, count(*) AS total
, count(somecol = 'a' OR NULL) AS somerow_a
, count(somecol = 'b' OR NULL) AS somerow_b
, ...
from mytable
group by id
order by id;
How does it work?
Compute percents from SUM() in the same SELECT sql query
SQL Fiddle.
2. crosstab()
crosstab() is more complex at first, but written in C, optimized for the task and shorter for long lists. You need the additional module tablefunc installed. Read the basics here if you are not familiar:
PostgreSQL Crosstab Query
SELECT * FROM crosstab(
$$
SELECT id
, count(*) OVER (PARTITION BY id)::int AS total
, somecol
, count(*)::int AS ct -- casting to int, don't think you need bigint?
FROM mytable
GROUP BY 1,3
ORDER BY 1,3
$$
,
$$SELECT unnest('{a,b,c,d}'::text[])$$
) AS f (id int, total int, a int, b int, c int, d int);