I have a table like below:
Type PKG_HA_01_ON PKG_HA_03_ON PKG_HA_04_ON PKG_HA_05_ON PKG_HA_06_ON PKG_HA_09_ON
duration 18.6694 60 15.1951 56.2068 13.6808 13.8404
counter 5 0 5 11 2 0
The first row is the header. Now, I would like to transpose table into this
Machine Duration Counter
PKG_HA_01_ON 18.6694 5
PKG_HA_03_ON 60 0
...
I have tried unpivot but the result is not desired table.
Thanks in advance,
Try this:
create table unpivot_raw(
[Type] nvarchar(255)
, PKG_HA_01_ON float null
, PKG_HA_03_ON float null
, PKG_HA_04_ON float null
, PKG_HA_05_ON float null
, PKG_HA_06_ON float null
, PKG_HA_09_ON float null
)
insert into unpivot_raw
select 'duration', 18.6694, 60, 15.1951, 56.2068, 13.6808, 13.8404
union
select 'counter', 5, 0, 5, 11, 2, 0
select
*
from
(
select
[Type]
, vl
, Machine
from
(
select
[Type]
, PKG_HA_01_ON
, PKG_HA_03_ON
, PKG_HA_04_ON
, PKG_HA_05_ON
, PKG_HA_06_ON
, PKG_HA_09_ON
from unpivot_raw
) p
unpivot
(
vl for Machine in
(
PKG_HA_01_ON
, PKG_HA_03_ON
, PKG_HA_04_ON
, PKG_HA_05_ON
, PKG_HA_06_ON
, PKG_HA_09_ON
)
) unpvt
) base
pivot
(
max(vl) for [Type] in (duration, counter )
) pvt
I recommend using cross apply to unpivot and then aggregation:
select machine,
max(case when type = 'duration' then val end) as duration,
max(case when type = 'counter' then val end) as counter
from t cross apply
(values ('PKG_HA_01_ON', PKG_HA_01_ON),
('PKG_HA_03_ON', PKG_HA_03_ON),
('PKG_HA_04_ON', PKG_HA_04_ON),
('PKG_HA_05_ON', PKG_HA_05_ON),
('PKG_HA_06_ON', PKG_HA_06_ON),
('PKG_HA_09_ON', PKG_HA_09_ON)
) v(machine, val)
group by machine;
I much, much prefer this over pivot/unpivot. Why? APPLY implements a lateral join, which is a powerful construct in SQL (and part of the SQL standard albeit with slightly different syntax). Unpivoting is a nice introduction to this feature.
The pivoting functions are bespoke functions and not part of the standard. They also do not generalize in any way, being designed for a single purpose.
Related
I have a set of columns CODE_1-10, which contain diagnostic codes. I want to create a set of variables CODE_GROUP_1-17, which indicate whether or not one of some particular set of diagnostic codes matches any of the CODE_1-10 variables. For example, CODE_GROUP_1 = 1 if any of CODE_1-10 match either '123' or '456', and CODE_GROUP_2 = 1 if any of CODE_1-10 match '789','111','333','444' or 'foo'.
Here's an example of how you could do this using values constructors.
CASE WHEN (SELECT count(value.val)
FROM (VALUES (CODE_1)
, (CODE_2)
, (CODE_3)
, (CODE_4)
, (CODE_5)
, (CODE_6)
, (CODE_7)
, (CODE_8)
, (CODE_9)
, (CODE_10)
) AS value(val)
WHERE value.val in ('123', '456')
) > 0 THEN 1 ELSE 0 END AS CODE_GROUP_1,
CASE WHEN (SELECT count(value.val)
FROM (VALUES (CODE_1)
, (CODE_2)
, (CODE_3)
, (CODE_4)
, (CODE_5)
, (CODE_6)
, (CODE_7)
, (CODE_8)
, (CODE_9)
, (CODE_10)
) AS value(val)
WHERE value.val in ('789','111','333','444','foo')
) > 0 THEN 1 ELSE 0 END AS CODE_GROUP_2
I am wondering if there is another way to do this that is more efficient. Is there a way to make a CLR UDF that takes an array of CODE_1-10, and outputs a set of columns CODE_GROUP_1-17?
You could at least avoid the repetition of FROM (VALUES ...) like this:
SELECT
CODE_GROUP_1 = COUNT(DISTINCT CASE WHEN val IN ('123', '456') THEN 1 END),
CODE_GROUP_2 = COUNT(DISTINCT CASE WHEN val IN ('789','111','333','444','foo') THEN 1 END),
...
FROM
(
VALUES
(CODE_1),
(CODE_2),
(CODE_3),
(CODE_4),
(CODE_5),
(CODE_6),
(CODE_7),
(CODE_8),
(CODE_9),
(CODE_10)
) AS value(val)
If CODE_1, CODE_2 etc. are column names, you can use the above query as a derived table in CROSS APPLY:
SELECT
...
FROM
dbo.atable -- table containing CODE_1, CODE_2 etc.
CROSS APPLY
(
SELECT ... -- the above query
) AS x
;
Can you create 2 new tables with the columns appended as rows? So one table would be dxCode with a source column if you need to retain the 1-10 value and the dx code and whatever key field(s) you need, the other table would be dxGroup with your 17 groups, the source groupID if you need it, and your target dx values.
Then to determine which codes are in which groups, you can join on your dx fields.
I was hoping someone perhaps could help. This problem was presented to me recently and I thought it would be easy, but (personally) found it a bit of a struggle. I can do it in Excel and SSRS - but I was curious if I was able to do it in SQL Server...
I would like to create a set of summary statistics (Max, Min) for a dataset. Easy enough... But I wanted to associate the corresponding date with those values.
Here is what my data looks like:
I have yearly data (not exactly - but beside the point) and I produce a pivoted summary like this using a series of CASE WHEN statements. This is fine - the output is seen on the right (above).
Each time I output this data - I like to provide a summary of the all the historic data (I only show the most recent data for sake of brevity). So... The question is how do I take an output like the one shown below (on different dates) and provide a summary data set like the one I have on the right?
So - a little background. I have already managed to join the Min and Max values using a UNION and that bit is fine. The tricky bit (I think) is how to form an INNER JOIN, using a sub query, with the Max or Min result values to return the corresponding Max or Min date, for each Type? Now it is highly likely that I am being a bit of an idiot and missing something obvious....but... Would really appreciate any help from anyone...
Many thanks in advance
This query will do the job, and for all TYPE
SELECT
Description, [CAR], [CAT], [MAT], [EAT], [PAR], [MAR], [FAR], [MOT], [LOT], [COT], [ROT]
FROM
(SELECT
unpvt.TYPE
,unpvt.Description
,unpvt.value
FROM (
SELECT
t.TYPE
,CONVERT(sql_variant,MAX(maxResult.MAX_RESULT)) as MAX_RESULT
,CONVERT(sql_variant,MIN(minResult.MIN_RESULT)) as MIN_RESULT
,CONVERT(sql_variant,MAX(CASE WHEN maxResult.MAX_RESULT IS NOT NULL THEN t.DATE ELSE NULL END)) as MAX_DATE
,CONVERT(sql_variant,MIN(CASE WHEN minResult.MIN_RESULT IS NOT NULL THEN t.DATE ELSE NULL END)) as MIN_DATE
FROM
table_name t -- You need to set your table name
LEFT JOIN (SELECT
TYPE
,MIN(RESULT) as MIN_RESULT
FROM
table_name -- You need to set your table name
GROUP BY
TYPE) minResult
on minResult.TYPE = t.TYPE
and minResult.MIN_RESULT = t.RESULT
LEFT JOIN (SELECT
TYPE
,MAX(RESULT) as MAX_RESULT
FROM
table_name -- You need to set your table name
GROUP BY
TYPE) maxResult
on maxResult.TYPE = t.TYPE
and maxResult.MAX_RESULT = t.RESULT
GROUP BY
t.TYPE) U
unpivot (
value
for Description in (MAX_RESULT, MIN_RESULT, MAX_DATE, MIN_DATE)
) unpvt) P
PIVOT
(
MAX(value)
FOR TYPE IN ([CAR], [CAT], [MAT], [EAT], [PAR], [MAR], [FAR], [MOT], [LOT], [COT], [ROT])
)AS PVT
DEMO : SQLFIDDLE
CONVERT(sql_variant, is a cast for columns to a common data type. This is a requirement of the UNPIVOT operator when you are running with subquery FROM.
It is possible to use the PIVOT command if your SQLServer is 2005 or better, but the raw data for the pivot need to be in a specific format, and the query I came up with is ugly
WITH minmax AS (
SELECT TYPE, RESULT, [date]
, row_number() OVER (partition BY TYPE ORDER BY TYPE, RESULT) a
, row_number() OVER (partition BY TYPE ORDER BY TYPE, RESULT DESC) d
FROM t)
SELECT info
, cam = CASE charindex('date', info)
WHEN 0 THEN cast(cast(cam AS int) AS varchar(50))
ELSE cast(cam AS varchar(50))
END
, car = CASE charindex('date', info)
WHEN 0 THEN cast(cast(car AS int) AS varchar(50))
ELSE cast(cam AS varchar(50))
END
, cat = CASE charindex('date', info)
WHEN 0 THEN cast(cast(cat AS int) AS varchar(50))
ELSE cast(cam AS varchar(50))
END
FROM (SELECT TYPE, 'maxres' info, RESULT value FROM minmax WHERE 1 = d
UNION ALL
SELECT TYPE, 'minres' info, RESULT value FROM minmax WHERE 1 = a
UNION ALL
SELECT TYPE, 'maxdate' info , [date] value FROM minmax WHERE 1 = d
UNION ALL
SELECT TYPE, 'mindate' info , [date] value FROM minmax WHERE 1 = a) DATA
PIVOT
(max(value) FOR TYPE IN ([CAM], [CAR], [CAT])) pvt
It's only a proof of concept so in SQLFiddle I have used a reducet set of fake data (3 row per 3 Type)
After the data preparation
SELECT TYPE, 'maxres' info, RESULT value FROM minmax WHERE 1 = d
UNION ALL
SELECT TYPE, 'minres' info, RESULT value FROM minmax WHERE 1 = a
UNION ALL
SELECT TYPE, 'maxdate' info , [date] value FROM minmax WHERE 1 = d
UNION ALL
SELECT TYPE, 'mindate' info , [date] value FROM minmax WHERE 1 = a
the value column is implicitly casted to the more complex datatype, in this case DateTime (you cannot have different data type in the same column), to see the data in the intended way an explicit cast is in needed, and is done with the CASE and CAST in
, cam = CASE charindex('date', info)
WHEN 0 THEN cast(cast(cam AS int) AS varchar(50))
ELSE cast(cam AS varchar(50))
END
the CASE check the data type, looking for the substring 'date' in the info column, then cast the row value back to INT for the minres and maxres column and in any case cast the value to varchar(50) to have the same data type again
UPDATE
With the sql_variant the CASE CAST block is not needed, thanks Ryx5
WITH minmax AS (
SELECT TYPE, RESULT, [date]
, row_number() OVER (partition BY TYPE ORDER BY TYPE, RESULT) a
, row_number() OVER (partition BY TYPE ORDER BY TYPE, RESULT DESC) d
FROM table_name)
SELECT info
, [CAM], [CAR], [CAT]
FROM (SELECT TYPE, 'maxres' info, cast(RESULT as sql_variant) value
FROM minmax WHERE 1 = d
UNION ALL
SELECT TYPE, 'minres' info, cast(RESULT as sql_variant) value
FROM minmax WHERE 1 = a
UNION ALL
SELECT TYPE, 'maxdate' info , cast([date] as sql_variant) value
FROM minmax WHERE 1 = d
UNION ALL
SELECT TYPE, 'mindate' info , cast([date] as sql_variant) value
FROM minmax WHERE 1 = a) DATA
PIVOT
(max(value) FOR TYPE IN ([CAM], [CAR], [CAT])) pvt
This is building on some code I got the other day (thanks to peterm). I am now trying to select the TOP X number of results after calculations on the query. The X can range from 1 to 8 depending on the number of results per player.
This is the code I have but I get a syntax error when I try to run it.
SELECT
PlayerID
, RoundID
, PlayedTo
, (SELECT Count(PlayerID) FROM PlayedToCalcs) AS C
, iif(
C <= 6
, 1
, iif(
C <= 8
, 2
, (
iif(
C <= 10
, 3
, (
iif(
C <= 12
, 4
, (
iif(
C <= 14
, 5
, (
iif(
C <= 16
, 6
, (
iif(
C <= 18
, 7
, (iif(C <= 20, 8, 999))
)
)
)
)
)
)
)
)
)
)
)
) AS X
FROM PlayedToCalcs AS s
WHERE PlayedTo IN (
SELECT TOP (X) PlayedTo
FROM PlayedToCalcs
WHERE PlayerID = s.PlayerID
ORDER BY PlayedTo DESC, RoundID DESC
)
ORDER BY PlayerID, PlayedTo DESC, RoundID DESC;
Here is a link http://sqlfiddle.com/#!3/a726c/4 with a small sample of the data I'm trying to use it on.
The Access db engine does not allow you to use a parameter for SELECT TOP. You must include a literal value in the SQL statement.
For example this query works correctly.
SELECT TOP 2 *
FROM tblFoo
ORDER BY id DESC;
But attempting to substitute a parameter, how_many, triggers error 3141, "The SELECT statement includes a reserved word or an argument name that is misspelled or missing, or the punctuation is incorrect."
SELECT TOP how_many *
FROM tblFoo
ORDER BY id DESC;
The reason being in SQL Server (the simulator you used in SQL Fiddle), you cannot use IIF. Try using CASE.
And there is a limitation of using 7 nested IIF in Access.
I have a TVF that returns two columns: 'measure' (a name) and 'score', a numeric score for that measure:
dbo.ScoringFunction(param1, param2, ..., paramN)
Measure Score
------- -----
measure1 10
measure2 5
... ...
measureN 15
I'm running this function against a large number of rows that contain its parameters:
Name Param1 Param2 ... ParamN
---- ------ ------ ------
Daniel 12 5 6
etc.
I am trying to find a way to display the measures and their scores next to the parameters that determine those scores:
Name Param1 Param2 ... ParamN measure1 measure2 ... measureN
---- ------ ------ ------ -------- -------- --------
Daniel 12 5 6 10 5 15
etc.
So far I have tried using a pivot table, but it's tricky since the data being pivoted is contained in a TVF rather than a static table. I've also tried using CROSS APPLY, but once I have the data (measures & scores), I'm still unable to pivot it into a nicely formatted row.
If anybody has any ideas, they would be much appreciated!
Without changing too much (hopefully), I would do the pivoting in the function, then use the function in CROSS APPLY and pull the columns. So if your function is something like this:
CREATE FUNCTION dbo.ScoringFunction (parameters)
RETURNS TABLE
RETURN (
SELECT Measure, Score
FROM …
)
then I would rewrite it like this:
CREATE FUNCTION dbo.ScoringFunction (parameters)
RETURNS TABLE
RETURN (
WITH originalSelect AS (
SELECT Measure, Score
FROM …
)
SELECT
measure1,
measure2,
…
FROM originalSelect
PIVOT (
MAX(Score) FOR Measure IN (measure1, measure2, …)
) p
)
and use it in the final query like this:
SELECT
t.Name,
t.param1,
t.param2,
…
x.measure1,
x.measure2,
…
FROM atable t
CROSS APPLY dbo.ScoringFunction (t.param1, t.param2, …) x
If you make a function that looks like this:
CREATE FUNCTION [dbo].[fGetSpecificMeasures]
(
#HeightScore INT
, #WeightScore INT
, #TvScore INT
)
RETURNS TABLE
RETURN
(
SELECT
Final.Height AS tv_height_score
, Final.[Weight] AS tv_weight_score
, Final.TV AS tv_score
FROM
(
SELECT measure, score FROM ScoringRubric WHERE measure = 'Height' AND #HeightScore BETWEEN bottom_of_range AND top_of_range
UNION ALL
SELECT measure, score FROM ScoringRubric WHERE measure = 'Weight' AND #WeightScore BETWEEN bottom_of_range AND top_of_range
UNION ALL
SELECT measure, score FROM ScoringRubric WHERE measure = 'TV' AND #TvScore BETWEEN bottom_of_range AND top_of_range
) Base
PIVOT
(
MAX(score)
FOR measure
IN
(
[Height]
, [Weight]
, [TV]
)
) Final
);
GO
And one that looks like this:
CREATE FUNCTION [dbo].[fGetMeasureScore]
(
#Measure VARCHAR(50)
, #Value INT
)
RETURNS TABLE
RETURN
(
SELECT
score
FROM ScoringRubric
WHERE measure = #Measure
AND #Value BETWEEN bottom_of_range AND top_of_range
);
GO
Then you can get your data with either of the following:
DECLARE #User VARCHAR(50) = 'Daniel'
SELECT
UserProfile.*
, HeightScore.score AS tv_height_score
, WeightScore.score AS tv_weight_score
, TvScore.score AS tv_score
FROM UserProfile
INNER JOIN ScoringRubric HeightScore
ON HeightScore.measure = 'Height'
AND UserProfile.height BETWEEN HeightScore.bottom_of_range AND HeightScore.top_of_range
INNER JOIN ScoringRubric WeightScore
ON WeightScore.measure = 'Weight'
AND UserProfile.[weight] BETWEEN WeightScore.bottom_of_range AND WeightScore.top_of_range
INNER JOIN ScoringRubric TvScore
ON TvScore.measure = 'TV'
AND UserProfile.TV BETWEEN TvScore.bottom_of_range AND TvScore.top_of_range
WHERE UserProfile.name = #User
SELECT
*
FROM UserProfile
CROSS APPLY dbo.fGetSpecificMeasures(height, [weight], TV)
WHERE name = #User
SELECT
UP.*
, HeightScore.score AS tv_height_score
, WeightScore.score AS tv_weight_score
, TvScore.score AS tv_score
FROM UserProfile UP
CROSS APPLY fGetMeasureScore('Height', UP.height) HeightScore
CROSS APPLY fGetMeasureScore('Weight', UP.[weight]) WeightScore
CROSS APPLY fGetMeasureScore('TV', UP.TV) TvScore
WHERE UP.name = #User
I don't really know which one you'll find most appropriate for your uses. Let me know if you have questions.
As for your original question, if this were the function:
CREATE FUNCTION [dbo].[fGetMeasureScoresOriginal]
(
#HeightScore INT
, #WeightScore INT
, #TvScore INT
)
RETURNS TABLE
RETURN
(
SELECT measure, score FROM ScoringRubric WHERE measure = 'Height' AND #HeightScore BETWEEN bottom_of_range AND top_of_range
UNION ALL
SELECT measure, score FROM ScoringRubric WHERE measure = 'Weight' AND #WeightScore BETWEEN bottom_of_range AND top_of_range
UNION ALL
SELECT measure, score FROM ScoringRubric WHERE measure = 'TV' AND #TvScore BETWEEN bottom_of_range AND top_of_range
)
GO
Then you could write the query and pivot like so:
SELECT
Final.name
, Final.OriginalHeight AS height
, Final.OriginalWeight AS [weight]
, Final.OriginalTv AS TV
, Final.Height AS tv_height_score
, Final.[Weight] AS tv_weight_score
, Final.TV AS tv_score
FROM
(
SELECT
UP.name
, UP.height AS OriginalHeight
, UP.[weight] AS OriginalWeight
, UP.TV AS OriginalTv
, Measures.measure
, Measures.score
FROM UserProfile UP
CROSS APPLY dbo.fGetMeasureScoresOriginal(UP.height, UP.[weight], UP.TV) Measures
WHERE UP.name = #User
) Base
PIVOT
(
MAX(score)
FOR measure
IN
(
[Height]
, [Weight]
, [TV]
)
) Final
EDIT: Just realized I didn't answer the original question. Adding that now.
If you want to pivot a static set of rows you can use the technique as I described here.
Reposting the example, so it will be different tables, but the technique can be applied in your case as well I think.
SELECT
Customers.CustID,
MAX(CASE WHEN CF.FieldID = 1 THEN CF.FieldValue ELSE NULL END) AS Field1,
MAX(CASE WHEN CF.FieldID = 2 THEN CF.FieldValue ELSE NULL END) AS Field2,
MAX(CASE WHEN CF.FieldID = 3 THEN CF.FieldValue ELSE NULL END) AS Field3,
MAX(CASE WHEN CF.FieldID = 4 THEN CF.FieldValue ELSE NULL END) AS Field4
-- Add more...
FROM Customers
LEFT OUTER JOIN CustomFields CF
ON CF.ID = Customers.CustID
WHERE Customers.CustName like 'C%'
GROUP BY Customers.CustID
I have a query which is taking some serious time to execute on anything older than the past, say, hours worth of data. This is going to create a view which will be used for datamining, so the expectations are that it would be able to search back weeks or months of data and return in a reasonable amount of time (even a couple minutes is fine... I ran for a date range of 10/3/2011 12:00pm to 10/3/2011 1:00pm and it took 44 minutes!)
The problem is with the two LEFT OUTER JOINs in the bottom. When I take those out, it can run in about 10 seconds. However, those are the bread and butter of this query.
This is all coming from one table. The ONLY thing this query returns differently than the original table is the column xweb_range. xweb_range is a calculated field column (range) which will only use the values from [LO,LC,RO,RC]_Avg where their corresponding [LO,LC,RO,RC]_Sensor_Alarm = 0 (do not include in range calculation if sensor alarm = 1)
WITH Alarm (sub_id,
LO_Avg, LO_Sensor_Alarm, LC_Avg, LC_Sensor_Alarm, RO_Avg, RO_Sensor_Alarm, RC_Avg, RC_Sensor_Alarm) AS (
SELECT sub_id, LO_Avg, LO_Sensor_Alarm, LC_Avg, LC_Sensor_Alarm, RO_Avg, RO_Sensor_Alarm, RC_Avg, RC_Sensor_Alarm
FROM dbo.some_table
where sub_id <> '0'
)
, AddRowNumbers AS (
SELECT rowNumber = ROW_NUMBER() OVER (ORDER BY LO_Avg)
, sub_id
, LO_Avg, LO_Sensor_Alarm
, LC_Avg, LC_Sensor_Alarm
, RO_Avg, RO_Sensor_Alarm
, RC_Avg, RC_Sensor_Alarm
FROM Alarm
)
, UnPivotColumns AS (
SELECT rowNumber, value = LO_Avg FROM AddRowNumbers WHERE LO_Sensor_Alarm = 0
UNION ALL SELECT rowNumber, LC_Avg FROM AddRowNumbers WHERE LC_Sensor_Alarm = 0
UNION ALL SELECT rowNumber, RO_Avg FROM AddRowNumbers WHERE RO_Sensor_Alarm = 0
UNION ALL SELECT rowNumber, RC_Avg FROM AddRowNumbers WHERE RC_Sensor_Alarm = 0
)
SELECT rowNumber.sub_id
, cds.equipment_id
, cds.read_time
, cds.LC_Avg
, cds.LC_Dev
, cds.LC_Ref_Gap
, cds.LC_Sensor_Alarm
, cds.LO_Avg
, cds.LO_Dev
, cds.LO_Ref_Gap
, cds.LO_Sensor_Alarm
, cds.RC_Avg
, cds.RC_Dev
, cds.RC_Ref_Gap
, cds.RC_Sensor_Alarm
, cds.RO_Avg
, cds.RO_Dev
, cds.RO_Ref_Gap
, cds.RO_Sensor_Alarm
, COALESCE(range1.range, range2.range) AS xweb_range
FROM AddRowNumbers rowNumber
LEFT OUTER JOIN (SELECT rowNumber, range = MAX(value) - MIN(value) FROM UnPivotColumns GROUP BY rowNumber HAVING COUNT(*) > 1) range1 ON range1.rowNumber = rowNumber.rowNumber
LEFT OUTER JOIN (SELECT rowNumber, range = AVG(value) FROM UnPivotColumns GROUP BY rowNumber HAVING COUNT(*) = 1) range2 ON range2.rowNumber = rowNumber.rowNumber
INNER JOIN dbo.some_table cds
ON rowNumber.sub_id = cds.sub_id
It's difficult to understand exactly what your query is trying to do without knowing the domain. However, it seems to me like your query is simply trying to find, for each row in dbo.some_table where sub_id is not 0, the range of the following columns in the record (or, if only one matches, that single value):
LO_AVG when LO_SENSOR_ALARM=0
LC_AVG when LC_SENSOR_ALARM=0
RO_AVG when RO_SENSOR_ALARM=0
RC_AVG when RC_SENSOR_ALARM=0
You constructed this query assigning each row a sequential row number, unpivoted the _AVG columns along with their row number, computed the range aggregate grouping by row number and then joining back to the original records by row number. CTEs don't materialize results (nor are they indexed, as discussed in the comments). So each reference to AddRowNumbers is expensive, because ROW_NUMBER() OVER (ORDER BY LO_Avg) is a sort.
Instead of cutting this table up just to join it back together by row number, why not do something like:
SELECT cds.sub_id
, cds.equipment_id
, cds.read_time
, cds.LC_Avg
, cds.LC_Dev
, cds.LC_Ref_Gap
, cds.LC_Sensor_Alarm
, cds.LO_Avg
, cds.LO_Dev
, cds.LO_Ref_Gap
, cds.LO_Sensor_Alarm
, cds.RC_Avg
, cds.RC_Dev
, cds.RC_Ref_Gap
, cds.RC_Sensor_Alarm
, cds.RO_Avg
, cds.RO_Dev
, cds.RO_Ref_Gap
, cds.RO_Sensor_Alarm
--if the COUNT is 0, xweb_range will be null (since MAX will be null), if it's 1, then use MAX, else use MAX - MIN (as per your example)
, (CASE WHEN stats.[Count] < 2 THEN stats.[MAX] ELSE stats.[MAX] - stats.[MIN] END) xweb_range
FROM dbo.some_table cds
--cross join on the following table derived from values in cds - it will always contain 1 record per row of cds
CROSS APPLY
(
SELECT COUNT(*), MIN(Value), MAX(Value)
FROM
(
--construct a table using the column values from cds we wish to aggregate
VALUES (LO_AVG, LO_SENSOR_ALARM),
(LC_AVG, LC_SENSOR_ALARM),
(RO_AVG, RO_SENSORALARM),
(RC_AVG, RC_SENSOR_ALARM)
) x (Value, Sensor_Alarm) --give a name to the columns for _AVG and _ALARM
WHERE Sensor_Alarm = 0 --filter our constructed table where _ALARM=0
) stats([Count], [Min], [Max]) --give our derived table and its columns some names
WHERE cds.sub_id <> '0' --this is a filter carried over from the first CTE in your example