MySQL ile medyan hesaplamanın kolay yolu


208

MySQL ile medyanı hesaplamanın en basit (ve umarım çok yavaş değil) yolu nedir? KullandımAVG(x)Ortalamayı bulmak için , ancak medyanı hesaplamanın basit bir yolunu bulmakta zorlanıyorum. Şimdilik, PHP'ye tüm satırları döndürüyorum, bir sıralama yapıyorum ve sonra orta satırı seçiyorum, ancak kesinlikle tek bir MySQL sorgusunda yapmanın basit bir yolu olmalı.

Örnek veriler:

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

Üzerinde sıralama valverir 2 2 3 4 7 8 9medyan olmalıdır, böylece 4karşı, SELECT AVG(val)hangi == 5.


73
MySQL'in bir medyan hesaplamak için bir işlevi olmadığı gerçeğine kapılan tek ben miyim? Saçma.
Monica Heddneck

3
MariaDB, 10.3 sürümünden birine sahip olduğundan, bkz. Mariadb.com/kb/en/library/median
berturion

Yanıtlar:


225

MariaDB / MySQL'de:

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 , ilk geçişten sonra @ rownum'un toplam satır sayısını içereceğine dikkat çekiyor. Bu, medyanı belirlemek için kullanılabilir, bu nedenle ikinci bir geçiş veya birleştirmeye gerek yoktur.

Ayrıca AVG(dd.val)ve dd.row_number IN(...)çift ​​sayıda kayıt olduğunda bir medyanı doğru bir şekilde üretmek için kullanılır. Akıl Yürütme:

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

Son olarak, MariaDB 10.3.3+ bir MEDIAN işlevi içerir


4
grup değerlerini göstermek için herhangi bir yolu var mı? gibi: o yer için yer / medyan ... yer seç gibi, tablodan median_value ... herhangi bir şekilde? teşekkürler
saulob

2
@rowNum, yürütmenin sonunda 'toplam sayıya' sahip olacaktır. Bu yüzden tekrar 'saymak' yapmak zorunda kalmamak istiyorsanız kullanabilirsiniz (bu benim durumum çünkü sorgum çok basit değildi)
Ahmed-Anas

Bir ifadeye sahip olmanın mantığı: (kat ((total_rows + 1) / 2), kat ((total_rows + 2) / 2)) medyan için gereken satırları hesaplamak harika! Bunu nasıl düşündüğünden emin değilim, ama harika. Takip etmediğim bölüm (SELECT @rownum: = 0) r - bu hangi amaca hizmet ediyor?
Shanemeister

ilkini WHERE 1, bu yöntemi yerli ile uyumlu tutmak için satırları WHERE d.val IS NOT NULLhariç NULLtutacak şekilde değiştirinAVG
chiliNUT

1
Değerim iki tablo birleştirme geldi, bu yüzden katılmak sonra satır sipariş doğru olduğundan emin olmak için başka bir alt sorgu eklemek zorunda kaldı! Yapı bir neviselect avg(value) from (select value, row_number from (select a - b as value from a_table join b_table order by value))
Daniel Buckmaster

62

Sadece yorumların online başka bir yanıt bulduk :

Hemen hemen her SQL'deki medyanlar için:

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

Sütunlarınızın iyi dizine eklendiğinden ve dizinin filtreleme ve sıralama için kullanıldığından emin olun. Açıklama planlarıyla doğrulayın.

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

"Ortanca" satır numarasını hesaplayın. Belki kullanın: median_row = floor(count / 2).

Sonra listeden seçin:

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

Bu size istediğiniz değeri içeren bir satır döndürmelidir.

Jacob


6
@rob lütfen düzenlemeye yardımcı olabilir misiniz? Yoksa sadece cırt çözümüne boyun eğmeli miyim? (aslında başka bir çözüme nasıl
ertelendiğinden

1
Büyük tablolar için çok yavaş olan "çapraz birleştirme" yaptığını unutmayın.
Rick James

1
Bu yanıt , çift sayıda satır için hiçbir şey döndürmez .
kuttumiah

Bu cevap bazı veri kümeleri için hiç işe yaramaz, örneğin, 0.1, 0.1, 0.1, 2 değerlerine sahip önemsiz veri kümesi - tüm değerler farklıysa çalışır, ancak yalnızca değerler çalışıyorsa çalışır
Kem Mason

32

Kabul edilen çözümün boş bir kümeyi döndürerek MySQL kurulumumda çalışmadığını gördüm, ancak bu sorgu benim test ettiğim tüm durumlarda benim için çalıştı:

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
kesinlikle doğru, dizinli tablolarımda mükemmel ve çok hızlı çalışıyor
Rob

2
buradaki tüm cevaplardan mysql üzerinde en hızlı çözüm gibi görünüyor, 200ms tabloda sadece kısa bir kayıtla
Rob

3
@FrankConijn: Bir tablodan iki kez seçim yapar. Tablonun adı datave iki ad ile kullanılıyor xve y.
Brian

3
sadece 33k satır içeren bir tabloda bu kesin sorgu ile benim mysqld durdu diyerek ...
Xenonite

1
Bu sorgu çift sayıda satır için yanlış cevap döndürür .
kuttumiah

26

Ne yazık ki, ne TheJacobTaylor'un ne de velcrow'un cevapları MySQL'in mevcut sürümleri için doğru sonuçlar getirmiyor.

Velcro'nun yukarıdan cevabı yakındır, ancak çift sayıda satıra sahip sonuç kümeleri için doğru hesaplamaz. Medyalar 1) tek sayılı kümelerdeki orta sayı veya 2) çift sayı kümelerindeki iki orta sayının ortalaması olarak tanımlanır.

Yani, velcro'nun çözümü hem tek hem de çift sayı kümelerini işlemek için yamalı:

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;

Bunu kullanmak için şu 3 kolay adımı izleyin:

  1. Yukarıdaki koddaki "median_table" (2 tekrar) yerine tablonuzun adını yazın
  2. "Median_column" (3 tekrar) değerini, bir medyan bulmak istediğiniz sütun adıyla değiştirin
  3. WHERE koşulunuz varsa, "WHERE 1" (2 tekrar) değerini bulunduğunuz koşulla değiştirin

Ve, dize değerlerinin Medyanı için ne yaparsınız?
Rick James

12

Daha hızlı bir yol öneriyorum.

Satır sayısını alın:

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

Ardından sıralı bir alt sorgudaki orta değeri alın:

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

Bunu rastgele sayıların 5x10e6 veri kümesiyle test ettim ve 10 saniyenin altında medyanı bulacak.


3
Neden olmasın: VERİDEN val SEÇİN SİPARİŞ TARAFı val val sınırı, 1
Bryan

1
İlk kod bloğunuzun değişken çıktısını ikinci kod bloğunuza nasıl çekersiniz?
Yolculuk

3
Olduğu gibi, orta değer nereden geliyor?
Yolculuk

@Bryan - Sana katılıyorum, bu benim için çok daha mantıklı. Hiç böyle yapmamak için bir neden buldun mu?
Shane N

5
Limit yan tümcesinde bir değişken kullanılamadığından bu çalışmaz.
codepk

8

MySQL belgelerinde bu sayfada yapılan bir yorum şu öneriye sahiptir:

-- (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, bu açıkça karmaşık bir alt
kümeden

Benim için iyi çalışıyor. 5.6.14 MySQL Topluluk Sunucusu. 11M kayıtlara sahip tabloda (diskte yaklaşık 20 Gb), iki birincil dizin yoktur (model_kimliği, fiyat). Tabloda (filtrasyondan sonra) medyanı hesaplamak için 500K kaydımız var. Sonuç olarak 30K kaydımız var (model_id, median_price). Sorgu süresi 1.5-2 saniyedir. Hız benim için Hızlı.
Mikl


6

Yukarıdaki çözümlerin çoğu yalnızca tablonun bir alanı için çalışır, sorgudaki birçok alan için medyan (50. persentil) almanız gerekebilir.

Bunu kullanıyorum:

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;

Yukarıdaki "50" yerine herhangi bir yüzdelik dilimi değiştirebilirsiniz, çok etkilidir.

GROUP_CONCAT için yeterli belleğe sahip olduğunuzdan emin olun, bunu aşağıdakilerle değiştirebilirsiniz:

SET group_concat_max_len = 10485760; #10MB max length

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


Unutmayın: Eşit sayıda değer için iki orta değerin üstünü alır. Oran sayısı için ortancadan sonraki yüksek değeri alır.
giordano

6

Ben HackerRank buldum bu kodu var ve oldukça basit ve her durumda çalışır.

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
Bu sadece girişleri tuhaf bir tablo ile çalışır inanıyorum. Eşit sayıda giriş için bunun bir sorunu olabilir.
Y. Chang

4

Velcro'nun cevabını oluşturmak, başka bir parametre ile gruplandırılmış bir şeyden medyan yapmak zorunda olanlar için:

Grp_field , t1 SEÇ .val FROM ( SELECT grp_field , @ rownum : = IF (@ s = grp_field , @ rownum + 1 , 0 ) AS , @ s : = IF (@ s = grp_field , @ s , grp_field ) AS saniye , d . val
   FROM verileri d , SEÇ 
         row_number
        ( @ rownum : = 0 , @ s : = 0 ) r
   ORDER BY grp_field , d . val
 ) olarak t1 artır ( SEC grp_field , sayısı (*) olarak TOTAL_ROWS
   KAYNAKLANAN veri d
   GRUP İLE grp_field
 ) olarak t2
 AÇIK t1 . grp_field = t2 . grp_field
 NEREDE t1 . = kat    
     satır numarası ( TOTAL_ROWS / 2 ) +1 ;


3

Burada bulunan kullanıcı tanımlı işlevi kullanabilirsiniz .


3
Bu en yararlı görünüyor, ancak mysql benim üretim sunucusuna çökmesine neden olabilir kararsız alfa yazılımı yüklemek istemiyorum :(
davr

6
Bu nedenle, ilgili işlev için kaynaklarını inceleyin, bunları düzeltin veya gerektiği gibi değiştirin ve bunu yaptıktan sonra "kendi" kararlı ve alfa olmayan sürümünüzü yükleyin - benzer şekilde daha az kanıtlanmış kod önerilerinden daha kötüsü SO? -)
Alex Martelli

3

Tek bir değer sayımı ile ilgilenir - bu durumda ortadaki iki değerin ortalamasını verir.

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

Kodum, tablolar veya ek değişkenler olmadan verimli:

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
Bu, önemli miktarda veri üzerinde başarısız olacaktır, çünkü GROUP_CONCATböyle bir fonksiyonun içinde kullanıldığında bile 1023 karakterle sınırlıdır.
Rob Van Barajı

2

İsteğe bağlı olarak, bunu saklı bir yordamda da yapabilirsiniz:

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

Bunun için teşekkürler! Kullanıcı eksik değerlerin (NULL) değer olarak kabul edildiğini bilmelidir. Bu sorunu önlemek için, 'x IS NOT NULL değil' koşulunu ekleyin.
giordano

1
@giordano Kodun hangi satırına x IS NOT NULLeklenmelidir?
Przemyslaw Remin

1
@PrzemyslawRemin Özür dilerim, ifademde net değildim ve şimdi SP'nin eksik değerler durumunu zaten dikkate aldığını fark ettim. SP bu şekilde adlandırılan edilmelidir: CALL median("table","x","x IS NOT NULL").
giordano

2

Aşağıda sunulan çözümüm tablo, değişken ve hatta alt sorgu oluşturmadan sadece bir sorguda çalışır. Ayrıca, grup-sorgularında her grup için medyan almanızı sağlar (bu ihtiyacım olan şey!):

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 ve substring_index'in akıllı kullanımı nedeniyle çalışır.

Ancak, büyük group_concat'e izin vermek için group_concat_max_len değerini daha yüksek bir değere ayarlamanız gerekir (varsayılan olarak 1024 karakter). Bunu şu şekilde ayarlayabilirsiniz (geçerli sql oturumu için):

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

Group_concat_max_len için daha fazla bilgi: https://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_group_concat_max_len


2

Velcrow'un cevabındaki başka bir riff, ancak tek bir ara tablo kullanıyor ve hesaplamak için ekstra bir sorgu yapmak yerine, sayıyı almak için satır numaralandırma için kullanılan değişkenten yararlanıyor. Ayrıca sayımı başlatır, böylece sadece taban ve tavanın ortalama satır (lar) ı seçmesine izin vermek için satır 0 olur.

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

Yukarıdaki benim için çalışıyor gibi görünüyor.


Bu Örneğin, değerlerin bile sayıda ortancasını doğru ortancasını döndürmüyordur {98,102,102,98}IS 100ancak kod verir 102. Tek sayılar için iyi çalıştı.
Nomiluks

1

İki sorgu yaklaşımı kullandım:

  • sayım, min, maks ve ort.
  • ikincisi (hazırlanmış ifade) "LIMIT @ count / 2, 1" ve "ORDER BY .." cümleleri ile medyan değer elde etmek için

Bunlar bir işlev defn'ye sarılır, böylece tüm değerler bir çağrıdan döndürülebilir.

Aralıklarınız statikse ve verileriniz sık sık değişmiyorsa, bu değerleri önceden hesaplamak / depolamak ve her seferinde sıfırdan sorgulamak yerine depolanan değerleri kullanmak daha verimli olabilir.


1

Ben sadece bir medyan VE yüzdelik çözüm gerekiyordu, ben bu konu bulguları dayalı basit ve oldukça esnek bir işlev yaptı. Projelerime dahil edilmesi kolay "hazır" işlevler bulursam kendim mutlu olduğumu biliyorum, bu yüzden hızla paylaşmaya karar verdim:

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

}

Kullanımı çok kolay, mevcut projemden örnek:

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

İşte benim yolum. Tabii ki, bir prosedüre koyabilirsiniz :-)

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;

Eğer @median_counterikame ederseniz , değişkeni önleyebilirsiniz :

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

Bu şekilde alt sorgu olmadan hem çift hem de tek sayıyı içerir.

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

T2 tablosunun ne olduğunu söyleyebilir misiniz?
xliiv

1

@ Bob'un cevabına dayanarak, bu, sorguyu bazı kriterlere göre gruplandırılmış birden fazla medyan döndürme yeteneğine sahip olacak şekilde genelleştirir.

Örneğin, bir araba partisinde kullanılmış otomobiller için yıl ayına göre gruplandırılmış ortalama satış fiyatı düşünün.

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

Genellikle, Medyan'ı sadece tüm tablo için değil, kimliğimizle ilgili agregalar için hesaplamamız gerekebilir. Başka bir deyişle, her kimliğin çok sayıda kayda sahip olduğu tablonuzdaki her kimlik için medyan hesaplayın. (iyi bir performans ve birçok SQL + 'da çalışır, eşit ve olasılık problemini giderir, farklı Medyan yöntemlerinin performansı hakkında daha fazla bilgi 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;

Umarım yardımcı olur


En iyi çözümdür. Ancak, büyük veri kümeleri için yavaşlar çünkü her kümedeki her öğe için tekrar sayılır. Daha hızlı yapmak için "COUNT (*)" ifadesini alt sorguyu ayırın.
Slava Murygin

1

MySQL, sürüm 8.0'dan beri pencere işlevlerini desteklediğinden, ROW_NUMBERveya DENSE_RANK( Spor sıralamasında olduğu gibi aynı değerlere aynı sıralamayı atadığı için KULLANMAYIN) kullanabilirsiniz 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'in ROW_NUMBER değeri varsa, MEDIAN (bu SQL Server sorgusundan esinlenerek):

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, çift sayıda girişiniz olması durumunda kullanılır.

Grup başına medyan bulmak istiyorsanız, OVER yan tümcelerinizde sadece PARTITION BY grubu kullanın.

soymak


1
Hayır, hayır ROW_NUMBER OVER, KESİNLİKLE, bunların hiçbiri; bu MySql, PostgreSQL, IBM DB2, MS SQL Server gibi gerçek bir DB motoru değil ;-).
Alex Martelli

0

Önceki tüm bunları okuduktan sonra gerçek gereksinimimle eşleşmediler, bu yüzden herhangi bir prosedüre veya karmaşık ifadeye ihtiyaç duymayan kendi GROUP_CONCATbirimi uyguladım , sadece MEDIAN'ı almak ve bir COUNT DIV BY uygulamak istediğim sütundaki tüm değerleri 2 Aşağıdaki sorgu yapar gibi listenin ortasındaki değeri ayıklamak:

(POS, medyanını almak istediğim sütunun adıdır)

(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

Umarım bu birçok yorum bu web sitesinden benim için olduğu gibi birisi için yararlı olabilir.


0

Tam satır sayısını bilerek bu sorguyu kullanabilirsiniz:

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

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


0

Sette ortanca yaşı belirlemek için ihtiyaç duyduğumuz yaklaşık 1 milyar satır içeren bir veritabanım var. Bir milyar satırı sıralamak zordur, ancak bulunabilecek farklı değerleri toplarsanız (yaşları 0 ila 100 arasında değişir), bu listeyi sıralayabilir ve aşağıdaki gibi istediğiniz yüzdelik dilimi bulmak için bazı aritmetik sihir kullanabilirsiniz:

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
;

Bu sorgu, db destekleyen pencere işlevlerine bağlıdır (ROWS UNBOUNDED PRECEDING dahil), ancak aggData CTE'sini kendisiyle birleştirmek ve önceki tüm toplamları hangisini belirlemek için kullanılan 'birikmiş' sütununa toplamak basit bir konudur. değeri belirtilen başlangıç ​​değerini içerir. Yukarıdaki örnek p10, p25, p50 (medyan), p75 ve p90'ı hesaplar.

-Chris


0

Alındığı yer: http://mdb-blog.blogspot.com/2015/06/mysql-find-median-nth-element-without.html

Katılmaksızın başka bir yol öneririm , ancak dizelerle çalışmak

i büyük veri tablolarla kontrol etmedi, ama küçük / orta tablolar gayet iyi çalışıyor.

Buradaki iyi şey, GROUPING tarafından da çalışması böylece birkaç öğe için medyanı iade edebilmesi.

İşte test tablosu için test kodu:

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 

ve her grup için medyanı bulma kodu:

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

Çıktı:

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}" medyanının 24 olması gerektiğini düşünmüyor musunuz?
Nomiluks

0

Bazı durumlarda medyan aşağıdaki gibi hesaplanır:

"Ortanca", değere göre sıralandıklarında sayılar listesindeki "orta" değerdir. Çift sayım kümeleri için, medyan iki orta değerin ortalamasıdır . Bunun için basit bir kod oluşturdum:

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

Geri dönüş $ medyan gerekli sonuç olurdu :-)

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.