Querying data groups with total row before starting next group? - sql

I need to query some data in the below format in SQL Server:
Id Group Price
1 A 10
2 A 20
Sum 30
1 B 6
2 B 4
Sum 10
1 C 100
2 C 200
Sum 300
I was thinking to do it in the follwoing steps:
Query one group
In other query do sum
Use Union operator to combine this result set
Do step 1-3 for all groups and finally return all sub sets of data using union.
Is there a better way to do this ? May be using some out of box feature ? Please advise.
Edit:
As per suggestions and code sample I tried this code:
Select
Case
when id is null then 'SUM'
else CAST(id as Varchar(10)) end as ID,
Case when [group] is null then 'ALL' else CAST([group] as Varchar(50)) end as [group]
,Price from
(
SELECT Id, [Group],BGAApplicationID,Section, SUM(PrimaryTotalArea) AS price
FROM vwFacilityDetails
where bgaapplicationid=1102
GROUP BY Id, [Group],BGAApplicationID,Section WITH ROLLUP
) a
And Even this code as well:
Select Id, [Group],BGAApplicationID,Section, SUM(PrimaryTotalArea) AS price
From vwFacilityDetails
Where Not ([group] Is Null And id Is Null And BGAApplicationId is null and section is null) and BGAApplicationId=1102
Group By Id, [Group],BGAApplicationID,Section
With Rollup
In results it groups up the data but for every record it shows it 3 times (in both above codes) like:
2879 Existing Facilities Whole School 25.00
2879 Existing Facilities Whole School 25.00
2879 Existing Facilities Whole School 25.00
2879 ALL 25.00
I guess there is some issue in my query, please guide me here as well.
Thanks

SQL Server introduced GROUPING SETS which is what you should be looking to use.
SQL Fiddle
MS SQL Server 2008 Schema Setup:
Create Table vwFacilityDetails (
id int not null,
[group] char(1) not null,
PrimaryTotalArea int not null,
Section int,
bgaapplicationid int
);
Insert Into vwFacilityDetails (id, [group], Section,bgaapplicationid,PrimaryTotalArea) values
(1, 'A', 1,1102,2),
(1, 'A', 1,1102,1),
(1, 'A', 1,1102,7),
(2, 'A', 1,1102,20),
(1, 'B', 1,1102,6),
(2, 'B', 1,1102,4),
(1, 'C', 1,1102,100),
(2, 'C', 1,1102,200);
Query 1:
SELECT CASE WHEN Id is null then 'SUM'
ELSE Right(Id,10) end Id,
[Group],BGAApplicationID,Section,
SUM(PrimaryTotalArea) price
FROM vwFacilityDetails
where bgaapplicationid=1102
GROUP BY GROUPING SETS (
(Id,[Group],BGAApplicationID,Section),
([Group])
)
ORDER BY [GROUP],
ID;
Results:
| ID | GROUP | BGAAPPLICATIONID | SECTION | PRICE |
----------------------------------------------------
| 1 | A | 1102 | 1 | 10 |
| 2 | A | 1102 | 1 | 20 |
| SUM | A | (null) | (null) | 30 |
| 1 | B | 1102 | 1 | 6 |
| 2 | B | 1102 | 1 | 4 |
| SUM | B | (null) | (null) | 10 |
| 1 | C | 1102 | 1 | 100 |
| 2 | C | 1102 | 1 | 200 |
| SUM | C | (null) | (null) | 300 |

Select
id,
[Group],
SUM(price) AS price
From
Test
Group By
[group],
id
With
Rollup
http://sqlfiddle.com/#!3/080cd/8

Select Case when id is null then 'SUM' else CAST(id as Varchar(10)) end as ID
,Case when [group] is null then 'ALL' else CAST([group] as Varchar(10)) end as [group]
,Price from
(
SELECT id, [group], SUM(price) AS Price
FROM IG
GROUP BY [GROUP],ID WITH ROLLUP
) a

Related

Replace value in column based on another column

I have the following table:
+----+--------+------------+----------------------+
| ID | Name | To_Replace | Replaced |
+----+--------+------------+----------------------+
| 1 | Fruits | 1 | Fruits |
| 2 | Apple | 1-2 | Fruits-Apple |
| 3 | Citrus | 1-3 | Fruits-Citrus |
| 4 | Orange | 1-3-4 | Fruits-Citrus-Orange |
| 5 | Empire | 1-2-5 | Fruits-Apple-Empire |
| 6 | Fuji | 1-2-6 | Fruits-Apple-Fuji |
+----+--------+------------+----------------------+
How can I create the column Replaced ? I thought of creating 10 maximum columns (I know there are no more than 10 nested levels) and fetch the ID from every substring split by '-', and then concatenating them if not null into Replaced, but I think there is a simpler solution.
While what you ask for is technically feasible (probably using a recursive query or a tally), I will take a different stance and suggest that you fix your data model instead.
You should not be storing multiple values as a delimited list in a single database column. This defeats the purpose of a relational database, and makes simple things both unnecessarily complicated and inefficient.
Instead, you should have a separate table to store that data, which each replacement id on a separate row, and possibly a column that indicates the sequence of each element in the list.
For your sample data, this would look like:
id replace_id seq
1 1 1
2 1 1
2 2 2
3 1 1
3 3 2
4 1 1
4 3 2
4 4 3
5 1 1
5 2 2
5 5 3
6 1 1
6 2 2
6 6 3
Now you can efficiently generate the expected result with either a join, a subquery, or a lateral join. Assuming that your table is called mytable and that the mapping table is mymapping, the lateral join solution would be:
select t.*, r.*
from mytable t
outer apply (
select string_agg(t1.name) within group(order by m.seq) replaced
from mymapping m
inner join mytable t1 on t1.id = m.replace_id
where m.id = t.id
) x
You can try something like this:
DECLARE #Data TABLE ( ID INT, [Name] VARCHAR(10), To_Replace VARCHAR(10) );
INSERT INTO #Data ( ID, [Name], To_Replace ) VALUES
( 1, 'Fruits', '1' ),
( 2, 'Apple', '1-2' ),
( 3, 'Citrus', '1-3' ),
( 4, 'Orange', '1-3-4' ),
( 5, 'Empire', '1-2-5' ),
( 6, 'Fuji', '1-2-6' );
SELECT
*
FROM #Data AS d
OUTER APPLY (
SELECT STRING_AGG ( [Name], '-' ) AS Replaced FROM #Data WHERE ID IN (
SELECT CAST ( [value] AS INT ) FROM STRING_SPLIT ( d.To_Replace, '-' )
)
) List
ORDER BY ID;
Returns
+----+--------+------------+----------------------+
| ID | Name | To_Replace | Replaced |
+----+--------+------------+----------------------+
| 1 | Fruits | 1 | Fruits |
| 2 | Apple | 1-2 | Fruits-Apple |
| 3 | Citrus | 1-3 | Fruits-Citrus |
| 4 | Orange | 1-3-4 | Fruits-Citrus-Orange |
| 5 | Empire | 1-2-5 | Fruits-Apple-Empire |
| 6 | Fuji | 1-2-6 | Fruits-Apple-Fuji |
+----+--------+------------+----------------------+
UPDATE
Ensure the id list order is maintained when aggregating names.
DECLARE #Data TABLE ( ID INT, [Name] VARCHAR(10), To_Replace VARCHAR(10) );
INSERT INTO #Data ( ID, [Name], To_Replace ) VALUES
( 1, 'Fruits', '1' ),
( 2, 'Apple', '1-2' ),
( 3, 'Citrus', '1-3' ),
( 4, 'Orange', '1-3-4' ),
( 5, 'Empire', '1-2-5' ),
( 6, 'Fuji', '1-2-6' ),
( 7, 'Test', '6-2-7' );
SELECT
*
FROM #Data AS d
OUTER APPLY (
SELECT STRING_AGG ( [Name], '-' ) AS Replaced FROM (
SELECT TOP 100 PERCENT
Names.[Name]
FROM ( SELECT CAST ( '<ids><id>' + REPLACE ( d.To_Replace, '-', '</id><id>' ) + '</id></ids>' AS XML ) AS id_list ) AS xIds
CROSS APPLY (
SELECT
x.f.value('.', 'INT' ) AS name_id,
ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL ) ) AS row_id
FROM xIds.id_list.nodes('//ids/id') x(f)
) AS ids
INNER JOIN #Data AS Names ON Names.ID = ids.name_id
ORDER BY row_id
) AS x
) List
ORDER BY ID;
Returns
+----+--------+------------+----------------------+
| ID | Name | To_Replace | Replaced |
+----+--------+------------+----------------------+
| 1 | Fruits | 1 | Fruits |
| 2 | Apple | 1-2 | Fruits-Apple |
| 3 | Citrus | 1-3 | Fruits-Citrus |
| 4 | Orange | 1-3-4 | Fruits-Citrus-Orange |
| 5 | Empire | 1-2-5 | Fruits-Apple-Empire |
| 6 | Fuji | 1-2-6 | Fruits-Apple-Fuji |
| 7 | Test | 6-2-7 | Fuji-Apple-Test |
+----+--------+------------+----------------------+
I'm sure there's optimization that can be done here, but this solution seems to guarantee the list order is kept.

SQL blank rows between rows

I am trying to output a blank row after each row.
For example:
SELECT id,job,amount FROM table
+----+-----+--------+
| id | job | amount |
+----+-----+--------+
| 1 | 100 | 123 |
| 2 | 200 | 321 |
| 3 | 300 | 421 |
+----+-----+--------+
To the following:
+----+-----+--------+
| id | job | amount |
+----+-----+--------+
| 1 | 100 | 123 |
| | | |
| 2 | 200 | 321 |
| | | |
| 3 | 300 | 421 |
+----+-----+--------+
I know I can do similar things with a UNION like:
SELECT null AS id, null AS job, null AS amount
UNION
SELECT id,job,amount FROM table
Which would give me a blank row at the beginning, but for the life of me I can't figure out how to do it every second row. A nested SELECT/UNION? - Have tried but nothing seemed to work.
The DBMS is SQL Server 2016
This is an akward requirement, that would most probably better handled on application side. Here is, however, one way to do it:
select id, job, amount
from (
select id, job, amount, id order_by from mytable
union all
select null, null, null, id from mytable
) t
order by order_by, id desc
The trick is to add an additional column to the unioned query, that keeps track of the original id, and can be used to sort the records in the outer query. You can then use id desc as second sorting criteria, which will put null values in second position.
Demo on DB Fiddle:
with mytable as (
select 1 id, 100 job, 123 amount
union all select 2, 200, 321
union all select 3, 300, 421
)
select id, job, amount
from (
select id, job, amount, id order_by from mytable
union all
select null, null, null, id from mytable
) t
order by order_by, id desc;
id | job | amount
---: | ---: | -----:
1 | 100 | 123
null | null | null
2 | 200 | 321
null | null | null
3 | 300 | 421
null | null | null
In SQL Server, you can just use apply:
select v.id, v.job, v.amount
from t cross apply
(values (id, job, amount, id, 1),
(null, null, null, id, 2)
) v(id, job, amount, ord1, ord2)
order by ord1, ord2;

SQL Server Display total value in separate column where group by already breaks down the total

Sorry for the convoluted title!
I have a table called bookings as follows:
|Booking_id|Client_Id|Supplier_id|
----------------------------------
| 1 | 8 | 123 |
| 2 | 54 | 354 |
| 3 | 54 | 100 |
| 4 | 8 | 123 |
| 5 | 79 | 64 |
| 6 | 123 | 354 |
| 7 | 8 | 354 |
| 8 | 54 | 100 |
| 9 | 123 | 354 |
| 10 | 22 | 123 |
I have to get the % of total bookings per supplier per client
So output should be:
% of total bookings per supplier per client
|Client_id|Supplier_id|NumOfBookings|%_ofClientBookings|
--------------------------------------------------------
| 8 | 123 | 2 | 66 |
| 8 | 354 | 1 | 33 |
| 22 | 123 | 1 | 100 |
| 54 | 354 | 1 | 50 |
| 54 | 100 | 1 | 50 |
| 79 | 64 | 1 | 100 |
| 123 | 354 | 2 | 100 |
The query I have is as follows:
SELECT Client_id, COUNT(Booking_id) As NumOfBookings, Supplier_id,
FROM Booking
GROUP BY Client_id, Supplier_id
ORDER BY Client_id asc, NumOfBookings desc
I need to GROUP_BY Supplier_id and Client_id to see the number of bookings per supplier per client, but then I can't display the % of total bookings per client in the same table because the total booking figure isn't available to me. I can't seem to solve this, any suggestions?
I was able to get the desired result using derived tables, one for the number of bookings by supplier and client, and one for the total bookings by client.
Below is the code I used, you would need to replace #table with your table and ignore the inserts/create table.
IF OBJECT_ID('tempdb..#table') IS NOT NULL
BEGIN
DROP TABLE #table
END
CREATE TABLE #table(Booking_id int, Client_id int, supplier_id int)
insert into #table values(1,8,123)
insert into #table values(2,54,354)
insert into #table values(3,54,100)
insert into #table values(4,8,123)
insert into #table values(5,79,64)
insert into #table values(6,134,354)
insert into #table values(7,8,354)
insert into #table values(8,54,100)
insert into #table values(9,123,354)
insert into #table values(10,22,123)
SELECT NoB.Client_id,NoB.supplier_id,NoB.NumOfBookings, (NoB.NumOfBookings/(NULLIF(Tot.NumOfBookings,0) *1.0)) *100 FROM
(
SELECT Client_id, COUNT(Booking_id) As NumOfBookings, Supplier_id
FROM #table
GROUP BY Client_id, Supplier_id
) NoB JOIN
(
SELECT Client_id, COUNT(Booking_id) As NumOfBookings
FROM #table
GROUP BY Client_id
) Tot on Tot.Client_id=NoB.Client_id
Use "with" or multiple inline views, i.e. queries in from clause. With is simpler.
With bookbyclient as (select that has total for client. Key for result is client)
Now select like you have but join to bookbyclient. In select list divide booking value here by the total from above to get percent
This script will help-
SELECT A.Client_Id,A.Supplier_id,A.C NumOfBookings,
CAST((A.C*1.0)/B.C*100.00 AS INT) AS Percentage
FROM
(
SELECT Client_Id,Supplier_id ,COUNT(Booking_id) C
FROM your_table
GROUP BY Client_Id,Supplier_id
)A
INNER JOIN
(
SELECT Client_Id,COUNT(Booking_id) C
FROM your_table
GROUP BY Client_Id
)B
ON A.Client_Id = B.Client_Id
Try something like this:
SELECT Client_id,
COUNT(Booking_id) As NumOfBookings,
FLOOR(100*(CAST(COUNT(Booking_id) AS FLOAT)/(SELECT COUNT(Booking_id) FROM Booking bb
WHERE bb.Client_id = b.Client_id))),
Supplier_id
FROM Booking b
GROUP BY Client_id, Supplier_id
ORDER BY Client_id asc, NumOfBookings desc
Live Demo.
Try this
SELECT Client_id, NumOfBookings, Supplier_id
, (CAST(NumOfBookings AS DECIMAL(5, 2)) / SUM(NumOfBookings) OVER(PARTITION BY Client_id)) * 100 AS [%_ofClientBookings]
FROM (
SELECT Client_id, COUNT(Booking_id) As NumOfBookings, Supplier_id
FROM Booking
GROUP BY Client_id, Supplier_id
) a
ORDER BY Client_id asc, NumOfBookings desc

Select distinct one field other first non empty or null

I have table
| Id | val |
| --- | ---- |
| 1 | null |
| 1 | qwe1 |
| 1 | qwe2 |
| 2 | null |
| 2 | qwe4 |
| 3 | qwe5 |
| 4 | qew6 |
| 4 | qwe7 |
| 5 | null |
| 5 | null |
is there any easy way to select distinct 'id' values with first non null 'val' values. if not exist then null. for example
result should be
| Id | val |
| --- | ---- |
| 1 | qwe1 |
| 2 | qwe4 |
| 3 | qwe5 |
| 4 | qew6 |
| 5 | null |
In your case a simple GROUP BY should be the solution:
SELECT Id
,MIN(val)
FROM dbo.mytable
GROUP BY Id
Whenever using a GROUP BY, you have to use an aggregate function on all columns, which are not listed in the GROUP BY.
If an Id has a value (val) other than NULL, this value will be returned.
If there are just NULLs for the Id, NULL will be returned.
As far as i unterstood (regarding your comment), this is exactly what you're going to approach.
If you always want to have "the first" value <> NULL, you'll need another sort criteria (like a timestamp column) and might be able to solve it with a WINDOW-function.
If you want the first non-NULL value (where "first" is based on id), then MIN() doesn't quite do it. Window functions do:
select t.*
from (select t.*,
row_number() over (partition by id
order by (case when val is not null then 1 else 2 end),
id
) as seqnum
from t
) t
where seqnum = 1;
SQL Fiddle:
Create Table from SQL Fiddle:
CREATE TABLE tab1(pid integer, id integer, val varchar(25))
Insert dummy records :
insert into tab1
values (1, 1 , null),
(2, 1 , 'qwe1' ),
(3, 1 , 'qwe2'),
(4, 2 , null ),
(5, 2 , 'qwe4' ),
(6, 3 , 'qwe5' ),
(7, 4 , 'qew6' ),
(8, 4 , 'qwe7' ),
(9, 5 , null ),
(10, 5 , null );
fire below query:
SELECT Id ,MIN(val) as val FROM tab1 GROUP BY Id;

group data based on a condition and then sequentially ranking

The table has two columns, the ID is the PK and the other column is Type, sample data looks like the following.
| ID | Type |
| 1 | 0 |
| 2 | 0 |
| 3 | 1 |
| 4 | 1 |
| 5 | 0 |
| 6 | 1 |
I was trying to group the sample data into the following desired output.
1. The desired output is ordered by ID.
2. The Category is calculated based on the following logic,continuous records with the same type will be assigned the same category, and records with different types will increase category by 1.
| ID | Type |Category |
| 1 | 0 | 1 |
| 2 | 0 | 1 |
| 3 | 1 | 2 |
| 4 | 1 | 2 |
| 5 | 0 | 3 |
| 6 | 1 | 4 |
Is it able to do it in SQL server 2008 and 2012?
I found this (http://explainextended.com/2011/02/18/things-sql-needs-series/) might be able to resolve it.
Thanks in advance
It is possible but a bit weird since the value for Category depends on previous data entries.
Look at ROW_NUMBER() and DENSE_RANK() functions, which will do part of the numbering for you. I'd probably select a derived table ordering how you want and with row_number() to sequence them, join that back onto itself and compare rows with their previous one to see if Type matches and Id = Id-1, then re-rank based on that.
You might try (all in sql with the mapping in the sql query)
select PK, ID, Category
from (select PK, ID, "1" as 'Category' from Table1 where ID = 0 and PK < 3;
UNION
select PK, ID, "2" as 'Category' from Table1 where ID = 1 and PK < 5
UNION
select PK, ID, "3" as 'Category' from Table1 where PK = 5
UNION
select PK, ID, "4" as 'Category' from Table1 where PK = 6
}
order by PK
may this you are looking for
DECLARE #amount TABLE (ID INT,TYPE INT)
INSERT INTo #amount (ID,TYPE)VALUES (1,0)
INSERT INTo #amount (ID,TYPE)VALUES (2,0)
INSERT INTo #amount (ID,TYPE)VALUES (3,1)
INSERT INTo #amount (ID,TYPE)VALUES (4,1)
INSERT INTo #amount (ID,TYPE)VALUES (5,0)
INSERT INTo #amount (ID,TYPE)VALUES (6,1)
INSERT INTo #amount (ID,TYPE)VALUES (7,0)
select ID,TYPE,DENSE_RANK()OVER (PARTITION BY TYPE ORDER BY ID ASC ) AS RN from #amount