The main problem is that you can have chains of overlapping entries, so you need to combine an indefinite amount of times to remove all the overlap - this is more suited to a procedural method than SQL. But if you would prefer to not use temporary tables, here's a CTE method - keep in mind that CTEs can only recurse a given number of times, so if you have any particularly long chains, it will fail.
WITH MergedAvailable
AS
(
SELECT Available.UserID, Available.AvailStart, MAX(Available.AvailEnd) AS AvailEnd
FROM Available
WHERE (
SELECT COUNT(*)
FROM Available AS InnerAvailable
WHERE InnerAvailable.AvailStart < Available.AvailStart
AND
InnerAvailable.AvailEnd >= Available.AvailStart
) = 0
GROUP BY Available.UserID, Available.AvailStart
UNION ALL
SELECT MergedAvailable.UserID, MergedAvailable.AvailStart,
LongestExtensionToAvailableInterval.NewIntervalEnd
FROM MergedAvailable
CROSS APPLY GetLongestExtensionToAvailableInterval(MergedAvailable.UserID,
MergedAvailable.AvailStart,
MergedAvailable.AvailEnd) AS LongestExtensionToAvailableInterval
WHERE LongestExtensionToAvailableInterval.NewIntervalEnd IS NOT NULL
)
SELECT SUM(DATEDIFF(MINUTE,
FinalAvailable.AvailStart,
FinalAvailable.AvailEnd)) AS MinsAvailable
FROM (
SELECT MergedAvailable.UserID, MergedAvailable.AvailStart,
MAX(MergedAvailable.AvailEnd) AS AvailEnd
FROM MergedAvailable
GROUP BY MergedAvailable.UserID, MergedAvailable.AvailStart
) AS FinalAvailable
This table function is required:
CREATE FUNCTION GetLongestExtensionToAvailableInterval
(
@UserID int,
@CurrentIntervalStart datetime,
@CurrentIntervalEnd datetime
)
RETURNS TABLE
AS
RETURN
SELECT MAX(Available.AvailEnd) AS NewIntervalEnd
FROM Available
WHERE Available.UserID = @UserID
AND
Available.AvailStart > @CurrentIntervalStart
AND
Available.AvailStart <= @CurrentIntervalEnd
AND
Available.AvailEnd > @CurrentIntervalEnd
The general idea is that it starts from all ranges where the start of the range isn't overlapping anything, and then with every recursion it extends the current range to the furthest extent of the currently overlapping ranges. The table function is needed to determine the furthest extent, as recursing sections of CTEs are not allowed to included plain aggregates.
With the data you've provided, the starting rows are:
456 2012-11-19 16:00 2012-11-19 17:10
456 2012-11-19 17:30 2012-11-19 18:10
The only row which ends up being added via the recursion is:
456 2012-11-19 17:30 2012-11-19 18:30
For the sake of the example, say you had a row with ID 7 which went from 18:20 to 19:20. Then there would be a second recursion which brought back the row:
456 2012-11-19 17:30 2012-11-19 19:20
So while the query will get to the start and end of each overlapping range, it will also be bringing back all the intermediate stages. This is why we need to take the aggregate maximum end date for each start date after the CTE, to remove them.