SQL Server - minimum value on row - sql

I have a table with a single row and two columns. Can I obtain the minimum value from the row using an SQL query?
value1 value2
1 43 39
The query should return the value 39.

The simplest method is probably apply:
select t.*, v.min_val
from t cross apply
(select min(val) as min_val
from (values (value1), (value2)) v(val)
) v;
For just two values that are not-null, you a case expression is also simple:
select t.*,
(case when value1 < value2 then value1 else value2 end) as min_val
from t;
However, this does not ignore null values. And it does not generalize quite as easily as one would like.

Certainly Gordon's answer (+1) is more performant and would be my first choice, but if by chance you are looking for a more "generalized" version, here is a simplified JSON approach.
This may be helpful if you have numerous or variable columns.
Example
Declare #YourTable table (ID int,value1 int,value2 int)
Insert into #YourTable values
(1,43,39)
Select A.*
,B.*
From #YourTable A
Cross Apply (
Select RowMin = min(Value)
From ( Select Value=try_convert(int,Value) -- << Set desired Datatype
From OpenJson( (Select A.* For JSON Path,Without_Array_Wrapper ) )
Where [Key] not in ('ID')
) J
) B
Returns
ID value1 value2 RowMin
1 43 39 39

Related

Selecting records with identical categories, but opposing integers

This is the nature of the table I am working with:
IF OBJECT_ID('TEMPDB..#TEMP') IS NOT NULL
DROP TABLE #TEMP
CREATE TABLE #TEMP (
CategoryA NVARCHAR(10),
CategoryB NVARCHAR(10),
CategoryC NVARCHAR(10),
IntegerA INT,
);
INSERT INTO #TEMP(CategoryA,CategoryB,CategoryC,IntegerA)
VALUES
('A','H','G',20),
('A','H','G',-15),
('F','L','C',10),
('N','U','X',12),
('K','G','G',15),
('K','G','G',-10);
SELECT * FROM #TEMP
Notice that the top 2 rows and the bottom 2 rows have identical categories, however they have integers of opposite polarity. The middle 2 rows are distinct with positive integers.
I need a way to select all of the records that are not duplicated (Such as the middle 2 rows). And I need to select the records with negative integers, without selecting their positive counter-parts.
The desired output in this case would be:
I have tried seeing if I can make my own table which inserts only the records I want, but I run into the same problem again where I cannot figure out how to distinguish between the records where all of the categories are the same.
For this dataset, you could just use row_number():
select categoryA, categoryB, categoryC, integerA
from (
select
t.*,
row_number() over(partition by categoryA, categoryB, categoryC order by integerA) rn
from temp t
) t
where rn = 1
Hmmm . . . I think you want:
select t.*
from #temp t
where t.integerA < 0 or
not exists (select 1
from #temp t2
where t2.A = t.A and t2.B = t.B and
t2.C = t.c and t2.integerA < 0
);
Here is a db<>fiddle.

Sum the two lowest values from a record

I have a table like this:
I need to sum the two lowest values for each record. For example, in the first row 2 and 4 (2 + 4 = 6).
I can find the lowest value for each row using CROSS APPLY, but I can't find the two lowest values at once to sum them.
Thanks in advance.
I would do this as:
select id, sumval - maxval
from t cross apply
(select sum(val) as sumval, max(val) as maxval
from values (value1), (value2), (value3)) v(val)
) v;
If you have three items, the sum of the smallest two is the sum of all of them minus the largest.
More generally, I would use something like this:
select id, sum2
from t cross apply
(select sum(val) as sum2
from (select top (2) val
from values (value1), (value2), (value3) v(val)
order by val asc
) v
) v
SELECT
IIF (VALUE1 < VALUE3 AND VALUE2 < VALUE3,
VALUE1 + VALUE2,
IIF(VALUE1 < VALUE2 AND VALUE3 < VALUE2,
VALUE1 + VALUE3,
IIF(VALUE3 < VALUE1 AND VALUE2 < VALUE1,
VALUE2 + VALUE3, 0)))
-- You will have to decide what to do if none of the conditions are met: I set the result to zero. This gets unwieldy if you add more columns
Initial data:
DECLARE #Table TABLE (ID INT IDENTITY(1,1),Value1 INT, Value2 INT, Value3 INT);
INSERT INTO #Table (Value1,Value2,Value3) VALUES
(2,4,5)
,(3,7,2)
,(9,1,6)
;
The code:
SELECT a.ID,SUM(a.[Value]) AS [Sum]
FROM (
SELECT p.ID,p.Value
,ROW_NUMBER()OVER(PARTITION BY p.ID ORDER BY p.Value ASC) AS [rn]
FROM #Table t
UNPIVOT(Value FOR Param IN ([Value1],[Value2],[Value3])) p
) a
WHERE a.rn <= 2 /*pick up only two lowest*/
GROUP BY a.ID
;

Average of rows in SQL Server

I have the below table
Col1 Col2 Col3 Col4 Col5
TotalAvg 68.79 65.39 88.21 63.14
I am already saving the total of all columns in the TotalAvg row but now I want to calculate the Average of the TotalAvg row. Can someone please tell me how I can calculate row average.
I am looking for
Select Avg(Col2,Col3,Col4,Col5)
where Col1 = 'TotalAvg'
Thanks
If some of them may have NULL values, you could still use AVG() inside an APPLY.
SELECT
yourTable.Col1,
RowStats.avg
FROM
yourTable
CROSS APPLY
(
SELECT
AVG(x) AS avg
FROM
(
SELECT yourTable.col2 AS x
UNION ALL
SELECT yourTable.col3 AS x
UNION ALL
SELECT yourTable.col4 AS x
UNION ALL
SELECT yourTable.col5 AS x
)
pivot
)
AS rowStats
If by chance you need a more dynamic approach (i.e. variable columns), and IF you're open to a TVF, consider the following:
EDIT
The 1st parameter is a delimited list of columns to exclude. For example: 'IDNr,Year,AnyOtherNumericCol'.
Example
Select A.*
,B.*
From YourTable A
Cross Apply [dbo].[tvf-Stat-Row-Agg]('',(Select A.* for XML Raw)) B
Returns
Col1 Col2 Col3 Col4 Col5 RetCnt RetSum RetMin RetMax RetAvg RetStd
TotalAvg 68.79 65.39 88.21 63.14 4 285.53 63.14 88.21 71.3825 11.4562162892757
The TVF if Interested
CREATE FUNCTION [dbo].[tvf-Stat-Row-Agg](#Exclude varchar(500),#XML xml)
Returns Table
As
Return (
Select RetCnt = Count(Value)
,RetSum = Sum(Value)
,RetMin = Min(Value)
,RetMax = Max(Value)
,RetAvg = Avg(Value)
,RetStd = Stdev(Value)
From (
Select Item = convert(varchar(100),xAttr.query('local-name(.)'))
,Value = try_convert(float,xAttr.value('.','varchar(max)'))
From #XML.nodes('//#*') x(xAttr)
) S
Where charindex(','+S.Item+',',','+#Exclude+',')=0
);
EDIT 2
If the columns are fixed, and performance is paramount, then...
Select A.*
,B.*
From YourTable A
Cross Apply (
Select AvgVal = avg(Value)
From (values (Col2)
,(Col3)
,(Col4)
,(Col5)
) B1(Value)
) B

SQL group by if values are close

Class| Value
-------------
A | 1
A | 2
A | 3
A | 10
B | 1
I am not sure whether it is practical to achieve this using SQL.
If the difference of values are less than 5 (or x), then group the rows (of course with the same Class)
Expected result
Class| ValueMin | ValueMax
---------------------------
A | 1 | 3
A | 10 | 10
B | 1 | 1
For fixed intervals, we can easily use "GROUP BY". But now the grouping is based on nearby row's value. So if the values are consecutive or very close, they will be "chained together".
Thank you very much
Assuming MSSQL
You are trying to group things by gaps between values. The easiest way to do this is to use the lag() function to find the gaps:
select class, min(value) as minvalue, max(value) as maxvalue
from (select class, value,
sum(IsNewGroup) over (partition by class order by value) as GroupId
from (select class, value,
(case when lag(value) over (partition by class order by value) > value - 5
then 0 else 1
end) as IsNewGroup
from t
) t
) t
group by class, groupid;
Note that this assumes SQL Server 2012 for the use of lag() and cumulative sum.
Update:
*This answer is incorrect*
Assuming the table you gave is called sd_test, the following query will give you the output you are expecting
In short, we need a way to find what was the value on the previous row. This is determined using a join on row ids. Then create a group to see if the difference is less than 5. and then it is just regular 'Group By'.
If your version of SQL Server supports windowing functions with partitioning the code would be much more readable.
SELECT
A.CLASS
,MIN(A.VALUE) AS MIN_VALUE
,MAX(A.VALUE) AS MAX_VALUE
FROM
(SELECT
ROW_NUMBER()OVER(PARTITION BY CLASS ORDER BY VALUE) AS ROW_ID
,CLASS
,VALUE
FROM SD_TEST) AS A
LEFT JOIN
(SELECT
ROW_NUMBER()OVER(PARTITION BY CLASS ORDER BY VALUE) AS ROW_ID
,CLASS
,VALUE
FROM SD_TEST) AS B
ON A.CLASS = B.CLASS AND A.ROW_ID=B.ROW_ID+1
GROUP BY A.CLASS,CASE WHEN ABS(COALESCE(B.VALUE,0)-A.VALUE)<5 THEN 1 ELSE 0 END
ORDER BY A.CLASS,cASE WHEN ABS(COALESCE(B.VALUE,0)-A.VALUE)<5 THEN 1 ELSE 0 END DESC
ps: I think the above is ANSI compliant. So should run in most SQL variants. Someone can correct me if it is not.
These give the correct result, using the fact that you must have the same number of group starts as ends and that they will both be in ascending order.
if object_id('tempdb..#temp') is not null drop table #temp
create table #temp (class char(1),Value int);
insert into #temp values ('A',1);
insert into #temp values ('A',2);
insert into #temp values ('A',3);
insert into #temp values ('A',10);
insert into #temp values ('A',13);
insert into #temp values ('A',14);
insert into #temp values ('b',7);
insert into #temp values ('b',8);
insert into #temp values ('b',9);
insert into #temp values ('b',12);
insert into #temp values ('b',22);
insert into #temp values ('b',26);
insert into #temp values ('b',67);
Method 1 Using CTE and row offsets
with cte as
(select distinct class,value,ROW_NUMBER() over ( partition by class order by value ) as R from #temp),
cte2 as
(
select
c1.class
,c1.value
,c2.R as PreviousRec
,c3.r as NextRec
from
cte c1
left join cte c2 on (c1.class = c2.class and c1.R= c2.R+1 and c1.Value < c2.value + 5)
left join cte c3 on (c1.class = c3.class and c1.R= c3.R-1 and c1.Value > c3.value - 5)
)
select
Starts.Class
,Starts.Value as StartValue
,Ends.Value as EndValue
from
(
select
class
,value
,row_number() over ( partition by class order by value ) as GroupNumber
from cte2
where PreviousRec is null) as Starts join
(
select
class
,value
,row_number() over ( partition by class order by value ) as GroupNumber
from cte2
where NextRec is null) as Ends on starts.class=ends.class and starts.GroupNumber = ends.GroupNumber
** Method 2 Inline views using not exists **
select
Starts.Class
,Starts.Value as StartValue
,Ends.Value as EndValue
from
(
select class,Value ,row_number() over ( partition by class order by value ) as GroupNumber
from
(select distinct class,value from #temp) as T
where not exists (select 1 from #temp where class=t.class and Value < t.Value and Value > t.Value -5 )
) Starts join
(
select class,Value ,row_number() over ( partition by class order by value ) as GroupNumber
from
(select distinct class,value from #temp) as T
where not exists (select 1 from #temp where class=t.class and Value > t.Value and Value < t.Value +5 )
) ends on starts.class=ends.class and starts.GroupNumber = ends.GroupNumber
In both methods I use a select distinct to begin because if you have a dulpicate entry at a group start or end things go awry without it.
Here is one way of getting the information you are after:
SELECT Under5.Class,
(
SELECT MIN(m2.Value)
FROM MyTable AS m2
WHERE m2.Value < 5
AND m2.Class = Under5.Class
) AS ValueMin,
(
SELECT MAX(m3.Value)
FROM MyTable AS m3
WHERE m3.Value < 5
AND m3.Class = Under5.Class
) AS ValueMax
FROM
(
SELECT DISTINCT m1.Class
FROM MyTable AS m1
WHERE m1.Value < 5
) AS Under5
UNION
SELECT Over4.Class,
(
SELECT MIN(m4.Value)
FROM MyTable AS m4
WHERE m4.Value >= 5
AND m4.Class = Over4.Class
) AS ValueMin,
(
SELECT Max(m5.Value)
FROM MyTable AS m5
WHERE m5.Value >= 5
AND m5.Class = Over4.Class
) AS ValueMax
FROM
(
SELECT DISTINCT m6.Class
FROM MyTable AS m6
WHERE m6.Value >= 5
) AS Over4

How to create a sum column without an internal subquery?

I'm using SQL Server 2008 R2
I have a complex query which I need to have a conditionally summed column for it.
Here is a simplified version of my query and results:
DECLARE #t TABLE (id int, condition int, value int);
INSERT INTO #t
VALUES (1,1,12), (2,0,88), (3,1,11)
SELECT
*,
(SELECT SUM(value) FROM #t WHERE condition = 1) as SumOfConditionalValues
FROM #t
Here are the results of this query"
id condition value SumOfConditionalValues
1 1 12 23
2 0 88 23
3 1 11 23
I can't afford the SumOfConditionalValues sub query.
Is there an elegant way to achieve the conditionally summed column without it?
Which aggregate commands are suitable here, if any, and how do I apply these?
Try this:
SELECT *, SUM(CASE WHEN condition = 1 THEN value END) OVER() SumOfConditionalValues
FROM #t
See here: http://sqlfiddle.com/#!3/1abea/1
Use a self join:
CREATE TABLE MyTable (id int, condition int, value int);
INSERT INTO MyTable
VALUES (1,1,12), (2,0,88), (3,1,11)
SELECT
MyTable.id,
MyTable.Condition,
MyTable.value,
SUM(JoinedMyTable.Value)
FROM
MyTable
LEFT JOIN MyTable JoinedMyTable ON MyTable.condition = JoinedMyTable.Condition
GROUP BY
MyTable.id,
MyTable.Condition,
MyTable.value
EDIT: Don't know if you want every row to show the sum of rows where condition = 1, but if you do just change the join clause to be:
LEFT JOIN MyTable JoinedMyTable ON JoinedMyTable.Condition = 1
I believe what you are looking for is the "CASE' statement...Very powerful, for example:
select id, sum(case when condition=1 then value else 0 end) group by id..etc
Declare #sum int
SELECT #sum=SUM(value) FROM #t WHERE condition = 1
select *,#sum from yourtable
You can bring the results aggregated by condition:
DECLARE #t TABLE (id int, condition int, value int);
INSERT INTO #t VALUES (1,1,12), (2,0,88), (3,1,11)
SELECT *,
sum(value) over (partition by condition) as SumOfConditionalValues
FROM #t
I think this is what you want:
SELECT id, condition, value,
SUM(CASE WHEN condition = 1 THEN value_at_1 END) OVER() SumOfConditionalValues
FROM (select *,
(case when condition = 1 then value end) as value_at_1
from #t
) t
You need a "conditional value", which you can create in a subquery.