Простий спосіб розрахунку медіани за допомогою MySQL


207

Який найпростіший (і сподіваємось, не надто повільний) спосіб обчислити медіану за допомогою 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.


71
Мене єдино нудить те, що MySQL не має функції для обчислення медіани? Смішно.
Моніка Геднек

3
MariaDB з версії 10.3 є її, дивіться mariadb.com/kb/en/library/median
berturion

Відповіді:


224

У 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
@rob, можете допомогти редагувати будь-ласка? Або я просто схиляюся перед розчином оксамиту? (насправді не впевнений, як відкласти інше рішення) Спасибі, Якоб
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 на цьому точному запиті на столі з
33-рядковими

1
Цей запит повертає неправильну відповідь для парної кількості рядків.
kuttumiah

26

На жаль, ні відповіді TheJacobTaylor, ні velcrow не дають точних результатів для поточних версій MySQL.

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

Отже, ось рішення на липучці, яке виправлено для обробки як непарних, так і парних наборів номерів:

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 випадки) вашим умовою

І що ви робите для медіани рядкових значень?
Рік Джеймс

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 з даних ЗАМОВЛЕННЯ ВАМИ ГРАНИ @middlevalue, 1
Брайан

1
Як ви втягуєте змінний вихід першого блоку коду у другий блок коду?
Поїздка

3
Як у, звідки походить @middlevalue?
Поїздка

@Bryan - Я згоден з тобою, це має для мене набагато більше сенсу. Ви коли-небудь знаходили причину не робити цього так?
Шейн N

5
Це не працює, оскільки змінна не може бути використана в обмежувальному пункті.
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 

ІМХО, це явно найкраще для ситуацій, коли вам потрібна медіана від складного підмножини (мені потрібно було обчислити окремі медіани великої кількості
наборів

Добре працює для мене. 5.6.14 Сервер спільноти MySQL. Таблиця з записами 11M (близько 20Gb на диску), має два не первинних індекси (model_id, ціна). У таблиці (після фільтрації) ми маємо записи 500K для обчислення медіани. У результаті ми маємо 30K записів (model_id, median_price). Тривалість запиту - 1,5-2 секунди. Швидкість для мене швидка.
Мікл

7

Встановіть та використовуйте статистичні функції mysql: http://www.xarg.org/2012/07/statistic-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-calculating-95th-99th-or-any-percentile-with-single-mysql-query/


Будьте в курсі: для парної кількості значень це більше, ніж два середніх значення. Для непарного числа значень воно приймає наступне вище значення після медіани.
giordano

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
Я вважаю, що це працює лише з таблицею, у якій кількість записів є непарною. Для парної кількості записів це може мати проблеми.
Y. Chang

4

Побудова відповіді на липучці для тих, хто з вас повинен зробити медіану від того, що згруповано за іншим параметром:

ВИБІР grp_field , t1 . val ВІД ( SELECT grp_field , @ rownum : = IF (@ s = grp_field , @ rownum + 1 , 0 ) AS , @ s : = IF (@ s = grp_field , @ s , grp_field ) AS sec , d . val
   FROM data d , ( 
         row_number
       ВИБІР @ ROWNUM : = 0 , @ s : = 0 ) г
   ORDER BY grp_field , д . Val
 ) , як t1 JOIN ( SELECT , grp_field , відлік (*) , як TOTAL_ROWS
   З даних d
   GROUP BY grp_field
 ) як t2
 ON t1 . grp_field = t2 . grp_field
 ДЕ t1 . рядок_число     
     = поверх ( total_rows / 2 ) +1 ;


3

Ви можете використовувати визначену користувачем функцію, яку ви знайдете тут .


3
Це виглядає найбільш корисно, але я не хочу встановлювати нестабільне альфа-програмне забезпечення, яке може спричинити збій mysql на моєму виробничому сервері :(
давр

6
Тож вивчіть їх джерела на функцію, що цікавить, виправте їх або модифікуйте за потребою та встановіть "власну" стабільну та не альфа-версію після того, як ви це зробили - як це гірше, ніж аналогічно налаштувати менш перевірені пропозиції щодо коду ви потрапляєте на SO? -)
Алекс Мартеллі

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 НЕ НУЛЬНИЙ, коли умова.
giordano

1
@giordano У якому рядку коду x IS NOT NULLслід додати?
Перемислав Ремін

1
@PrzemyslawRemin Вибачте, мені не було зрозуміло у своїй заяві, і я зрозумів, що СП вже розглядає випадок відсутніх значень. SP повинен бути викликаний таким чином: CALL median("table","x","x IS NOT NULL").
giordano

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/uk/server-system-variables.html#sysvar_group_concat_max_len


2

Інший риф на відповідь Velcrow, але використовує єдину проміжну таблицю і використовує змінну, яка використовується для нумерації рядків, щоб отримати підрахунок, а не виконувати додатковий запит для його обчислення. Також починається підрахунок, щоб перший рядок був рядок 0, щоб дозволити просто використовувати підлогу та стелю для вибору серединних рядів.

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. Це спрацювало чудово для непарних чисел.
Nomiluks

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

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 + виправляє проблеми парних і шансів, докладніше про продуктивність різних медіано-методів 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 використовується в тому випадку, якщо у вас є парна кількість записів.

Якщо ви хочете знайти медіану для кожної групи, то просто розділіть групу у своїх ЗАДАЧАХ.

Роб


1
Ні, ні ROW_NUMBER OVER, не ПАРТІЯ, нічого з цього; це 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

Я б запропонував інший спосіб, без приєднання , але роботи зі струнами

я не перевіряв це таблицями з великими даними, але малі / середні таблиці він працює просто чудово.

Хороша річ, що він працює також за допомогою GROUPING, щоб він міг повернути медіану для кількох елементів.

ось код тесту для тестової таблиці:

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;

Повернута $ медіана буде необхідним результатом :-)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.