How to find only the top 3 values of a column and group the rest of the column values as zero? - sql-server

Consider a table having data as shown. I want to find the top 3 marks and combine the rest of the values of column marks as a single value 0.
name age marks height
-----------------------------
anil 25 67 5
ashish 23 75 6
ritu 22 0 4
manish 25 0 6
kirti 23 97 5
Output
name age marks height
-----------------------------
kirti 23 97 5
ashish 23 75 6
anil 25 67 5
OTHERS 0 0 0

With TOP 3 and UNION ALL for the last row:
select t.* from (
select top 3 * from tablename
order by marks desc
) t
union all
select 'OTHERS', 0, 0, 0
See the demo.
Results:
> name | age | marks | height
> :----- | --: | ----: | -----:
> kirti | 23 | 97 | 5
> ashish | 23 | 75 | 6
> anil | 25 | 67 | 5
> OTHERS | 0 | 0 | 0

I would use a CTE (Common Table Expression) and the ROW_NUMBER() function:
;WITH cte AS (SELECT
[Name],
Age,
Marks,
Height,
ROW_NUMBER() OVER (ORDER BY Marks DESC) AS [Rank]
FROM
Test
)
SELECT
[Name],
Age,
Marks,
Height
FROM
cte
WHERE
[Rank] <= 3
UNION ALL SELECT 'OTHERS', 0, 0, 0

You can use select top 3 or row_number()
You can use row_number() as follows
declare #mytable as table(name varchar(50),age int,marks int,height int)
insert into #mytable values('anil', 25, 67, 5),('ashish', 23, 75, 6),('ritu', 22, 0, 4),('manish', 25, 0, 6),('kirti', 23, 97, 5),
('other',0,0,0);
with cte as(
select name,age,marks,height,row_number() over(partition by 1 order by marks desc) row# from #mytable )
select name,age,marks,height from cte where row#<4 or name='other'
order by row#
Another way, using union without inserting ('other',0,0,0) to the table, you can the same result
declare #mytable as table(name varchar(50),age int,marks int,height int)
insert into #mytable values('anil', 25, 67, 5),('ashish', 23, 75, 6),('ritu', 22, 0, 4),('manish', 25, 0, 6),('kirti', 23, 97, 5)
--,('other',0,0,0)
;
with cte as(
select name,age,marks,height,row_number() over(partition by 1 order by marks desc) row# from #mytable )
select name,age,marks,height,row# from cte where row#<4
union select 'others',0,0,0,4
order by row#

Related

SQL Server find chains between two columns

I have a table like this:
from | to
-----+-----
23 | 24
24 | 25
25 | 27
27 | 30
45 | 46
46 | 47
50 | 52
53 | 60
I need a SQL Server query that detect chain's and return min (from) and max (to) in each chain (also chain's with one record):
from | to
-----+-----
23 | 30
45 | 47
50 | 52
53 | 60
Here's an approach using a recursive CTE.
CREATE TABLE #chainLinks(linkFrom INTEGER, linkTo INTEGER);
INSERT INTO #chainLinks VALUES (23,24);
INSERT INTO #chainLinks VALUES (24,25);
INSERT INTO #chainLinks VALUES (25,27);
INSERT INTO #chainLinks VALUES (27,30);
INSERT INTO #chainLinks VALUES (45,46);
INSERT INTO #chainLinks VALUES (46,47);
INSERT INTO #chainLinks VALUES (50,52);
INSERT INTO #chainLinks VALUES (53,60);
WITH reccte AS
(
/*Recursive Seed*/
SELECT linkFrom AS chainStart,
linkFrom,
linkTo,
0 as links
FROM #chainLinks as chainLinks
WHERE linkFrom NOT IN (SELECT DISTINCT linkTo FROM #chainLinks)
UNION ALL
/*Recursive Term*/
SELECT
reccte.chainStart,
chainLinks.linkFrom,
chainLinks.linkTo,
links + 1
FROM reccte
INNER JOIN #chainLinks as chainLinks ON reccte.linkTo = chainLinks.linkFrom
)
SELECT chainStart, linkTo AS chainEnd
FROM
(
SELECT chainStart, linkFrom, linkTo, links, ROW_NUMBER() OVER (PARTITION BY chainStart ORDER BY links DESC) AS rn
FROM reccte
)subrn
WHERE rn = 1;
A recursive CTE takes two parts
A recursive seed - This is the part above the UNION where we determine which records from our table begin the recursion. Here we want any linkFrom that isn't also a linkTo
A recusrive term - This is the part below the UNION where we join the cte called reccte back to the original table. This part of the CTE iterates over and over again until that join fails.
In here we are also tracking that links which is just a counter of the number of iterations we have gone through to get to that outputted record. We keep the highest number of links for each starting point chainStart.
Here is the working example: https://rextester.com/JWUW57837
If there are branches within the chains it become a little bit more tricky.
In the sample data below, there's a split on From=12.
So the result shows 2 chains starting from 14.
create table yourtable (
[From] int not null,
[To] int not null,
PRIMARY KEY ([From],[To])
)
GO
✓
insert into yourtable
([From],[To]) values
(2,3),(3,5),(5,4)
,(14,12),(12,15),(15,11),(11,10)
,(12,9)
,(21,23)
GO
9 rows affected
;WITH RCTE_CHAINS AS
(
-- seeding with the start of chains
SELECT [From] AS MinFrom, [From], [To], 0 AS Lvl
, CAST(IIF(EXISTS(
SELECT 1 FROM YourTable n
WHERE n.[From] = t.[To]
),1,0) AS BIT) AS hasNext
FROM YourTable t
WHERE NOT EXISTS
(
SELECT 1
FROM YourTable t2
WHERE t2.[To] = t.[From]
)
UNION ALL
-- looping through the childs
SELECT c.MinFrom, t.[From], t.[To], c.Lvl+1
, CAST(IIF(EXISTS(
SELECT 1 FROM YourTable n
WHERE n.[From] = t.[To]
),1,0) AS BIT) AS hasNext
FROM RCTE_CHAINS c
JOIN YourTable t ON t.[From] = c.[To]
)
SELECT MinFrom AS [From], [To]
FROM RCTE_CHAINS
WHERE hasNext = 0
GO
From | To
---: | -:
21 | 23
14 | 9
14 | 10
2 | 4
db<>fiddle here

Filling missing records with previous existing records

I have an existing database where some logic is made by the front end application.
Now I have to make reports from that database and I'm facing to a proble of missing records which are covered on a record basis in the frontend but have issues in the report
Given the following tables:
create table #T (id int, id1 int, label varchar(50))
create table #T1 (id int, T_id1 int, A int, B int, C int)
With the values:
insert into #T values (10, 1, 'label1'), (10, 2, 'label2'), (10, 3, 'label3'), (10, 15, 'label15'), (10, 16, 'label16'), (20, 100, 'label100'), (20, 101, 'label101')
insert into #T1 values (10, 1, 100, 200, 300), (10, 15, 150, 250, 350), (20, 100, 151, 251, 351), (20, 101, 151, 251, 351)
if I make a report we can see some missing records:
select #T.id, #T.id1, #T1.A, #T1.B, #T1.C
from #T left join #T1 on #T.id1 = #T1.T_id1
result:
id id1 A B C
10 1 100 200 300
10 2 NULL NULL NULL
10 3 NULL NULL NULL
10 15 150 250 350
10 16 NULL NULL NULL
20 100 151 251 351
20 101 151 251 351
Expected result would be:
id id1 A B B
10 1 100 200 300
10 2 100 200 300
10 3 100 200 300
10 15 150 250 350
10 16 150 250 350
20 100 151 251 351
20 101 151 251 351
As you can see here the missing data is filled out of the the first (in id, id1 order) previous existing record for a given id. For a given id there can be any number of "missing" records and for the given id there can be any number of existing records after a not existing ones.
I can do this with a cursor but I'm looking for a solution without cursor
You can use subquery (to find groups with same values) + window function
WITH Grouped AS (
SELECT #T.id, #T.id1, #T1.A, #T1.B, #T1.C,
GroupN = SUM(CASE WHEN #T1.A IS NULL THEN 0 ELSE 1 END) OVER(/* PARTITION BY id ? */ ORDER BY id1 ROWS UNBOUNDED PRECEDING)
FROM #T
LEFT JOIN #T1 ON #T.id1 = #T1.T_id1
)
SELECT Grouped.id, Grouped.id1,
A = MAX(A) OVER(PARTITION BY GroupN),
B = MAX(B) OVER(PARTITION BY GroupN),
C = MAX(C) OVER(PARTITION BY GroupN)
FROM Grouped
You can use below sql for thedesired output:
with cte (id, id1, A, B, C)
as
(
select #T.id, #T.id1, #T1.A, #T1.B, #T1.C
from #T left join #T1 on #T.id1 = #T1.T_id1
)
select cte.id, cte.id1,
coalesce(cte.A,TT.A) A,
coalesce(cte.B,TT.B) B,
coalesce(cte.C,TT.C) C
from cte
left join
(
select p.id1,max(q.id1) id1_max
from cte p
inner join cte q on p.id1 > q.id1 and p.a is null and q.a is not null
group by p.id1
) T on cte.id1 = T.id1
left join cte TT on T.id1_max = TT.id1

Calculate percentage based on row_number in sql

I'm trying to write a sql query that for each returned row, will also return a percentage (0-100) based on its row position. To further complicate this, the query is also currently grouped. Here's some example data to play with:
create table #test(StockId int)
insert into #test values (101), (101), (202), (202), (303), (404), (505)
select
StockId,
count(*) as BinMovements
from #test
group by StockId
order by BinMovements desc
This query currently returns:
StockId, BinMovements
101, 2
202, 2
303, 1
404, 1
505, 1
(Though obviously, because BinMovements is either 2 or 1 it would be equally correct to return the StockIds in a different order such as):
202, 2
101, 2
404, 1
303, 1
505, 1
I'd like to add a 'percentage' column, but this is really just based on row position, so I'd like to see values like:
101, 2, 100
202, 2, 80
303, 1, 60
404, 1, 40
505, 1, 20
I imagine the solution may involve ROW_NUMBER, and I started down this path thinking I could just get ROW_NUMBER / total rows * 100, but this isn't working for some reason. Possibly because of the group by clause?
select
StockId,
count(*) as BinMovements,
ROW_NUMBER() OVER(order by (count(*))) as RowNumber,
count(*) over () as TotalCount,
(ROW_NUMBER() OVER(order by (count(*)))) / (count(*) over ()) * 100 as Percentage
from #test
group by StockId
order by RowNumber desc
returns:
StockId BinMovements RowNumber TotalCount Percentage
202 2 5 5 100
101 2 4 5 0
505 1 3 5 0
404 1 2 5 0
303 1 1 5 0
I'd prefer to do this in a single select if possible, though if not wrapping it in an outer select may be a solution. Thanks
Both COUNT and ROW_NUMBER functions returns BIGINT.
Therefore result of division is also converted to BIGINT.
This works (you should multiply before divide):
(ROW_NUMBER() OVER(order by (count(*)))) * 100 / (count(*) over ()) as Percentage
Using count() over() and row_number() similar to your question, just reordering equation.
Note that the order by for the row_number() is the inverse of the order by for the statement, because we want to start the percentage at 100. Otherwise you can end up with 80 as the first row, 100 as the second, etc.
select
StockId
, BinMovements = count(*)
, Percentage = 100/count(*) over ()
* row_number() over (order by count(*) asc, stockid desc)
from #test
group by StockId
order by BinMovements desc, StockId asc
rextester demo: http://rextester.com/TCPE10348
+---------+--------------+------------+
| StockId | BinMovements | Percentage |
+---------+--------------+------------+
| 101 | 2 | 100 |
| 202 | 2 | 80 |
| 303 | 1 | 60 |
| 404 | 1 | 40 |
| 505 | 1 | 20 |
+---------+--------------+------------+
If you want a decimal percentage, change 100 to 100.0 in the equation.

Using SQL unpivot on two groups

I have a table as below:
Name ValueA1 ValueA2 ValueA3 ValueB1 ValueB2 ValueB3 QtyA1 QtyA2 QtyA3 QtyB1 QtyB2 QtyB3
John 1 2 3 4 5 6 100 200 300 150 250 350
Dave 11 12 13 14 15 16 100 200 300 150 250 350
I am able to use unpivot to get the values:
select [Name]
, Replace(u.[Period],'Value','') as [Period]
, u.[Value]
from Table1
unpivot
(
[Value]
for [Period] in ([ValueA1], [ValueA2], [ValueA3], [ValueB1], [ValueB2], [ValueB3])
) u;
SQL Fiddle
However I'm trying to get both the Value and Qty columns on a single row, what I want to end up with is:
Name Number Value Qty
John A1 1 100
John A2 2 200
John A3 3 300
John B1 4 150
John B2 5 250
John B3 6 350
Dave A1 11 100
Dave A2 12 200
Dave A3 13 300
Dave B1 14 150
Dave B2 15 250
Dave B3 16 350
What I have so far is (which doesn't work at all):
select [Name]
, Replace(u.[Period],'Value','') as [Period]
, u.[Value]
, u2.[Value]
from Table1
unpivot
(
[Value]
for [Period] in ([ValueA1], [ValueA2], [ValueA3], [ValueB1], [ValueB2], [ValueB3])
) u
unpivot
(
[Qty]
for [Period] in ([QtyA1], [QtyA2], [QtyA3], [QtyB1], [QtyB2], [QtyB3])
) u2;
Is what I am trying to do even possible with unpivot?
You can use a simple apply for this by specifying the pairs in a values clause:
declare #table table (Name varchar(10), ValueA1 int, ValueA2 int, QtyA1 int, QtyA2 int);
insert into #table
select 'John', 1, 2, 100, 200 union all
select 'Dave', 11, 12, 100, 200;
select Name, Number, Value, Qty
from #table
cross
apply ( values
('A1', ValueA1, QtyA1),
('A2', ValueA2, QtyA2)
) c (number, value, qty);
If you're using an older edition of MSSQL, you might need to use this
instead of the values clause above:
cross
apply ( select 'A1', ValueA1, QtyA1 union all
select 'A2', ValueA2, QtyA2
) c (number, value, qty);
Returns:
Name Number Value Qty
John A1 1 100
John A2 2 200
Dave A1 11 100
Dave A2 12 200

How to get this sql table from other table?

Is there a way to have something like:
id Name value
--------------------
1 sex m
2 age 12
3 weight 200
4 height 200
5 rx 34
from a known table:
sex age weight height rx
--------------------------
m 12 200 200 34
If I do:
Select
[id] = ORDINAL_POSITION,
[Name] = COLUMN_NAME
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'known'
I get:
id Name
-----------
1 sex
2 age
3 weight
4 height
5 rx
how to changethe query to get:
id Name value
--------------------
1 sex m
2 age 12
3 weight 200
4 height 200
5 rx 34
If they were 2 rows:
sex age weight height rx
--------------------------
m 12 200 200 34
f 34 245 111 67
id Name value
--------------------
1 sex m
2 age 12
3 weight 200
4 height 200
5 rx 34
6 sex f
7 age 34
8 weight 240
9 height 111
10 rx 67
-----------------EDIT--------------------
Thanks for your answers, but I am wondering if this can be possible intead of getting
id value
-------------------
1 m
2 12
3 200
4 200
5 34
from:
sex age weight height rx
--------------------------
m 12 200 200 34
using
Select
[id] = ORDINAL_POSITION,
[Value] ...
from INFORMATION_SCHEMA.COLUMNS
where TABLE_NAME = 'known'
The problem you're going to run into whatever method you choose is that all data in a particular column must be of the same type. In your case you have sex, which is a character, and a bunch of numbers (probably integers). This stops you being able to do this neatly as you cant use UNPIVOT. However there is always a way...
Given this setup code:
CREATE TABLE test(sex char(1), age int, weight int, height int, rx int)
INSERT INTO test
SELECT 'm', 12 , 200, 200, 34
union select 'f',34,245,111,67
You can do this, which is just a small addition to your query:
Select
[id] = ROW_NUMBER() OVER(ORDER BY isc.ORDINAL_POSITION),
[Name] = COLUMN_NAME,
[Value] = CASE LOWER(COLUMN_NAME)
WHEN 'sex' THEN CAST(d.sex AS VARCHAR(20))
WHEN 'age' then CAST(d.age AS VARCHAR(20))
WHEN 'weight' THEN CAST(d.weight AS VARCHAR(20))
WHEN 'height' THEN CAST(d.height AS VARCHAR(20))
WHEN 'rx' THEN CAST(d.rx AS VARCHAR(20))
END
from INFORMATION_SCHEMA.COLUMNS isc
CROSS JOIN dbo.test d
where TABLE_NAME = 'test'
Output:
1 sex m
2 sex f
3 age 12
4 age 34
5 weight 200
6 weight 245
7 height 200
8 height 111
9 rx 34
10 rx 67
You''ll notice this output is in a slightly different order to your own. This is because you have not described any key on your "known" table. If you did have a key on that table, you simply change this line:
[id] = ROW_NUMBER() OVER(ORDER BY isc.ORDINAL_POSITION),
to
[id] = ROW_NUMBER() OVER(ORDER BY d.yourKeyField, isc.ORDINAL_POSITION),
You can do it with PIVOT/UNPIVOT (see Books Online).
You can try this.
declare #T table
(
sex char(1),
age int,
weight int,
height int,
rx int
)
insert into #T values
('m', 12, 200, 200, 34),
('f', 34, 245, 111, 67)
select row_number() over(order by (select 1)) as ID,
T2.X.value('local-name(.)', 'varchar(128)') as Name,
T2.X.value('.', 'varchar(10)') as Value
from (select *
from #T
for xml path(''), type
) as T1(X)
cross apply T1.X.nodes('/*') as T2(X)
This part order by (select 1) makes the assignment of ID's somewhat unpredictable. If you had a primary key (ID int identity) or a datetime to use in order by you could change that to order by ID instead.

Resources