Modo semplice per calcolare la mediana con MySQL


208

Qual è il modo più semplice (e speriamo non troppo lento) di calcolare la mediana con MySQL? Ho usatoAVG(x) per trovare la media, ma faccio fatica a trovare un modo semplice per calcolare la mediana. Per ora, sto restituendo tutte le righe a PHP, facendo un ordinamento e quindi selezionando la riga centrale, ma sicuramente ci deve essere un modo semplice per farlo in una singola query MySQL.

Dati di esempio:

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

L'ordinamento val2 2 3 4 7 8 9, quindi dovrebbe essere la mediana 4, rispetto a SELECT AVG(val)quale == 5.


72
sono l'unico nauseato dal fatto che MySQL non ha una funzione per calcolare una mediana? Ridicolo.
Monica Heddneck,

3
MariaDB dalla versione 10.3 ne ha una, vedi mariadb.com/kb/en/library/median
berturion

Risposte:


225

In 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) );

Steve Cohen sottolinea che dopo il primo passaggio, @rownum conterrà il numero totale di righe. Questo può essere usato per determinare la mediana, quindi non è necessario alcun secondo passaggio o join.

Anche AVG(dd.val)e dd.row_number IN(...)viene utilizzato per produrre correttamente una mediana quando ci sono un numero pari di record. Ragionamento:

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

Infine, MariaDB 10.3.3+ contiene una funzione MEDIANA


4
un modo per farlo mostrare i valori di gruppo? come: posto / mediana per quel posto ... come selezionare posto, median_value dalla tabella ... in qualche modo? grazie
saulob il

2
@rowNum avrà il "conteggio totale" alla fine dell'esecuzione. Quindi puoi usarlo se vuoi evitare di dover fare di nuovo un "conto tutto" (che era il mio caso perché la mia domanda non era così semplice)
Ahmed-Anas,

La logica di avere una frase: (floor ((total_rows + 1) / 2), floor ((total_rows + 2) / 2)) calcola le righe necessarie per la mediana è fantastica! Non sono sicuro di come ci hai pensato, ma è geniale. La parte che non seguo è (SELECT @rownum: = 0) r - a quale scopo serve?
Shanemeister,

cambia il primo WHERE 1in WHERE d.val IS NOT NULLmodo da escludere le NULLrighe per mantenere questo metodo allineato con il nativoAVG
chiliNUT

1
Il mio valore proveniva da un join a due tabelle, quindi ho dovuto aggiungere un'altra query secondaria per assicurarmi che l'ordinamento delle righe fosse corretto dopo il join! La struttura era una specie diselect avg(value) from (select value, row_number from (select a - b as value from a_table join b_table order by value))
Daniel Buckmaster l'

62

Ho appena trovato un'altra risposta online nei commenti :

Per mediane in quasi tutti gli 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

Assicurati che le tue colonne siano ben indicizzate e che l'indice sia usato per filtrare e ordinare. Verificare con i piani di spiegazione.

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

Calcola il numero di riga "mediana". Forse usare:median_row = floor(count / 2) .

Quindi selezionalo dall'elenco:

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

Questo dovrebbe restituirti una riga con solo il valore desiderato.

Giacobbe


6
@rob puoi aiutarmi a modificare per favore? O dovrei semplicemente inchinarmi alla soluzione velcrow? (non sono sicuro di come rinviare a un'altra soluzione) Grazie, Jacob
TheJacobTaylor,

1
Si noti che esegue un "cross join", che è molto lento per le tabelle di grandi dimensioni.
Rick James,

1
Questa risposta non restituisce nulla per anche il numero di righe.
kuttumiah,

Questa risposta non funziona affatto per alcuni set di dati, ad esempio il set di dati banali con valori 0,1, 0,1, 0,1, 2 - funzionerà se tutti i valori sono distinti, ma funziona solo se i valori
Kem Mason

32

Ho scoperto che la soluzione accettata non funzionava sulla mia installazione MySQL, restituendo un set vuoto, ma questa query ha funzionato per me in tutte le situazioni in cui l'ho testata:

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
assolutamente corretto, funziona perfettamente e molto velocemente sulle mie tabelle indicizzate
Rob

2
questa sembra essere la soluzione più veloce su mysql tra tutte le risposte qui, 200 ms con poco meno di un milione di record nella tabella
Rob

3
@FrankConijn: seleziona due volte da una tabella. Il nome della tabella è dataed è utilizzato con due nomi xe y.
Brian,

3
sto solo dicendo che ho bloccato il mio mysqld con questa query esatta su un tavolo con 33k righe ...
Xenonite

1
Questa query restituisce una risposta sbagliata per anche il numero di righe.
kuttumiah,

26

Sfortunatamente, né le risposte di TheJacobTaylor né di velcrow restituiscono risultati accurati per le versioni attuali di MySQL.

La risposta di Velcro dall'alto è vicina, ma non viene calcolata correttamente per i set di risultati con un numero pari di righe. Le mediane sono definite come 1) il numero medio in serie dispari, oppure 2) la media dei due numeri medi in serie pari.

Quindi, ecco la soluzione di velcro patchata per gestire i set di numeri pari e dispari:

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;

Per usarlo, segui questi 3 semplici passaggi:

  1. Sostituisci "median_table" (2 occorrenze) nel codice sopra con il nome della tua tabella
  2. Sostituisci "median_column" (3 occorrenze) con il nome della colonna per cui desideri trovare una mediana
  3. Se hai una condizione WHERE, sostituisci "WHERE 1" (2 occorrenze) con la tua condizione where

E cosa fai per la mediana dei valori di stringa?
Rick James,

12

Propongo un modo più veloce.

Ottieni il conteggio delle righe:

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

Quindi prendere il valore medio in una sottoquery ordinata:

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

Ho provato questo con un set di dati 5x10e6 di numeri casuali e troverà la mediana in meno di 10 secondi.


3
Why not: SELECT val FROM data ORDER BY val limit @middlevalue, 1
Bryan

1
Come si estrae l'output variabile del primo blocco di codice nel secondo blocco di codice?
Viaggio il

3
Come in, da dove viene @middlevalue?
Viaggio dal

@Bryan - Sono d'accordo con te, questo ha molto più senso per me. Hai mai trovato un motivo per non farlo in questo modo?
Shane N,

5
Questo non funziona in quanto una variabile non può essere utilizzata nella clausola limit.
codepk,

8

Un commento su questa pagina nella documentazione di MySQL ha il seguente suggerimento:

-- (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 

IMHO, questo è chiaramente il migliore per le situazioni in cui hai bisogno della mediana da un sottoinsieme (i) complicato (avevo bisogno di calcolare mediane separate di un gran numero di sottoinsiemi di dati)
mblackwell8

Funziona bene per me. 5.6.14 MySQL Community Server. La tabella con record 11M (circa 20 GB su disco), ha due indici non primari (model_id, prezzo). Nella tabella (dopo la filtrazione) abbiamo 500K record per il calcolo della mediana. Di conseguenza abbiamo 30.000 record (model_id, median_price). La durata della query è di 1,5-2 secondi. La velocità è veloce per me.
Mikl,

8

Installa e usa queste funzioni statistiche mysql: http://www.xarg.org/2012/07/statistical-functions-in-mysql/

Successivamente, calcolare la mediana è facile:

SELECT median(val) FROM data;

1
Ho appena provato questo, e per quello che vale, installarlo è stato super veloce / facile, e ha funzionato come pubblicizzato, incluso il raggruppamento, ad esempio "seleziona nome, mediana (x) DA t1 gruppo per nome" - fonte github qui: github.com/infusion/udf_infusion
Kem Mason

6

La maggior parte delle soluzioni sopra funzionano solo per un campo della tabella, potrebbe essere necessario ottenere la mediana (50 ° percentile) per molti campi della query.

Io lo uso questo:

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;

È possibile sostituire "50" nell'esempio sopra con qualsiasi percentile, è molto efficiente.

Assicurati di avere memoria sufficiente per GROUP_CONCAT, puoi cambiarlo con:

SET group_concat_max_len = 10485760; #10MB max length

Maggiori dettagli: http://web.performancerasta.com/metrics-tips-calculating-95th-99th-or-any-percentile-with-single-mysql-query/


Attenzione: per un numero pari di valori è necessario il più alto dei due valori medi. Per il numero di probabilità di valori prende il successivo valore più alto dopo la mediana.
giordano,

6

Ho questo codice di seguito che ho trovato su HackerRank ed è piuttosto semplice e funziona in ogni caso.

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
Credo che questo funziona solo con una tabella che ha il numero di voci è dispari. Per un numero pari di voci, questo potrebbe avere un problema.
Y. Chang,

4

Costruendo la risposta di velcro, per quelli di voi che devono fare una mediana su qualcosa che è raggruppato da un altro parametro:

SELEZIONA grp_field , t1 . val FROM ( SELECT grp_field , @ rownum : = IF (@ s = grp_field , @ rownum + 1 , 0 ) AS , @ s : = IF (@ s = grp_field , @ s , grp_field ) AS sec , d . val
   DA dati d , SELEZIONA 
         row_number
        (  @ rownum : = 0 , @ s : = 0 ) r
   ORDINA PER grp_field , d . val
 ) come t1 UNISCITI ( SELEZIONA grp_field , conta (*) come total_rows
   DA dati d
   GROUP BY grp_field
 ) come t2
 ON t1 . grp_field = t2 . grp_field
 DOVE t1 . row_number    
     = piano( total_rows / 2 ) +1 ;


3

È possibile utilizzare la funzione definita dall'utente che si trova qui .


3
Questo sembra il più utile, ma non voglio installare software alpha instabili che potrebbero causare l'arresto anomalo di mysql sul mio server di produzione :(
davr

6
Quindi studia le loro fonti per la funzione di interesse, correggile o modificale secondo necessità, e installa la "tua" versione stabile e non alfa una volta che l'hai fatta - com'è peggio che modificare in modo simile suggerimenti di codice meno comprovati si sale così? -)
Alex Martelli

3

Si occupa di un conteggio dei valori dispari: in questo caso fornisce la media dei due valori nel mezzo.

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

Il mio codice, efficiente senza tabelle o variabili aggiuntive:

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
Ciò fallirà su qualsiasi quantità sostanziale di dati perché GROUP_CONCATè limitato a 1023 caratteri, anche se utilizzato all'interno di un'altra funzione come questa.
Rob Van Dam,

2

Facoltativamente, è possibile farlo anche in una stored procedure:

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);

Grazie per questo! L'utente deve essere consapevole che i valori mancanti (NULL) sono considerati valori. per evitare questo problema aggiungere 'x IS NOT NULL dove condition.
giordano,

1
@giordano In quale riga del codice x IS NOT NULLaggiungere?
Przemyslaw Remin,

1
@PrzemyslawRemin Spiacente, non ero chiaro nella mia dichiarazione e mi sono reso conto ora che l'SP considera già il caso dei valori mancanti. La SP dovrebbe essere chiamato in questo modo: CALL median("table","x","x IS NOT NULL").
giordano,

2

La mia soluzione presentata di seguito funziona in una sola query senza la creazione di tabelle, variabili o persino query secondarie. Inoltre, ti permette di ottenere una mediana per ogni gruppo nelle query raggruppate (questo è quello di cui avevo bisogno!):

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`;

Funziona a causa di un uso intelligente di group_concat e substring_index.

Ma, per consentire big group_concat, devi impostare group_concat_max_len su un valore più alto (1024 caratteri per impostazione predefinita). Puoi impostarlo in questo modo (per la sessione sql corrente):

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

Ulteriori informazioni per group_concat_max_len: https://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_group_concat_max_len


2

Un altro riff sulla risposta di Velcrow, ma utilizza una singola tabella intermedia e sfrutta la variabile utilizzata per la numerazione delle righe per ottenere il conteggio, anziché eseguire una query aggiuntiva per calcolarlo. Inoltre, avvia il conteggio in modo che la prima riga sia la riga 0 per consentire semplicemente di utilizzare Floor e Ceil per selezionare le righe mediane.

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

Quanto sopra sembra funzionare per me.


Non sta restituendo la mediana corretta per il numero pari di valori, ad esempio, la mediana di {98,102,102,98}è 100ma il codice fornisce 102. Ha funzionato bene per i numeri dispari.
Nomiluchi il

1

Ho usato un approccio a due query:

  • il primo per ottenere conteggio, min, max e media
  • secondo (istruzione preparata) con le clausole "LIMIT @ count / 2, 1" e "ORDER BY .." per ottenere il valore mediano

Questi sono racchiusi in una funzione defn, quindi tutti i valori possono essere restituiti da una chiamata.

Se i tuoi intervalli sono statici e i tuoi dati non cambiano spesso, potrebbe essere più efficiente pre-calcolare / archiviare questi valori e utilizzare i valori memorizzati invece di eseguire query da zero ogni volta.


1

dato che avevo solo bisogno di una soluzione mediana e percentile, ho realizzato una funzione semplice e abbastanza flessibile basata sui risultati di questo thread. So di essere felice anche se trovo funzioni "readymade" facili da includere nei miei progetti, quindi ho deciso di condividere rapidamente:

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;
    }

}

L'utilizzo è molto semplice, ad esempio dal mio progetto attuale:

...
$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

Ecco la mia strada Certo, potresti metterlo in una procedura :-)

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;

Potresti evitare la variabile @median_counter, se la sostituisci:

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

In questo modo sembra includere sia il conteggio pari che dispari senza subquery.

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

Potresti dire qual è la tabella T2?
xliiv

1

Basato sulla risposta di @ bob, questo generalizza la query per avere la possibilità di restituire più mediane, raggruppate secondo alcuni criteri.

Pensa, ad esempio, al prezzo di vendita medio delle auto usate in un lotto di auto, raggruppate per anno-mese.

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

Spesso, potrebbe essere necessario calcolare la mediana non solo per l'intera tabella, ma per gli aggregati rispetto al nostro ID. In altre parole, calcola la mediana per ciascun ID nella nostra tabella, dove ogni ID ha molti record. (buone prestazioni e funziona in molti SQL + risolve il problema di pari e dispari, di più sulle prestazioni di diversi metodi mediani 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;

Spero che sia d'aiuto


È la migliore soluzione Tuttavia, per insiemi di dati di grandi dimensioni rallenterà perché viene conteggiato nuovamente per ogni elemento di ogni insieme. Per renderlo più veloce, inserisci "COUNT (*)" per separare la query secondaria.
Slava Murygin,

1

MySQL supporta le funzioni della finestra dalla versione 8.0, è possibile utilizzare ROW_NUMBERo DENSE_RANK( NON utilizzare RANKin quanto assegna lo stesso rango a stessi valori, come nella classifica sportiva):

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

Se MySQL ha ROW_NUMBER, MEDIAN è (ispirati a questa query di 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 viene utilizzato nel caso in cui si disponga di un numero pari di voci.

Se vuoi trovare la mediana per gruppo, allora solo PARTITION BY group nelle clausole OVER.

rapinare


1
No, no ROW_NUMBER OVER, no PARTITION BY, niente di tutto ciò; questo è MySql, non un vero motore DB come PostgreSQL, IBM DB2, MS SQL Server e così via ;-).
Alex Martelli,

0

Dopo aver letto tutti quelli precedenti non corrispondevano ai miei effettivi requisiti, quindi ho implementato il mio che non ha bisogno di alcuna procedura o di dichiarazioni complicate, solo io GROUP_CONCAT tutti i valori dalla colonna che volevo ottenere il MEDIANO e applicare un COUNT DIV BY 2 Estraggo il valore dal centro dell'elenco come fa la seguente query:

(POS è il nome della colonna che voglio ottenere la sua mediana)

(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

Spero che questo possa essere utile per qualcuno come molti altri commenti sono stati per me da questo sito web.


0

Conoscendo il conteggio esatto delle righe è possibile utilizzare questa query:

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

Dove <half> = ceiling(<size> / 2.0) - 1


0

Ho un database contenente circa 1 miliardo di righe necessarie per determinare l'età media nel set. L'ordinamento di un miliardo di righe è difficile, ma se si aggregano i valori distinti che possono essere trovati (le età vanno da 0 a 100), è possibile ordinare QUESTO elenco e utilizzare un po 'di magia aritmetica per trovare qualsiasi percentile desiderato come segue:

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
;

Questa query dipende dalle funzioni della finestra di supporto del tuo db (incluso ROWS UNBOUNDED PRECEDING) ma se non lo hai, è semplice unire aggData CTE con se stesso e aggregare tutti i totali precedenti nella colonna 'accumulata' che viene utilizzata per determinare quale Il valore contiene il precentile specificato. L'esempio sopra calcola p10, p25, p50 (mediana), p75 e p90.

-Chris


0

Tratto da: http://mdb-blog.blogspot.com/2015/06/mysql-find-median-nth-element-without.html

Suggerirei un altro modo, senza join , ma lavorando con le stringhe

non l'ho controllato con tabelle con dati di grandi dimensioni, ma tabelle piccole / medie funziona bene.

La cosa buona qui, che funziona anche da GROUPING modo che possa restituire la mediana per diversi elementi.

ecco il codice di test per la tabella di test:

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 

e il codice per trovare la mediana per ciascun gruppo:

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

Produzione:

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

Non pensi che la mediana di `{22,26}` dovrebbe essere 24?
Nomiluchi il

0

In alcuni casi la mediana viene calcolata come segue:

La "mediana" è il valore "medio" nell'elenco dei numeri quando sono ordinati per valore. Per i set di conteggi pari, la mediana è la media dei due valori medi . Ho creato un codice semplice per questo:

$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;

La $ mediana restituita sarebbe il risultato richiesto :-)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.