Вычислить медиану столбца В общем табличном выражении SQL


В MSSQL2008 я пытаюсь вычислить медиану столбца чисел из общего табличного выражения, используя классический запрос медианы следующим образом:

WITH cte AS
(
   SELECT number
   FROM table
) 

SELECT cte.*,
(SELECT 
  (SELECT (   
    (SELECT TOP 1 cte.number  
     FROM     
     (SELECT TOP 50 PERCENT cte.number     
      FROM cte
      ORDER BY cte.number) AS medianSubquery1   
    ORDER BY cte.number DESC)  
    +   
  (SELECT TOP 1 cte.number
   FROM     
    (SELECT TOP 50 PERCENT cte.number    
     FROM cte   
     ORDER BY cte.number DESC) AS medianSubquery2   
   ORDER BY cte.number ASC) ) / 2)) AS median

FROM cte
ORDER BY cte.number

Результирующий набор, который я получаю, следующий:

NUMBER    MEDIAN
x1        x1
x1        x1
x1        x1
x2        x2
x3        x3
Другими словами, столбец " медиана "такой же, как и столбец" число", когда я ожидал бы, что средний столбец будет" x1 " полностью вниз. Я использую аналогичное выражение для вычисления режима, и оно отлично работает над тем же самым общим табличным выражением.
3 2

3 ответа:

Проблема с вашим запросом заключается в том, что вы делаете

SELECT TOP 1 cte.number FROM...

Но он не коррелирует с подзапросом, он коррелирует с внешним запросом, поэтому подзапрос не имеет значения. Это объясняет, почему вы просто получаете одно и то же значение на всем пути вниз. Удаление cte. (как показано ниже) дает медиану CTE. Что является постоянной величиной. Что ты пытаешься сделать?

WITH cte AS
    ( SELECT NUMBER
    FROM master.dbo.spt_values
    WHERE TYPE='p'
    )

SELECT cte.*,
(SELECT 
  (SELECT (   
    (SELECT TOP 1 number  
     FROM     
     (SELECT TOP 50 PERCENT cte.number     
      FROM cte
      ORDER BY cte.number) AS medianSubquery1   
    ORDER BY number DESC)  
    +   
  (SELECT TOP 1 number
   FROM     
    (SELECT TOP 50 PERCENT cte.number    
     FROM cte   
     ORDER BY cte.number DESC) AS medianSubquery2   
   ORDER BY number ASC) ) / 2)) AS median
FROM cte
ORDER BY cte.number

Возвращает

NUMBER      median
----------- -----------
0           1023
1           1023
2           1023
3           1023
4           1023
5           1023
6           1023
7           1023

Вот немного другой способ сделать это:

WITH cte AS
(
   SELECT number
   FROM table1
)
SELECT T1.number, T3.median
FROM cte T1, 
(
    SELECT AVG(number) AS median
    FROM
    (
        SELECT number, ROW_NUMBER() OVER(ORDER BY number) AS rn
        FROM cte
    ) T2
    WHERE T2.rn = ((SELECT COUNT(*) FROM table1) + 1) / 2
    OR T2.rn = ((SELECT COUNT(*) FROM table1) + 2) / 2
) T3

Это не совсем новый ответ, поскольку он в основном расширяет ответ Марка Байера, но есть несколько вариантов для дальнейшего упрощения запроса.

Первое-это действительно использовать CTE. не только у вас может быть несколько CTE, но они могут ссылаться друг на друга. Имея это в виду, мы можем создать дополнительный CTE для вычисления медианы на основе результатов первого. Это инкапсулирует медианное вычисление и оставляет фактический выбор делать только то, что ему нужно делать. Обратите внимание, что ROW_NUMBER() должен был быть перемещен в первый CTE.

;WITH cte AS
(
   SELECT number, ROW_NUMBER() OVER(ORDER BY number) AS rn
   FROM table1
),
med AS
(
    SELECT AVG(number) AS median
    FROM cte
    WHERE cte.rn = ((SELECT COUNT(*) FROM cte) + 1) / 2
    OR cte.rn = ((SELECT COUNT(*) FROM cte) + 2) / 2
)
SELECT cte.number, med.median
FROM cte
CROSS JOIN med

И чтобы еще больше снизить сложность, вы "можете" использовать пользовательский агрегат CLR для обработки медианы (например, тот, который предоставляется в бесплатной библиотеке SQL# в http://www.SQLsharp.com/ [автором которого являюсь я]).

;WITH cte AS
(
   SELECT number
   FROM table1
),
med AS
(
    SELECT  SQL#.Agg_Median(cte.number) AS median
    FROM    cte
)
SELECT cte.number, med.median
FROM cte
CROSS JOIN med