Select Top n Values Across Columns and Average - sql-server

I have a table that has 13 columns, one with type varchar(25) and the rest with type `int (holding values for each month of the year).
For each row, I would like to pick the top 6 int values from the 12 columns and calculate the average of those values.
I know how to select the top n from a given column, but how do you do it across multiple columns?

select ID,
(
select avg(C)
from (
select top(6) C
from (values(C1),(C2),(C3),(C4),(C5),(C6),(C7),
(C8),(C9),(C10),(C11),(C12)) as T(C)
order by C desc
) as T
) as C
from YourTable
SQL Fiddle
For SQL Server 2005 it would look like this since you can't use the Table Value Constructor
select ID,
(
select avg(C)
from (
select top(6) C
from (select C1 union all
select C2 union all
select C3 union all
select C4 union all
select C5 union all
select C6 union all
select C7 union all
select C8 union all
select C9 union all
select C10 union all
select C11 union all
select C12) as T(C)
order by C desc
) as T
) as C
from YourTable
SQL Fiddle
And for SQL Server 2000 this could work for you.
select T1.ID,
avg(C) as C
from (
select ID, C1 as C from YourTable union all
select ID, C2 from YourTable union all
select ID, C3 from YourTable union all
select ID, C4 from YourTable union all
select ID, C5 from YourTable union all
select ID, C6 from YourTable union all
select ID, C7 from YourTable union all
select ID, C8 from YourTable union all
select ID, C9 from YourTable union all
select ID, C10 from YourTable union all
select ID, C11 from YourTable union all
select ID, C12 from YourTable
) as T1
where (
select count(*)
from (
select ID, C1 as C from YourTable union all
select ID, C2 from YourTable union all
select ID, C3 from YourTable union all
select ID, C4 from YourTable union all
select ID, C5 from YourTable union all
select ID, C6 from YourTable union all
select ID, C7 from YourTable union all
select ID, C8 from YourTable union all
select ID, C9 from YourTable union all
select ID, C10 from YourTable union all
select ID, C11 from YourTable union all
select ID, C12 from YourTable
) as T2
where T1.ID = T2.ID and
T1.C <= T2.C
) <= 6
group by T1.ID
SQL Fiddle
I would not expect this to be particularly fast. Perhaps a better option is to store an intermediate result in a temp table.
create table #T
(
ID varchar(25),
C int
)
insert into #T
select ID, C1 as C from YourTable union all
select ID, C2 from YourTable union all
select ID, C3 from YourTable union all
select ID, C4 from YourTable union all
select ID, C5 from YourTable union all
select ID, C6 from YourTable union all
select ID, C7 from YourTable union all
select ID, C8 from YourTable union all
select ID, C9 from YourTable union all
select ID, C10 from YourTable union all
select ID, C11 from YourTable union all
select ID, C12 from YourTable
select T1.ID,
avg(C) as C
from #T as T1
where (
select count(*)
from #T as T2
where T1.ID = T2.ID and
T1.C <= T2.C
) <= 6
group by T1.ID
drop table #T

Firstly, it is important to understand that using TOP in conjunction with aggregates will not limit the aggregation but rather the result set. Look at this example:
SELECT TOP 2 SUM(col) FROM
(SELECT 1 AS col
UNION
SELECT 2
UNION
SELECT 3)sq
The result is still "6".
Secondly, aggregation does NOT work across columns, only rows. You would need to evaluate them manually. Probably the most efficient way would be to create a table from the row like this:
SELECT
(SELECT MAX(myval) FROM (values (col1), (col2), (col3), (col4)) as all_values(myval))
FROM (SELECT 1 as col1, 2 as col2, 3 as col3, 4 as col4)sq

If you are using SQL Server 2005 or later you can unpivot the table, then rank the values, and last calculate the averages the for the top 6 values per identifier.
something like this:
;WITH UnPivoted AS (
SELECT pk, MonthID, MonthNumber, MonthValue
FROM
(SELECT pk, Month1, Month2, Month3, Month4, Month5, Month6, Month7, Month8, Month9, Month10, Month11, Month12
FROM pvt) p
UNPIVOT
(pk FOR MonthNumber IN
(Month1, Month2, Month3, Month4, Month5, Month6, Month7, Month8, Month9, Month10, Month11, Month12)
)AS unpvt
),
UnPivotedRanked AS (
SELECT pk, MonthValue, RANK() OVER(PARTITION BY pk ORDER BY MonthValue DESC) AS pkRanked
FROM UnPivoted
GROUP BY pk
)
SELECT pk, AVG(MonthValue) AS Top6Average
FROM UnPivotedRanked
WHERE pkRanked < 6
GROUP BY pk

Related

T-SQL: Need Top N to always return N rows, even if null or blank

T-SQL: Need Top N to always return N rows, even if null or blank
Typically, the command
Select Top 5 * FROM ourTable
will return up to 5 rows, but less, depending whether the rows exist.
I want to ensure that it always returns 5 rows, (or in general N rows).
What is the syntax to achieve this?
The idea is to sort of generalize the LINQ concept of "FirstOrDefault" to "First_N_OrDefault", but using TSQL not LINQ.
Clearly, the 'extra' rows would have null or empty columns.
This is for Microsoft SQL Server 2014 using SSMS 14.0.17
I want to use the "TOP" syntax, if at all possible, therefore it is different than the possible duplicate. Also, as noted below, this is possibly something that could be solved at a different layer in the system, but it would be nice to have for TSQL as well.
select top (5) c1, c2, c3 from (
select top (5) c1, c2, c3, 0 as priority from ourTable
union all
select c1, c2, c3, 1 from (values (null, null, null), (null, null, null), (null, null, null), (null, null, null), (null, null, null)) v (c1, c2, c3)
) t
order by priority
You can use another dummy table with rows to generate empty rows of your table with a not matching JOIN. So you don't have to repeat the columns and rows in the UNION ALL part:
SELECT TOP 5 * FROM (
SELECT 0 AS isDummy, * FROM table_name
-- WHERE column_name = value
UNION ALL
SELECT 1 AS isDummy, t1.* FROM table_name t1
RIGHT JOIN INFORMATION_SCHEMA.COLUMNS ON t1.id = -1000 -- not valid condition so t1 columns are empty.
) t2
ORDER BY isDummy ASC
In this case the INFORMATION_SCHEMA.COLUMNS table is used to generate the additional rows. You can choose any other table with rows. You can use a TOP N value up to the count of rows in the right table (here: INFORMATION_SCHEMA.COLUMNS).
You can also generate a table with many rows (like on a calendar table):
SELECT TOP 5 * FROM (
SELECT 0 isDummy, * FROM table_name
-- WHERE column_name = value
UNION ALL
SELECT 1 isDummy, t1.* FROM table_name t1 RIGHT JOIN (
SELECT * FROM
(SELECT 0 t0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t0,
(SELECT 0 t1 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
(SELECT 0 t2 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
(SELECT 0 t3 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
(SELECT 0 t4 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t4
) t2 ON t1.id = -1000 -- not valid condition so t1 columns are empty.
)x
ORDER BY isDummy ASC
You can use a limited tally table together with a gaplessly generated row-number like here:
The SELECT is always the same. The only thing changing is the amount of rows in the mockup-table:
DECLARE #TopCount INT=5;
--Case 1: More then 5 rows in the table
DECLARE #tbl TABLE(ID INT IDENTITY,SomeValue VARCHAR(100));
INSERT INTO #tbl VALUES
('Value1'),('Value2'),('Value3'),('Value4'),('Value5'),('Value6'),('Value7');
WITH Tally(Nmbr) AS(SELECT TOP(#TopCount) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values)
,NumberedRows AS(SELECT ROW_NUMBER() OVER(ORDER BY ID) AS GeneratedRowNumber, * FROM #tbl)
SELECT *
FROM NumberedRows nr
FULL OUTER JOIN Tally t ON nr.GeneratedRowNumber=t.Nmbr;
--Case 2: Less than 5 rows in the table
DELETE FROM #tbl WHERE ID BETWEEN 2 AND 5;
WITH Tally(Nmbr) AS(SELECT TOP(#TopCount) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values)
,NumberedRows AS(SELECT ROW_NUMBER() OVER(ORDER BY ID) AS GeneratedRowNumber, * FROM #tbl)
SELECT *
FROM NumberedRows nr
FULL OUTER JOIN Tally t ON nr.GeneratedRowNumber=t.Nmbr;
--Case 3: Exactly one row in the table
DELETE FROM #tbl WHERE ID <> 6;
WITH Tally(Nmbr) AS(SELECT TOP(#TopCount) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values)
,NumberedRows AS(SELECT ROW_NUMBER() OVER(ORDER BY ID) AS GeneratedRowNumber, * FROM #tbl)
SELECT *
FROM NumberedRows nr
FULL OUTER JOIN Tally t ON nr.GeneratedRowNumber=t.Nmbr;
--Case 4: Table is empty
DELETE FROM #tbl;
WITH Tally(Nmbr) AS(SELECT TOP(#TopCount) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values)
,NumberedRows AS(SELECT ROW_NUMBER() OVER(ORDER BY ID) AS GeneratedRowNumber, * FROM #tbl)
SELECT *
FROM NumberedRows nr
FULL OUTER JOIN Tally t ON nr.GeneratedRowNumber=t.Nmbr;
This will return all rows from the source, but at least the specified count.
If you want to limit the set to exactly 5 rows (e.g. in "Case 1"), you can use SELECT TOP(#TopCount) * and place an appropriate ORDER BY. This would return the specified row count in any case.

Two columns into one using alternate rows

I have a table with two columns like this:
A 1
B 2
C 3
D 4
E 5
etc.
I want to get them into one column, with each column's data in alternate rows of the new column like this:
A
1
B
2
C
3
D
4
E
5
etc.
I would use a UNION ALL but here is the UNPIVOT alternative:
CREATE TABLE #Table1(letter VARCHAR(10),Id VARCHAR(10))
INSERT INTO #Table1(letter ,Id )
SELECT 'A',1 UNION ALL
SELECT 'B',2 UNION ALL
SELECT 'C',3 UNION ALL
SELECT 'D',4 UNION ALL
SELECT 'E',5
SELECT [value]
FROM #Table1
UNPIVOT
(
[value] FOR [Column] IN ([Id], [letter])
) UNPVT
DROP TABLE #Table1;
The tricky part is the data in alternate rows
select col2
from
( select col1, 1 as flag, col1 from tab
union all
select col1, 2, col2 from tab
) as dt
order by col1, flag
But why do you try to do this at all?
Try this :
WITH CTE AS
(
SELECT Col1Name Name,(ROW_NUMBER() OVER(ORDER BY(SELECT NULL))) RN
FROM TableName
UNION
SELECT Col2Name Name,(ROW_NUMBER() OVER(ORDER BY(SELECT NULL)))+1 RN
FROM TableName
)
SELECT Name
FROM CTE
ORDER BY RN
CREATE TABLE #Table1(Value VARCHAR(10),Id VARCHAR(10))
INSERT INTO #Table1(Value ,Id )
SELECT 'A',1 UNION ALL
SELECT 'B',2 UNION ALL
SELECT 'C',3 UNION ALL
SELECT 'D',4 UNION ALL
SELECT 'E',5
;WITH _CTE (Name) AS
(
SELECT Value [Name]
FROM #Table1
UNION ALL
SELECT Id [Name]
FROM #Table1
)
SELECT * FROM _CTE

How make possible Joins with group by clause

I have two tables, table1 has 2 columns as
id name
1 Amal
2 Varun
3 Sari
table2 has 3 columns as
id Subject marks
1 Maths 80
1 Malayalam 75
1 History 45
2 Maths 90
2 Malayalam 85
2 History 50
3 Maths 88
3 Malayalam 75
3 History 80
My question is to find the names who has the maximum mark for each subject (Subject wisw topper) the resultant table have to includes the fields name subject and marks
I tested with the following query
SELECT
table1.Student_Name, (table2.subject), max(table2.Marks_obt)
FROM
table2
INNER JOIN
table1 ON table2.stud_id = table1.Student_ID
GROUP BY
[Student_Name], table2.Subject
HAVING
MAX(Marks_obt) IN (SELECT MAX(Marks_obt) AS total_marks
FROM table2
GROUP BY subject)
In SQL Server 2008, but I got the result as
name subject
Sari History 80
Varun Malayalam 85
Amal Maths 80
Varun Maths 90
how I get the topper of three subject with these manner?
You can use ROW_NUMBER() :
SELECT s.subject,s.name,s.marks
FROM(
SELECT t1.*,t2.subject,t2.marks,
ROW_NUMBER() OVER(PARTITION BY t2.subject ORDER BY t2.marks DESC) as rnk
FROM Table1
JOIN Table2
ON table2.stud_id = table1.Student_ID) s
WHERE s.rnk = 1
Use Rank You will not miss any people for example like two people will get same highest marks in same subject. And if you want query to find 2nd highest marks or 3rd highest use Dense_Rank() function even dense_rank() also works for finding 1st highest. For More cilck here
SELECT NAME,
SUBJECT,
MARKS
FROM (SELECT NAME,
SUBJECT,
MARKS,
rank()
OVER(
PARTITION BY [SUBJECT]
ORDER BY MARKS DESC) RNO
FROM #TABLE1 T
JOIN #TABLE2 T2
ON T.ID = T2.ID) A
WHERE RNO = 1
CREATE TABLE #table1
(Student_ID INT,
Student_Name VARCHAR(20))
INSERT INTO #table1
SELECT 1,'Amal'
UNION
SELECT 2,'Varun'
UNION
SELECT 3,'Sari'
CREATE TABLE #table2
(
stud_id INT,
[subject] VARCHAR(20),
Marks_obt INT
)
INSERT INTO #table2
SELECT 1,'Maths',80
UNION
SELECT 1,'Malayalam',75
UNION
SELECT 1,'History',45
UNION
SELECT 2,'Maths',90
UNION
SELECT 2,'Malayalam',85
UNION
SELECT 2,'History',80
UNION
SELECT 3,'Maths',88
UNION
SELECT 3,'Malayalam',75
UNION
SELECT 3,'History',80
/*Table 1*/
SELECT * FROM #table1
/*Table 2*/
SELECT * FROM #table2
/*Top Mark*/
SELECT [subject],
Student_Name,
Marks_obt
FROM(SELECT Student_Name,
[subject],
Marks_obt,
RANK()
OVER(
PARTITION BY [subject]
ORDER BY Marks_obt DESC) RowNum
FROM #table1 T1
JOIN #table2 T2
ON T1.Student_ID= T2.stud_id) AS data
WHERE data.RowNum = 1
DROP TABLE #table1,#table2
you can use a cross apply too like this
with maxi as (
select Subject, max(marks) maximark from table2
group by Subject
)
select * from maxi f1
cross apply
(
select top 1 f2.name from table1 f2 inner join table2 f3 on f2.id=f3.id
where f1.maximark=f3.marks and f1.subject=f3.subject
) f3
if multiple users are possible for a maxi mark, remove "top 1"
other solution with imbication:
with maxi as (
select Subject, max(marks) maximark from table2
group by Subject
)
select (select top 1 f2.name from table1 f2 inner join table2 f3 on f2.id=f3.id where f1.maximark=f3.marks and f1.Subject=f3.Subject) as Name, f1.*
from maxi f1

How to find the maximum value in join without using if in sql stored procedure

I have a two tables like below
A
Id Name
1 a
2 b
B
Id Name
1 t
6 s
My requirement is to find the maximum id from table and display the name and id for that maximum without using case and if.
i findout the maximum by using below query
SELECT MAX(id)
FROM (SELECT id,name FROM A
UNION
SELECT id,name FROM B) as c
I findout the maximum 6 using the above query.but i can't able to find the name.I tried the below query but it's not working
SELECT MAX(id)
FROM (SELECT id,name FROM A
UNION
SELECT id,name FROM B) as c
How to find the name?
Any help will be greatly appreciated!!!
First combine the tables, since you need to search both. Next, determine the id you need. JOIN the id back with the temporarily created table to retreive the name that belongs to that id:
WITH tmpTable AS (
SELECT id,name FROM A
UNION
SELECT id,name FROM B
)
, max AS (
SELECT MAX(id) id
FROM tmpTable
)
SELECT t.id, t.name
FROM max m
JOIN tmpTable t ON m.id = t.id
You could use ROW_NUMBER(). You have to UNION ALL TableA and TableB first.
WITH TableA(Id, Name) AS(
SELECT 1, 'a' UNION ALL
SELECT 2, 'b'
)
,TableB(Id, Name) AS(
SELECT 1, 't' UNION ALL
SELECT 6, 's'
)
,Combined(Id, Name) AS(
SELECT * FROM TableA UNION ALL
SELECT * FROM TableB
)
,CTE AS(
SELECT *, RN = ROW_NUMBER() OVER(ORDER BY ID DESC) FROM Combined
)
SELECT Id, Name
FROM CTE
WHERE RN = 1
Just order by over the union and take first row:
SELECT TOP 1 * FROM (SELECT * FROM A UNION SELECT * FROM B) x
ORDER BY ID DESC
This won't show ties though.
For you stated that you used SQL Server 2008. Therefore,I used FULL JOIN and NESTED SELECT to get what your looking for. See below:
SELECT
(SELECT
1,
ISNULL(A.Id,B.Id)Id
FROM A FULL JOIN B ON A.Id=B.Id) AS Id,
(SELECT
1,
ISNULL(A.Name,B.Name)Name
FROM A FULL JOIN B ON A.Id=B.Id) AS Name
It's possible to use ROW_NUMBER() or DENSE_RANK() functions to get new numiration by Id, and then select value with newly created orderId equal to 1
Use:
ROW_NUMBER() to get only one value (even if there are some repetitions of max id)
DENSE_RANK() to get all equal max id values
Here is an example:
DECLARE #tb1 AS TABLE
(
Id INT
,[Name] NVARCHAR(255)
)
DECLARE #tb2 AS TABLE
(
Id INT
,[Name] NVARCHAR(255)
)
INSERT INTO #tb1 VALUES (1, 'A');
INSERT INTO #tb1 VALUES (7, 'B');
INSERT INTO #tb2 VALUES (4, 'C');
INSERT INTO #tb1 VALUES (7, 'D');
SELECT * FROM
(SELECT Id, Name, ROW_NUMBER() OVER (ORDER BY Id DESC) AS [orderId]
FROM
(SELECT Id, Name FROM #tb1
UNION
SELECT Id, Name FROM #tb2) as tb3) AS TB
WHERE [orderId] = 1
SELECT * FROM
(SELECT Id, Name, DENSE_RANK() OVER (ORDER BY Id DESC) AS [orderId]
FROM
(SELECT Id, Name FROM #tb1
UNION
SELECT Id, Name FROM #tb2) as tb3) AS TB
WHERE [orderId] = 1
Results are:

Calculating value using previous value of a row in T-SQL

I got following table and want to calculate value of Column2 on each row using the value of the same column (Column2) from the previous row in a sql without using cursor or while loop.
Id Date Column1 Column2
1 01/01/2011 5 5 => Same as Column1
2 02/01/2011 2 18 => (1 + (value of Column2 from the previous row)) * (1 + (Value of Column1 from the current row)) i.e. (1+5)*(1+2)
3 03/01/2011 3 76 => (1+18)*(1+3) = 19*4
and so on
Any thoughts?
Assuming at least SQL Server 2005 for the recursive CTE:
;with cteCalculation as (
select t.Id, t.Date, t.Column1, t.Column1 as Column2
from YourTable t
where t.Id = 1
union all
select t.Id, t.Date, t.Column1, (1+t.Column1)*(1+c.Column2) as Column2
from YourTable t
inner join cteCalculation c
on t.Id-1 = c.id
)
select c.Id, c.Date, c.Column1, c.Column2
from cteCalculation c
I solved the problem, just mentioned.
This is my code:
;with cteCalculation as (
select t.Id, t.Column1, t.Column1 as Column2
from table_1 t
where t.Id = 1
union all
select t.Id, t.Column1, (1+t.Column1)*(1+c.Column2) as Column2
from table_1 t
inner join cteCalculation c
on t.Id-1 = c.id
),
cte2 as(
select t.Id, t.Column1 as Column3
from table_1 t
where t.Id = 1
union all
select t.Id, (select column2+1 from cteCalculation c where c.id = t.id) as Column3
from table_1 t
inner join cte2 c2
on t.Id-1 = c2.id
)
select c.Id, c.Column1, c.Column2, c2.column3
from cteCalculation c
inner join cte2 c2 on c.id = c2.id
The result is as I was expected:
1 5 5 5
2 2 18 19
3 3 76 77
Here is an example using ROW_NUMBER() if the Id's aren't necessarily in order:
;with DataRaw as (
select 1 as Id, '01/01/11' as Date, 5 as Column1 union
select 2 as Id, '02/01/11' as Date, 2 as Column1 union
select 4 as Id, '03/01/11' as Date, 3 as Column1
),
Data as (
select RowId = ROW_NUMBER() over (order by Id), Id, Date, Column1 from DataRaw
),
Data2 as (
select
RowId, id, Date, Column1, Column1 as Column2
from
Data d
where
RowId = 1
union all
select
d1.RowId, d1.id, d1.Date, d1.Column1, (1+d1.column1)*(1+d2.column2) as column2
from
Data d1
cross join
Data2 d2
where
d2.RowId + 1 = d1.RowId
)
select
Id, Date, Column1, Column2
from
Data2
edit: shoudld have read the question better...
Another go woudl be this:
;with DataRaw as (
select 1 as Id, '01/01/11' as Date, 5 as Column1 union
select 2 as Id, '02/01/11' as Date, 2 as Column1 union
select 4 as Id, '03/01/11' as Date, 3 as Column1
),
Data as (
select Ord = ROW_NUMBER() over (order by Id), Id, Date, Column1 from DataRaw
),
select -- formula goes here, using current and prev as datasources.
from data current
left join data prev on current.Ord = prev.Ord + 1 -- pick the previous row by adding 1 to the ordinal
I think a normal join to get to the previous row would be faster than a CTE. You;d have to check for yourself though.
Looks easier to me.
Good luck, GJ

Resources