Простой способ рассчитать медиану с MySQL


208

Какой самый простой (и, надеюсь, не слишком медленный) способ расчета медианы с MySQL? Я использовал AVG(x)для нахождения среднего значения, но мне трудно найти простой способ вычисления медианы. Сейчас я возвращаю все строки в PHP, выполняю сортировку, а затем выбираю среднюю строку, но наверняка должен быть какой-то простой способ сделать это в одном запросе MySQL.

Пример данных:

id | val
--------
 1    4
 2    7
 3    2
 4    2
 5    9
 6    8
 7    3

Сортировка по valдает 2 2 3 4 7 8 9, поэтому медиана должна быть 4, по сравнению с SELECT AVG(val)которой == 5.


72
меня тошнит от того, что в MySQL нет функции для вычисления медианы? Смешной.
Моника Хеднек

3
MariaDB начиная с версии 10.3, см. Mariadb.com/kb/en/library/median
berturion

Ответы:


225

В MariaDB / MySQL:

SELECT AVG(dd.val) as median_val
FROM (
SELECT d.val, @rownum:=@rownum+1 as `row_number`, @total_rows:=@rownum
  FROM data d, (SELECT @rownum:=0) r
  WHERE d.val is NOT NULL
  -- put some where clause here
  ORDER BY d.val
) as dd
WHERE dd.row_number IN ( FLOOR((@total_rows+1)/2), FLOOR((@total_rows+2)/2) );

Стив Коэн отмечает, что после первого прохода @rownum будет содержать общее количество строк. Это можно использовать для определения медианы, поэтому второй проход или соединение не требуется.

Также AVG(dd.val)и dd.row_number IN(...)используется для правильного получения медианы, когда существует четное количество записей. Обоснование:

SELECT FLOOR((3+1)/2),FLOOR((3+2)/2); -- when total_rows is 3, avg rows 2 and 2
SELECT FLOOR((4+1)/2),FLOOR((4+2)/2); -- when total_rows is 4, avg rows 2 and 3

Наконец, MariaDB 10.3.3+ содержит функцию MEDIAN


4
любой способ сделать это, чтобы показать значения группы? как: место / медиана для этого места ... как выбрать место, медиана_значение из таблицы ... в любом случае? спасибо
saulob

2
@rowNum будет иметь «общее количество» в конце выполнения. Таким образом, вы можете использовать это, если хотите избежать повторного «подсчета всех» (что было в моем случае, потому что мой запрос не был таким простым)
Ахмед-Анас

Логика одного утверждения: (floor ((total_rows + 1) / 2), floor ((total_rows + 2) / 2)) вычислить строки, необходимые для медианы, потрясающая! Не знаю, как вы об этом подумали, но это блестяще. Часть, которой я не следую, - это (SELECT @rownum: = 0) r - какой цели это служит?
Шанемейстер

измените первое WHERE 1на WHERE d.val IS NOT NULLтак, чтобы исключить NULLстроки, чтобы этот метод был выровнен с роднымAVG
chiliNUT

1
Мое значение пришло из объединения двух таблиц, поэтому мне пришлось добавить еще один подзапрос, чтобы убедиться, что после объединения порядок строк был правильным! Структура была своего родаselect avg(value) from (select value, row_number from (select a - b as value from a_table join b_table order by value))
Даниэль Бакмастер

62

Я просто нашел другой ответ онлайн в комментариях :

Для медиан практически в любом SQL:

SELECT x.val from data x, data y
GROUP BY x.val
HAVING SUM(SIGN(1-SIGN(y.val-x.val))) = (COUNT(*)+1)/2

Убедитесь, что ваши столбцы хорошо проиндексированы, а индекс используется для фильтрации и сортировки. Проверьте с планами объяснения.

select count(*) from table --find the number of rows

Рассчитайте «средний» номер строки. Возможно использовать: median_row = floor(count / 2).

Затем выберите его из списка:

select val from table order by val asc limit median_row,1

Это должно вернуть вам одну строку только с тем значением, которое вы хотите.

Иаков


6
@ Роб, можешь помочь, пожалуйста? Или я должен просто поклониться решению на липучке? (на самом деле не уверен, как отложить до другого решения) Спасибо, Джейкоб
TheJacobTaylor

1
Обратите внимание, что он выполняет «перекрестное соединение», которое очень медленно для больших таблиц.
Рик Джеймс

1
Этот ответ ничего не возвращает для четного числа строк.
kuttumiah

Этот ответ не работает вообще для некоторых наборов данных, например, тривиального набора данных со значениями 0,1, 0,1, 0,1, 2 - он будет работать, если все значения различны, но работает только, если значения
Kem Mason

32

Я обнаружил, что принятое решение не работает в моей установке MySQL, возвращая пустой набор, но этот запрос работал для меня во всех ситуациях, в которых я его тестировал:

SELECT x.val from data x, data y
GROUP BY x.val
HAVING SUM(SIGN(1-SIGN(y.val-x.val)))/COUNT(*) > .5
LIMIT 1

1
абсолютно правильно, работает отлично и очень быстро на моих проиндексированных таблицах
Роб

2
Похоже, что это самое быстрое решение для mysql из всех приведенных здесь ответов, 200 мс с почти миллионом записей в таблице
Роб

3
@FrankConijn: он выбирает из одной таблицы дважды. Имя таблицы - dataи оно используется с двумя именами, xи y.
Брайан

3
просто сказать, что я затормозил свой mysqld этим точным запросом к таблице с 33k строками ...
Ксенонит

1
Этот запрос возвращает неправильный ответ для четного числа строк.
kuttumiah

26

К сожалению, ни ответы TheJacobTaylor, ни velcrow не дают точных результатов для текущих версий MySQL.

Ответ липучки сверху близок, но он не рассчитывается правильно для наборов результатов с четным числом строк. Медианы определяются как 1) среднее число на наборах с нечетными номерами, или 2) среднее двух средних чисел на наборах с нечетными числами.

Итак, вот решение Velcro, исправленное для обработки как нечетных, так и четных наборов чисел:

SELECT AVG(middle_values) AS 'median' FROM (
  SELECT t1.median_column AS 'middle_values' FROM
    (
      SELECT @row:=@row+1 as `row`, x.median_column
      FROM median_table AS x, (SELECT @row:=0) AS r
      WHERE 1
      -- put some where clause here
      ORDER BY x.median_column
    ) AS t1,
    (
      SELECT COUNT(*) as 'count'
      FROM median_table x
      WHERE 1
      -- put same where clause here
    ) AS t2
    -- the following condition will return 1 record for odd number sets, or 2 records for even number sets.
    WHERE t1.row >= t2.count/2 and t1.row <= ((t2.count/2) +1)) AS t3;

Чтобы использовать это, выполните следующие 3 простых шага:

  1. Замените "median_table" (2 вхождения) в приведенном выше коде именем вашей таблицы
  2. Замените "median_column" (3 вхождения) на имя столбца, для которого вы хотите найти медиану
  3. Если у вас есть условие WHERE, замените «WHERE 1» (2 вхождения) на условие where.

И что вы делаете для медианы строковых значений?
Рик Джеймс

12

Я предлагаю более быстрый способ.

Получить количество строк:

SELECT CEIL(COUNT(*)/2) FROM data;

Затем возьмите среднее значение в отсортированном подзапросе:

SELECT max(val) FROM (SELECT val FROM data ORDER BY val limit @middlevalue) x;

Я проверил это с набором случайных чисел 5x10e6, и он найдет медиану менее чем за 10 секунд.


3
Почему бы и нет: ВЫБЕРИТЕ val ОТ ДАННЫХ ORDER BY val limit @middlevalue, 1
Брайан

1
Как вы выводите переменный вывод вашего первого блока кода во второй блок кода?
Поездка

3
Как и откуда @middlevalue?
Поездка

@ Брайан - я согласен с тобой, это имеет для меня гораздо больше смысла. Вы когда-нибудь находили причину этого не делать?
Шейн N

5
Это не работает, так как переменная не может быть использована в предложении limit.
codepk

8

Комментарий к этой странице в документации MySQL содержит следующее предложение:

-- (mostly) High Performance scaling MEDIAN function per group
-- Median defined in http://en.wikipedia.org/wiki/Median
--
-- by Peter Hlavac
-- 06.11.2008
--
-- Example Table:

DROP table if exists table_median;
CREATE TABLE table_median (id INTEGER(11),val INTEGER(11));
COMMIT;


INSERT INTO table_median (id, val) VALUES
(1, 7), (1, 4), (1, 5), (1, 1), (1, 8), (1, 3), (1, 6),
(2, 4),
(3, 5), (3, 2),
(4, 5), (4, 12), (4, 1), (4, 7);



-- Calculating the MEDIAN
SELECT @a := 0;
SELECT
id,
AVG(val) AS MEDIAN
FROM (
SELECT
id,
val
FROM (
SELECT
-- Create an index n for every id
@a := (@a + 1) mod o.c AS shifted_n,
IF(@a mod o.c=0, o.c, @a) AS n,
o.id,
o.val,
-- the number of elements for every id
o.c
FROM (
SELECT
t_o.id,
val,
c
FROM
table_median t_o INNER JOIN
(SELECT
id,
COUNT(1) AS c
FROM
table_median
GROUP BY
id
) t2
ON (t2.id = t_o.id)
ORDER BY
t_o.id,val
) o
) a
WHERE
IF(
-- if there is an even number of elements
-- take the lower and the upper median
-- and use AVG(lower,upper)
c MOD 2 = 0,
n = c DIV 2 OR n = (c DIV 2)+1,

-- if its an odd number of elements
-- take the first if its only one element
-- or take the one in the middle
IF(
c = 1,
n = 1,
n = c DIV 2 + 1
)
)
) a
GROUP BY
id;

-- Explanation:
-- The Statement creates a helper table like
--
-- n id val count
-- ----------------
-- 1, 1, 1, 7
-- 2, 1, 3, 7
-- 3, 1, 4, 7
-- 4, 1, 5, 7
-- 5, 1, 6, 7
-- 6, 1, 7, 7
-- 7, 1, 8, 7
--
-- 1, 2, 4, 1

-- 1, 3, 2, 2
-- 2, 3, 5, 2
--
-- 1, 4, 1, 4
-- 2, 4, 5, 4
-- 3, 4, 7, 4
-- 4, 4, 12, 4


-- from there we can select the n-th element on the position: count div 2 + 1 

ИМХО, это однозначно лучше всего подходит для ситуаций, когда вам нужна медиана из сложного подмножества (мне нужно было рассчитать отдельные медианы большого количества подмножеств данных)
mblackwell8

У меня отлично работает. 5.6.14. MySQL Community Server. Таблица с 11M записями (около 20Gb на диске), имеет два неосновных индекса (model_id, price). В таблице (после фильтрации) у нас есть 500K записей для расчета медианы. В результате мы имеем 30K записей (model_id, median_price). Продолжительность запроса составляет 1,5-2 секунды. Скорость быстрая для меня.
Микл

8

Установите и используйте эти статистические функции mysql: http://www.xarg.org/2012/07/statistical-functions-in-mysql/

После этого рассчитать медиану легко:

SELECT median(val) FROM data;

1
Я только что попробовал это сам, и для чего бы это ни стоило, установка была супер быстрой / простой, и она работала так, как рекламировалось, включая группировку, например, «выберите имя, медиану (x) ОТ группы t1 по имени» - источник github здесь: github.com/infusion/udf_infusion
Кем Мейсон

6

Большинство из приведенных выше решений работают только для одного поля таблицы, вам может потребоваться получить медиану (50-й процентиль) для многих полей в запросе.

Я использую это:

SELECT CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(
 GROUP_CONCAT(field_name ORDER BY field_name SEPARATOR ','),
  ',', 50/100 * COUNT(*) + 1), ',', -1) AS DECIMAL) AS `Median`
FROM table_name;

Вы можете заменить «50» в приведенном выше примере на любой процентиль, это очень эффективно.

Просто убедитесь, что у вас достаточно памяти для GROUP_CONCAT, вы можете изменить это с помощью:

SET group_concat_max_len = 10485760; #10MB max length

Более подробная информация: http://web.performancerasta.com/metrics-tips-calculation-95th-99th-or-any-percentile-with-single-mysql-query/


Помните: для четного числа значений требуется более высокое из двух средних значений. Для числа шансов значения принимает следующее более высокое значение после медианы.
Джордано

6

У меня есть этот код ниже, который я нашел на HackerRank, и он довольно прост и работает в каждом конкретном случае.

SELECT M.MEDIAN_COL FROM MEDIAN_TABLE M WHERE  
  (SELECT COUNT(MEDIAN_COL) FROM MEDIAN_TABLE WHERE MEDIAN_COL < M.MEDIAN_COL ) = 
  (SELECT COUNT(MEDIAN_COL) FROM MEDIAN_TABLE WHERE MEDIAN_COL > M.MEDIAN_COL );

2
Я считаю, что это работает только с таблицей, которая имеет количество записей нечетно. Для четного количества записей это может иметь проблему.
Я. Чанг

4

Построение ответа на липучке, для тех из вас, кто должен сделать медиану из чего-то, что сгруппировано по другому параметру:

ВЫБЕРИТЕ grp_field , t1 . val FROM ( SELECT SELECT @ rownum : = 0 , 
    grp_field , @ rownum : = IF (@ s = grp_field , @ rownum + 1 , 0 ) AS , @ s : = IF (@ s = grp_field , @ s , grp_field ) AS sec , d . val
   ИЗ данных д , (      row_number
          @ s : = 0 ) г
   ORDER BY grp_field , д . val
 ) как t1 JOIN ( ВЫБЕРИТЕ grp_field , считать (*) как total_rows
   ОТ данных d
   GROUP BY grp_field
 ) как t2
 ON t1 . grp_field = t2 . grp_field
 ГДЕ t1 . row_number   
     = этаж ( total_rows / 2 ) +1 ;


3

Вы можете использовать пользовательскую функцию, найденную здесь .


3
Это выглядит наиболее полезным, но я не хочу устанавливать нестабильную альфа-
версию

6
Так что изучите их источники для интересующей функции, исправьте их или измените их по мере необходимости и установите «свою» стабильную и не альфа-версию, как только вы это сделаете - чем это может быть хуже, чем аналогичная настройка менее проверенных предложений кода ты получаешь ТАК? -)
Алекс Мартелли

3

Заботится о количестве нечетных значений - в этом случае дает среднее из двух значений в середине.

SELECT AVG(val) FROM
  ( SELECT x.id, x.val from data x, data y
      GROUP BY x.id, x.val
      HAVING SUM(SIGN(1-SIGN(IF(y.val-x.val=0 AND x.id != y.id, SIGN(x.id-y.id), y.val-x.val)))) IN (ROUND((COUNT(*))/2), ROUND((COUNT(*)+1)/2))
  ) sq

2

Мой код, работающий без таблиц или дополнительных переменных:

SELECT
((SUBSTRING_INDEX(SUBSTRING_INDEX(group_concat(val order by val), ',', floor(1+((count(val)-1) / 2))), ',', -1))
+
(SUBSTRING_INDEX(SUBSTRING_INDEX(group_concat(val order by val), ',', ceiling(1+((count(val)-1) / 2))), ',', -1)))/2
as median
FROM table;

3
Это не удастся для любого существенного объема данных, потому что GROUP_CONCATограничен 1023 символами, даже если используется внутри другой функции, подобной этой.
Роб Ван Дам

2

При желании вы также можете сделать это в хранимой процедуре:

DROP PROCEDURE IF EXISTS median;
DELIMITER //
CREATE PROCEDURE median (table_name VARCHAR(255), column_name VARCHAR(255), where_clause VARCHAR(255))
BEGIN
  -- Set default parameters
  IF where_clause IS NULL OR where_clause = '' THEN
    SET where_clause = 1;
  END IF;

  -- Prepare statement
  SET @sql = CONCAT(
    "SELECT AVG(middle_values) AS 'median' FROM (
      SELECT t1.", column_name, " AS 'middle_values' FROM
        (
          SELECT @row:=@row+1 as `row`, x.", column_name, "
          FROM ", table_name," AS x, (SELECT @row:=0) AS r
          WHERE ", where_clause, " ORDER BY x.", column_name, "
        ) AS t1,
        (
          SELECT COUNT(*) as 'count'
          FROM ", table_name, " x
          WHERE ", where_clause, "
        ) AS t2
        -- the following condition will return 1 record for odd number sets, or 2 records for even number sets.
        WHERE t1.row >= t2.count/2
          AND t1.row <= ((t2.count/2)+1)) AS t3
    ");

  -- Execute statement
  PREPARE stmt FROM @sql;
  EXECUTE stmt;
END//
DELIMITER ;


-- Sample usage:
-- median(table_name, column_name, where_condition);
CALL median('products', 'price', NULL);

Спасибо за это! Пользователь должен знать, что пропущенные значения (NULL) рассматриваются как значения. чтобы избежать этой проблемы, добавьте 'x IS NOT NULL, где условие.
Джордано

1
@giordano В какую строку кода x IS NOT NULLнужно добавить?
Пшемыслав Ремин

1
@PrzemyslawRemin Извините, я не был ясен в своем утверждении, и теперь я понял, что SP уже рассматривает случай пропущенных значений. SP должен быть вызван таким образом: CALL median("table","x","x IS NOT NULL").
Джордано

2

Мое решение, представленное ниже, работает в одном запросе без создания таблицы, переменной или даже подзапроса. Кроме того, он позволяет вам получить медиану для каждой группы в групповых запросах (это то, что мне нужно!):

SELECT `columnA`, 
SUBSTRING_INDEX(SUBSTRING_INDEX(GROUP_CONCAT(`columnB` ORDER BY `columnB`), ',', CEILING((COUNT(`columnB`)/2))), ',', -1) medianOfColumnB
FROM `tableC`
-- some where clause if you want
GROUP BY `columnA`;

Это работает из-за умного использования group_concat и substring_index.

Но, чтобы разрешить большой group_concat, вы должны установить для group_concat_max_len более высокое значение (по умолчанию 1024 символа). Вы можете установить его так (для текущей сессии sql):

SET SESSION group_concat_max_len = 10000; 
-- up to 4294967295 in 32-bits platform.

Дополнительная информация для group_concat_max_len: https://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_group_concat_max_len


2

Еще один рифф на ответе Велкроу, но он использует одну промежуточную таблицу и использует переменную, используемую для нумерации строк, чтобы получить счет, а не выполнять дополнительный запрос для его вычисления. Также начинается отсчет, так что первая строка - это строка 0, позволяющая просто использовать Floor и Ceil для выбора медианной строки (строк).

SELECT Avg(tmp.val) as median_val
    FROM (SELECT inTab.val, @rows := @rows + 1 as rowNum
              FROM data as inTab,  (SELECT @rows := -1) as init
              -- Replace with better where clause or delete
              WHERE 2 > 1
              ORDER BY inTab.val) as tmp
    WHERE tmp.rowNum in (Floor(@rows / 2), Ceil(@rows / 2));

2
SELECT 
    SUBSTRING_INDEX(
        SUBSTRING_INDEX(
            GROUP_CONCAT(field ORDER BY field),
            ',',
            ((
                ROUND(
                    LENGTH(GROUP_CONCAT(field)) - 
                    LENGTH(
                        REPLACE(
                            GROUP_CONCAT(field),
                            ',',
                            ''
                        )
                    )
                ) / 2) + 1
            )),
            ',',
            -1
        )
FROM
    table

Выше, кажется, работает для меня.


Он не возвращает правильную медиану для четного числа значений. Например, медиана {98,102,102,98}- это, 100но ваш код дает 102. Это работало нормально для нечетных чисел.
Номилукс

1

Я использовал два запроса:

  • первый, чтобы получить количество, мин, макс и средний
  • второй (подготовленный оператор) с предложениями «LIMIT @ count / 2, 1» и «ORDER BY ..» для получения медианного значения

Они заключены в функцию defn, поэтому все значения могут быть возвращены за один вызов.

Если ваши диапазоны статичны и ваши данные не меняются часто, может быть более эффективно предварительно вычислить / сохранить эти значения и использовать сохраненные значения вместо того, чтобы каждый раз запрашивать с нуля.


1

так как мне просто нужно решение со средним и процентилем, я сделал простую и довольно гибкую функцию, основанную на выводах в этой теме. Я знаю, что сам рад, если найду «готовые» функции, которые легко включить в мои проекты, поэтому я решил быстро поделиться:

function mysql_percentile($table, $column, $where, $percentile = 0.5) {

    $sql = "
            SELECT `t1`.`".$column."` as `percentile` FROM (
            SELECT @rownum:=@rownum+1 as `row_number`, `d`.`".$column."`
              FROM `".$table."` `d`,  (SELECT @rownum:=0) `r`
              ".$where."
              ORDER BY `d`.`".$column."`
            ) as `t1`, 
            (
              SELECT count(*) as `total_rows`
              FROM `".$table."` `d`
              ".$where."
            ) as `t2`
            WHERE 1
            AND `t1`.`row_number`=floor(`total_rows` * ".$percentile.")+1;
        ";

    $result = sql($sql, 1);

    if (!empty($result)) {
        return $result['percentile'];       
    } else {
        return 0;
    }

}

Использование очень просто, пример из моего текущего проекта:

...
$table = DBPRE."zip_".$slug;
$column = 'seconds';
$where = "WHERE `reached` = '1' AND `time` >= '".$start_time."'";

    $reaching['median'] = mysql_percentile($table, $column, $where, 0.5);
    $reaching['percentile25'] = mysql_percentile($table, $column, $where, 0.25);
    $reaching['percentile75'] = mysql_percentile($table, $column, $where, 0.75);
...

1

Вот мой путь. Конечно, вы можете поместить это в процедуру :-)

SET @median_counter = (SELECT FLOOR(COUNT(*)/2) - 1 AS `median_counter` FROM `data`);

SET @median = CONCAT('SELECT `val` FROM `data` ORDER BY `val` LIMIT ', @median_counter, ', 1');

PREPARE median FROM @median;

EXECUTE median;

Вы можете избежать переменной @median_counter, если вы замените ее:

SET @median = CONCAT( 'SELECT `val` FROM `data` ORDER BY `val` LIMIT ',
                      (SELECT FLOOR(COUNT(*)/2) - 1 AS `median_counter` FROM `data`),
                      ', 1'
                    );

PREPARE median FROM @median;

EXECUTE median;

1

Этот способ включает в себя как четные, так и нечетные числа без подзапроса.

SELECT AVG(t1.x)
FROM table t1, table t2
GROUP BY t1.x
HAVING SUM(SIGN(t1.x - t2.x)) = 0

Не могли бы вы сказать, что такое таблица t2?
xliiv

1

Основываясь на ответе @ bob, это обобщает запрос, чтобы иметь возможность возвращать несколько медиан, сгруппированных по некоторым критериям.

Подумайте, например, о средней цене продажи подержанных автомобилей на автомобильном участке, сгруппированной по годам и месяцам.

SELECT 
    period, 
    AVG(middle_values) AS 'median' 
FROM (
    SELECT t1.sale_price AS 'middle_values', t1.row_num, t1.period, t2.count
    FROM (
        SELECT 
            @last_period:=@period AS 'last_period',
            @period:=DATE_FORMAT(sale_date, '%Y-%m') AS 'period',
            IF (@period<>@last_period, @row:=1, @row:=@row+1) as `row_num`, 
            x.sale_price
          FROM listings AS x, (SELECT @row:=0) AS r
          WHERE 1
            -- where criteria goes here
          ORDER BY DATE_FORMAT(sale_date, '%Y%m'), x.sale_price
        ) AS t1
    LEFT JOIN (  
          SELECT COUNT(*) as 'count', DATE_FORMAT(sale_date, '%Y-%m') AS 'period'
          FROM listings x
          WHERE 1
            -- same where criteria goes here
          GROUP BY DATE_FORMAT(sale_date, '%Y%m')
        ) AS t2
        ON t1.period = t2.period
    ) AS t3
WHERE 
    row_num >= (count/2) 
    AND row_num <= ((count/2) + 1)
GROUP BY t3.period
ORDER BY t3.period;

1

Часто нам может потребоваться рассчитать медиану не только для всей таблицы, но и для агрегатов по нашему идентификатору. Другими словами, рассчитайте медиану для каждого идентификатора в нашей таблице, где каждый идентификатор имеет много записей. (хорошая производительность и работает во многих SQL + исправляет проблему четных и шансов, больше о производительности различных методов Median https://sqlperformance.com/2012/08/t-sql-queries/median )

SELECT our_id, AVG(1.0 * our_val) as Median
FROM
( SELECT our_id, our_val, 
  COUNT(*) OVER (PARTITION BY our_id) AS cnt,
  ROW_NUMBER() OVER (PARTITION BY our_id ORDER BY our_val) AS rn
  FROM our_table
) AS x
WHERE rn IN ((cnt + 1)/2, (cnt + 2)/2) GROUP BY our_id;

Надеюсь, поможет


Это лучшее решение. Однако для больших наборов данных он будет замедляться, поскольку он пересчитывается для каждого элемента в каждом наборе. Чтобы сделать это быстрее, поместите «COUNT (*)» в отдельный подзапрос.
Слава Мурыгин

1

MySQL поддерживает оконные функции начиная с версии 8.0, которую вы можете использовать ROW_NUMBERили DENSE_RANK( НЕ используйте, так RANKкак она присваивает одинаковый ранг одинаковым значениям, как в спортивном рейтинге):

SELECT AVG(t1.val) AS median_val
  FROM (SELECT val, 
               ROW_NUMBER() OVER(ORDER BY val) AS rownum
          FROM data) t1,
       (SELECT COUNT(*) AS num_records FROM data) t2
 WHERE t1.row_num IN
       (FLOOR((t2.num_records + 1) / 2), 
        FLOOR((t2.num_records + 2) / 2));

0

Если MySQL имеет ROW_NUMBER, то MEDIAN (вдохновлен этим запросом SQL Server):

WITH Numbered AS 
(
SELECT *, COUNT(*) OVER () AS Cnt,
    ROW_NUMBER() OVER (ORDER BY val) AS RowNum
FROM yourtable
)
SELECT id, val
FROM Numbered
WHERE RowNum IN ((Cnt+1)/2, (Cnt+2)/2)
;

IN используется, если у вас есть четное количество записей.

Если вы хотите найти медиану для каждой группы, просто укажите PARTITION BY group в ваших предложениях OVER.

обкрадывать


1
Нет, нет ROW_NUMBER OVER, нет PARTITION BY, ничего из этого; это MySql, а не настоящий движок БД, такой как PostgreSQL, IBM DB2, MS SQL Server и т. д .;-).
Алекс Мартелли

0

Прочитав все предыдущие, они не соответствовали моему фактическому требованию, поэтому я реализовал свое собственное, которое не нуждается ни в какой процедуре или усложняет утверждения, просто я GROUP_CONCAT все значения из столбца, который я хотел получить MEDIAN, и применяя COUNT DIV BY 2 Я извлекаю значение из середины списка, как это делает следующий запрос:

(POS - это название столбца, который я хочу получить в медиане)

(query) SELECT
SUBSTRING_INDEX ( 
   SUBSTRING_INDEX ( 
       GROUP_CONCAT(pos ORDER BY CAST(pos AS SIGNED INTEGER) desc SEPARATOR ';') 
    , ';', COUNT(*)/2 ) 
, ';', -1 ) AS `pos_med`
FROM table_name
GROUP BY any_criterial

Я надеюсь, что это может быть полезно для кого-то, как многие другие комментарии были для меня с этого сайта.


0

Зная точное количество строк, вы можете использовать этот запрос:

SELECT <value> AS VAL FROM <table> ORDER BY VAL LIMIT 1 OFFSET <half>

куда <half> = ceiling(<size> / 2.0) - 1


0

У меня есть база данных, содержащая около 1 миллиарда строк, которые нам необходимы для определения среднего возраста в наборе. Сортировать миллиард строк сложно, но если вы объединяете различные значения, которые можно найти (возраст от 0 до 100), вы можете отсортировать ЭТОТ список и использовать некоторую арифметическую магию, чтобы найти любой процентиль, который вы хотите, следующим образом:

with rawData(count_value) as
(
    select p.YEAR_OF_BIRTH
        from dbo.PERSON p
),
overallStats (avg_value, stdev_value, min_value, max_value, total) as
(
  select avg(1.0 * count_value) as avg_value,
    stdev(count_value) as stdev_value,
    min(count_value) as min_value,
    max(count_value) as max_value,
    count(*) as total
  from rawData
),
aggData (count_value, total, accumulated) as
(
  select count_value, 
    count(*) as total, 
        SUM(count(*)) OVER (ORDER BY count_value ROWS UNBOUNDED PRECEDING) as accumulated
  FROM rawData
  group by count_value
)
select o.total as count_value,
  o.min_value,
    o.max_value,
    o.avg_value,
    o.stdev_value,
    MIN(case when d.accumulated >= .50 * o.total then count_value else o.max_value end) as median_value,
    MIN(case when d.accumulated >= .10 * o.total then count_value else o.max_value end) as p10_value,
    MIN(case when d.accumulated >= .25 * o.total then count_value else o.max_value end) as p25_value,
    MIN(case when d.accumulated >= .75 * o.total then count_value else o.max_value end) as p75_value,
    MIN(case when d.accumulated >= .90 * o.total then count_value else o.max_value end) as p90_value
from aggData d
cross apply overallStats o
GROUP BY o.total, o.min_value, o.max_value, o.avg_value, o.stdev_value
;

Этот запрос зависит от ваших оконных функций, поддерживающих db (включая ROWS UNBOUNDED PRECEDING), но если у вас его нет, просто объединить aggData CTE с самим собой и объединить все предыдущие итоги в столбец «накопленный», который используется для определения того, какие значение содержит указанный прецентиль. В приведенном выше примере вычисляются значения p10, p25, p50 (медиана), p75 и p90.

-Крис


0

Взято с: http://mdb-blog.blogspot.com/2015/06/mysql-find-median-nth-element-without.html

Я бы предложил другой способ, без объединения , но работа со строками

я не проверял это с таблицами с большими данными, но с маленькими / средними таблицами это работает просто отлично.

Хорошо, что здесь работает и группировка поэтому может возвращать медиану для нескольких элементов.

Вот тестовый код для тестовой таблицы:

DROP TABLE test.test_median
CREATE TABLE test.test_median AS
SELECT 'book' AS grp, 4 AS val UNION ALL
SELECT 'book', 7 UNION ALL
SELECT 'book', 2 UNION ALL
SELECT 'book', 2 UNION ALL
SELECT 'book', 9 UNION ALL
SELECT 'book', 8 UNION ALL
SELECT 'book', 3 UNION ALL

SELECT 'note', 11 UNION ALL

SELECT 'bike', 22 UNION ALL
SELECT 'bike', 26 

и код для нахождения медианы для каждой группы:

SELECT grp,
         SUBSTRING_INDEX( SUBSTRING_INDEX( GROUP_CONCAT(val ORDER BY val), ',', COUNT(*)/2 ), ',', -1) as the_median,
         GROUP_CONCAT(val ORDER BY val) as all_vals_for_debug
FROM test.test_median
GROUP BY grp

Вывод:

grp | the_median| all_vals_for_debug
bike| 22        | 22,26
book| 4         | 2,2,3,4,7,8,9
note| 11        | 11

Вы не думаете, что медиана `{22,26}` должна быть 24?
Номилукс

0

В некоторых случаях медиана рассчитывается следующим образом:

«Медиана» - это «среднее» значение в списке чисел, когда они упорядочены по значению. Для четных наборов значений медиана является средним из двух средних значений . Я создал простой код для этого:

$midValue = 0;
$rowCount = "SELECT count(*) as count {$from} {$where}";

$even = FALSE;
$offset = 1;
$medianRow = floor($rowCount / 2);
if ($rowCount % 2 == 0 && !empty($medianRow)) {
  $even = TRUE;
  $offset++;
  $medianRow--;
}

$medianValue = "SELECT column as median 
               {$fromClause} {$whereClause} 
               ORDER BY median 
               LIMIT {$medianRow},{$offset}";

$medianValDAO = db_query($medianValue);
while ($medianValDAO->fetch()) {
  if ($even) {
    $midValue = $midValue + $medianValDAO->median;
  }
  else {
    $median = $medianValDAO->median;
  }
}
if ($even) {
  $median = $midValue / 2;
}
return $median;

Возвращенное значение $ median будет требуемым результатом :-)

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.