web-dev-qa-db-fra.com

Un moyen simple de calculer la médiane avec MySQL

Quel est le moyen le plus simple (et heureusement pas trop lent) de calculer la médiane avec MySQL? J'ai utilisé AVG(x) pour trouver la moyenne, mais j'ai du mal à trouver un moyen simple de calculer la médiane. Pour le moment, je renvoie toutes les lignes à PHP, en effectuant un tri, puis en choisissant la ligne du milieu, mais il doit exister un moyen simple de le faire en une seule requête MySQL.

Exemple de données:

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

Le tri sur val donne 2 2 3 4 7 8 9, la médiane doit donc être 4, par rapport à SELECT AVG(val) qui == 5.

177
davr

Dans 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 souligne, qu'après le premier passage, @rownum contiendra le nombre total de lignes. Ceci peut être utilisé pour déterminer la médiane, de sorte qu'aucun second passage ou jointure n'est nécessaire.

De plus, AVG(dd.val) et dd.row_number IN(...) sont utilisés pour produire correctement une médiane lorsqu'il existe un nombre pair d'enregistrements. Raisonnement:

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

Enfin, MariaDB 10.3.3+ contient une fonction MEDIAN

201
velcrow

Je viens de trouver une autre réponse en ligne dans les commentaires :

Pour les médianes dans presque tous les 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

Assurez-vous que vos colonnes sont bien indexées et que l'index est utilisé pour le filtrage et le tri. Vérifiez avec les plans d'expliquer.

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

Calculez le numéro de la "médiane". Peut-être utiliser: median_row = floor(count / 2).

Puis choisissez-le dans la liste:

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

Cela devrait vous retourner une ligne avec juste la valeur que vous voulez.

Jacob

54
TheJacobTaylor

J'ai trouvé que la solution acceptée ne fonctionnait pas sur mon installation MySQL, renvoyant un ensemble vide, mais cette requête fonctionnait pour moi dans toutes les situations sur lesquelles je l'avais testée:

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
28
zookatron

Malheureusement, ni les réponses de TheJacobTaylor ni celles de velcro ne donnent des résultats précis pour les versions actuelles de MySQL.

La réponse ci-dessus de Velcro est proche, mais elle ne se calcule pas correctement pour les ensembles de résultats comportant un nombre pair de lignes. Les médianes sont définies comme suit: 1) le nombre du milieu dans les ensembles impairs, ou 2) la moyenne des deux nombres du milieu dans les ensembles de nombres pairs.

Voici donc la solution de velcro corrigée pour gérer les ensembles de nombres pairs et impairs:

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;

Pour utiliser ceci, suivez ces 3 étapes faciles:

  1. Remplacez "median_table" (2 occurrences) dans le code ci-dessus par le nom de votre table.
  2. Remplacez "median_column" (3 occurrences) par le nom de la colonne pour laquelle vous souhaitez trouver une médiane.
  3. Si vous avez une condition WHERE, remplacez "WHERE 1" (2 occurrences) par votre condition Where.
16
bob

Je propose un moyen plus rapide.

Obtenez le nombre de lignes:

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

Prenez ensuite la valeur du milieu dans une sous-requête triée:

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

J'ai testé cela avec un jeu de données 5x10e6 de nombres aléatoires et la médiane sera trouvée en moins de 10 secondes.

9
Reggie Edwards

Un commentaire sur cette page de la documentation MySQL a la suggestion suivante:

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

Construire à partir de la réponse de velcro, pour ceux d'entre vous devant faire une médiane à partir de quelque chose qui est groupé par un autre paramètre:

 SELECT grp_field, t1.val FROM ( SELECT grp_field, @rownum: = IF (@s = grp_field, @rownum + 1, 0) AS row_number, @S: = IF (@s = grp_field, @s, grp_field) AS sec, d.val FROM données d, (SELECT @ rangée: = 0, @s: = 0) r ORDER BY grp_field, d.val ) as t1 JOIN ( SELECT grp_field, compte (*) comme total_rows FROM données d GROUP BY grp_field ) comme t2 ON t1.grp_field = t2.grp_field WHERE t1. row_number = floor (total_rows/2) +1; 

4
Doug

J'ai ce code ci-dessous que j'ai trouvé sur HackerRank et il est assez simple et fonctionne dans tous les cas.

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 );
4
Prashant Srivastav

La plupart des solutions ci-dessus ne fonctionnent que pour un seul champ de la table. Vous devrez peut-être obtenir la médiane (50e centile) pour de nombreux champs de la requête. 

J'utilise ceci: 

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;

Vous pouvez remplacer le "50" dans l'exemple ci-dessus par n'importe quel centile, est très efficace.

Assurez-vous simplement d'avoir assez de mémoire pour GROUP_CONCAT, vous pouvez le changer avec:

SET group_concat_max_len = 10485760; #10MB max length

Plus de détails: http://web.performancerasta.com/metrics-tips-calculating-95th-99th-or-any-percentile-with-single-mysql-query/

4
Nico

Prend soin du nombre de valeurs impaires - indique le nombre moyen des deux valeurs situées au centre dans ce cas.

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
3
Franz K.

Vous pouvez utiliser la fonction définie par l'utilisateur qui se trouve ici .

3
Alex Martelli
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

Ce qui précède semble fonctionner pour moi.

2
Nochum Sossonko

Eventuellement, vous pouvez également le faire dans une procédure stockée:

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

Installez et utilisez les fonctions statistiques mysql: http://www.xarg.org/2012/07/statistical-functions-in-mysql/

Après cela, calculer la médiane est facile:

SELECT médiane (x) DE t1

2
Leonardo Nicolas

Mon code, efficace sans tables ni variables supplémentaires:

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;
2
Oscar Canon

Un autre riff sur la réponse de Velcrow, mais utilise une seule table intermédiaire et tire parti de la variable utilisée pour la numérotation des lignes afin d'obtenir le nombre, plutôt que d'effectuer une requête supplémentaire pour le calculer. Commence également le décompte pour que la première ligne soit la ligne 0, ce qui permet simplement d’utiliser Floor et Ceil pour sélectionner la ou les lignes médianes.

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));
1
Steve Cohen

Cette méthode semble inclure à la fois le nombre pair et impair sans sous-requête.

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

Ma solution présentée ci-dessous fonctionne en une seule requête sans création de table, de variable ni même de sous-requête . De plus, elle vous permet d'obtenir la médiane de chaque groupe dans les requêtes groupées (c'est ce dont j'avais besoin!):

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

Cela fonctionne grâce à une utilisation intelligente de group_concat et de substring_index.

Mais, pour autoriser big group_concat, vous devez définir group_concat_max_len sur une valeur plus élevée (1024 caractères par défaut) . Vous pouvez le définir comme ceci (pour la session SQL en cours): 

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

Plus d'infos pour group_concat_max_len: https://dev.mysql.com/doc/refman/5.1/fr/server-system-variables.html#sysvar_group_concat_max_len

1
didier2l

comme j'avais juste besoin d'une solution médiane ET centile, j'ai créé une fonction simple et assez souple basée sur les résultats de ce fil. Je sais que je suis heureux si je trouve des fonctions "readymade" faciles à inclure dans mes projets. J'ai donc décidé de partager rapidement:

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'utilisation est très facile, exemple de mon projet actuel:

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

J'ai utilisé une approche à deux requêtes:

  • le premier à compter, min, max et avg
  • deuxième (instruction préparée) avec les clauses "LIMIT @ count/2, 1" et "ORDER BY .." pour obtenir la valeur médiane

Celles-ci sont encapsulées dans une fonction defn, afin que toutes les valeurs puissent être renvoyées à partir d'un appel.

Si vos plages sont statiques et que vos données ne changent pas souvent, il peut être plus efficace de précalculer/stocker ces valeurs et d’utiliser les valeurs stockées au lieu d’interroger à nouveau chaque fois.

1
btk

Souvent, nous pouvons avoir besoin de calculer la médiane non seulement pour l'ensemble du tableau, mais également pour les agrégats relatifs à notre identité. En d’autres termes, calculez la médiane de chaque identifiant de notre tableau, où chaque identifiant comporte de nombreux enregistrements. (bonnes performances et fonctionne dans de nombreux problèmes SQL + résolus problème de même et de chances, plus sur la performance de différentes méthodes médianes 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;

J'espère que ça aide

1

Voici mon chemin. Bien sûr, vous pouvez le mettre en procédure :-)

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;

Vous pouvez éviter la variable @median_counter si vous la substituez:

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
pucawo

Si MySQL a ROW_NUMBER, le MEDIAN est (s'inspire de cette requête 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)
;

L'IN est utilisé dans le cas où vous avez un nombre pair d'entrées.

Si vous voulez trouver la médiane par groupe, choisissez simplement PARTITION BY groupe dans vos clauses OVER.

Rob

0
Rob Farley

J'ai une base de données contenant environ 1 milliard de lignes dont nous avons besoin pour déterminer l'âge médian de l'ensemble. Il est difficile de trier un milliard de lignes, mais si vous agrégez les valeurs distinctes pouvant être trouvées (les âges vont de 0 à 100 ans), vous pouvez trier CETTE liste et utiliser une magie arithmétique pour trouver le centile de votre choix, comme suit:

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
;

Cette requête dépend de votre base de données prenant en charge les fonctions de fenêtre (y compris ROWS UNBOUNDED PRECEDING), mais si ce n’est pas le cas, il est simple de joindre aggData CTE avec lui-même et d’agréger tous les totaux antérieurs dans la colonne "accumulée" utilisée pour déterminer valeur contient le précentile spécifié. L'échantillon ci-dessus calcule p10, p25, p50 (médiane), p75 et p90.

-Chris

0
Chris Knoll

Après avoir lu toutes les précédentes, elles ne correspondaient pas à mes besoins réels. J'ai donc implémenté la mienne qui ne nécessite aucune procédure ni instruction compliquée, mais je GROUP_CONCAT toutes les valeurs de la colonne dans laquelle je voulais obtenir la MEDIAN et en appliquant un COUNT DIV BY 2 J'extrais la valeur du milieu de la liste comme le fait la requête suivante: 

(POS est le nom de la colonne que je veux obtenir sa médiane)

(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

J'espère que cela pourra être utile à quelqu'un de la même manière que beaucoup d'autres commentaires de ce site. 

0
Gabriel G.
create table med(id integer);
insert into med(id) values(1);
insert into med(id) values(2);
insert into med(id) values(3);
insert into med(id) values(4);
insert into med(id) values(5);
insert into med(id) values(6);

select (MIN(count)+MAX(count))/2 from 
(select case when (select count(*) from 
med A where A.id<B.id)=(select count(*)/2 from med) OR 
(select count(*) from med A where A.id>B.id)=(select count(*)/2 
from med) then cast(B.id as float)end as count from med B) C;

 ?column? 
----------
  3.5
(1 row)

OR

select cast(avg(id) as float) from 
(select t1.id from med t1 JOIN med t2 on t1.id!= t2.id 
group by t1.id having ABS(SUM(SIGN(t1.id-t2.id)))=1) A;
0
Dwipam Katariya

Le code SQL suivant vous aidera à calculer la médiane dans MySQL en utilisant des variables définies par l'utilisateur.

create table employees(salary int);

insert into employees values(8);
insert into employees values(23);
insert into employees values(45);
insert into employees values(123);
insert into employees values(93);
insert into employees values(2342);
insert into employees values(2238);

select * from employees;

Select salary from employees  order by salary;

set @rowid=0;
set @cnt=(select count(*) from employees);
set @middle_no=ceil(@cnt/2);
set @odd_even=null;

select AVG(salary) from 
(select salary,@rowid:=@rowid+1 as rid, (CASE WHEN(mod(@cnt,2)=0) THEN @odd_even:=1 ELSE @odd_even:=0 END) as odd_even_status  from employees  order by salary) as tbl where tbl.rid=@middle_no or tbl.rid=(@middle_no+@odd_even);

Si vous recherchez une explication détaillée, veuillez vous référer à ce blog.

0

J'ai trouvé cette réponse très utile - https://www.eversql.com/how-to-calculate-median-value-in-mysql-using-a-simple-sql-query/

SET @rowindex := -1;

SELECT
   AVG(g.grade)
FROM
   (SELECT @rowindex:=@rowindex + 1 AS rowindex,
       grades.grade AS grade
    FROM grades
    ORDER BY grades.grade) AS g
WHERE
g.rowindex IN (FLOOR(@rowindex / 2) , CEIL(@rowindex / 2));
0
Kwex

Ces méthodes sélectionnent deux fois dans la même table. Si les données source proviennent d'une requête coûteuse, évitez de l'exécuter deux fois:

select KEY_FIELD, AVG(VALUE_FIELD) MEDIAN_VALUE
from (
    select KEY_FIELD, VALUE_FIELD, RANKF
    , @rownumr := IF(@prevrowidr=KEY_FIELD,@rownumr+1,1) RANKR
    , @prevrowidr := KEY_FIELD
    FROM (
        SELECT KEY_FIELD, VALUE_FIELD, RANKF
        FROM (
            SELECT KEY_FIELD, VALUE_FIELD 
            , @rownumf := IF(@prevrowidf=KEY_FIELD,@rownumf+1,1) RANKF
            , @prevrowidf := KEY_FIELD     
            FROM (
                SELECT KEY_FIELD, VALUE_FIELD 
                FROM (
                    -- some expensive query
                )   B
                ORDER BY  KEY_FIELD, VALUE_FIELD
            ) C
            , (SELECT @rownumf := 1) t_rownum
            , (SELECT @prevrowidf := '*') t_previd
        ) D
        ORDER BY  KEY_FIELD, RANKF DESC
    ) E
    , (SELECT @rownumr := 1) t_rownum
    , (SELECT @prevrowidr := '*') t_previd
) F
WHERE RANKF-RANKR BETWEEN -1 and 1
GROUP BY KEY_FIELD
0
RobbertNix

Dans certains cas, la médiane est calculée comme suit:

La "médiane" est la "moyenne" valeur dans la liste des nombres quand ils sont classés par valeur. Pour les ensembles de comptes pairs, median est la moyenne des deux valeurs intermédiaires .

$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 médiane retournée serait le résultat requis :-)

0
jitendrapurohit

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

Je suggérerais une autre façon, sans rejoindre, Mais en travaillant avec strings

je ne l'ai pas vérifié avec des tables avec de grandes données, mais des tables petites/moyennes, ça fonctionne très bien.

La bonne chose ici, que cela fonctionne aussi par GROUPING afin de pouvoir renvoyer la médiane de plusieurs éléments.

voici le code de test pour la table de 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 

et le code pour trouver la médiane pour chaque groupe:

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

Sortie:

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

Sur la base de la réponse de @bob, la requête est généralisée pour pouvoir renvoyer plusieurs médianes, regroupées selon certains critères.

Pensez, par exemple, au prix de vente médian des voitures d’occasion dans un lot de voitures, groupé par année-mois.

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;
0
Ariel Allon

Connaissant le nombre exact de lignes, vous pouvez utiliser cette requête:

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

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

0
ZhekaKozlov

Médianes regroupées par dimension:

SELECT your_dimension, avg(t1.val) as median_val FROM (
SELECT @rownum:=@rownum+1 AS `row_number`,
   IF(@dim <> d.your_dimension, @rownum := 0, NULL),
   @dim := d.your_dimension AS your_dimension,
   d.val
   FROM data d,  (SELECT @rownum:=0) r, (SELECT @dim := 'something_unreal') d
  WHERE 1
  -- put some where clause here
  ORDER BY d.your_dimension, d.val
) as t1
INNER JOIN  
(
  SELECT d.your_dimension,
    count(*) as total_rows
  FROM data d
  WHERE 1
  -- put same where clause here
  GROUP BY d.your_dimension
) as t2 USING(your_dimension)
WHERE 1
AND t1.row_number in ( floor((total_rows+1)/2), floor((total_rows+2)/2) )

GROUP BY your_dimension;
0
Vladimir_M