วิธีง่ายๆในการคำนวณค่ามัธยฐานด้วย 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 ไม่มีฟังก์ชั่นในการคำนวณค่ามัธยฐาน ไร้สาระ.
โมนิก้า Heddneck

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

Steve Cohenชี้ให้เห็นว่าหลังจากผ่านครั้งแรก @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
มีวิธีใดบ้างในการแสดงค่ากลุ่ม like: place / median สำหรับสถานที่นั้น ... เช่น select place, median_value จากตาราง ... ไม่ว่าจะด้วยวิธีใด? ขอบคุณ
saulob

2
@rowNum จะมี 'จำนวนรวม' เมื่อสิ้นสุดการดำเนินการ ดังนั้นคุณสามารถใช้ว่าถ้าคุณต้องการที่จะหลีกเลี่ยงการทำ 'นับ' อีกครั้ง (ซึ่งเป็นกรณีของฉันเพราะแบบสอบถามของฉันไม่ได้ง่ายดังนั้น)
อาเหม็ดอานัส-

ตรรกะของการมีหนึ่งคำสั่ง: (floor ((total_rows + 1) / 2), floor ((total_rows + 2) / 2)) คำนวณแถวที่จำเป็นสำหรับค่ามัธยฐานนั้นยอดเยี่ยมมาก! ไม่แน่ใจว่าคุณคิดอย่างไร แต่ก็ยอดเยี่ยม ส่วนที่ฉันไม่ได้ติดตามคือ (SELECT @rownum: = 0) r - วัตถุประสงค์นี้ให้บริการอะไร
Shanemeister

เปลี่ยนคนแรกที่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))
Daniel Buckmaster

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 คุณสามารถช่วยแก้ไขได้มั้ย หรือฉันควรโค้งคำนับกับสารละลาย velcrow? (ไม่แน่ใจว่าจะเลื่อนไปใช้วิธีแก้ไขปัญหาอื่นได้อย่างไร) ขอบคุณ Jacob
TheJacobTaylor

1
โปรดทราบว่ามันเป็น "cross join" ซึ่งช้ามากสำหรับตารางขนาดใหญ่
Rick James

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 ของคำตอบทั้งหมดที่นี่ 200ms มีเพียงสั้น ๆ ของล้านระเบียนในตาราง
ร็อบ

3
@FrankConijn: มันเลือกจากหนึ่งตารางสองครั้ง ชื่อตารางที่เป็นdataและมันจะถูกนำมาใช้กับสองชื่อและx y
Brian

3
เพียงแค่บอกว่าฉันจนตรอก mysqld ของฉันกับแบบสอบถามนี้แน่นอนบนโต๊ะที่มี 33k แถว ...
Xenonite

1
แบบสอบถามนี้ส่งกลับคำตอบที่ผิดสำหรับแม้จำนวนแถว
kuttumiah

26

น่าเสียดายที่คำตอบของ TheJacobTaylor และ velcrow ไม่ได้ผลลัพธ์ที่แม่นยำสำหรับ MySQL รุ่นปัจจุบัน

คำตอบของ Velcro จากด้านบนนั้นปิด แต่ก็ไม่สามารถคำนวณได้อย่างถูกต้องสำหรับชุดผลลัพธ์ที่มีจำนวนแถวเท่ากัน มัธยฐานถูกกำหนดเป็น 1) หมายเลขกลางในชุดเลขคี่หรือ 2) ค่าเฉลี่ยของตัวเลขกลางสองตัวบนชุดเลขคู่

ดังนั้นนี่คือวิธีแก้ปัญหาของ velcro ที่ได้รับการติดตั้งเพื่อจัดการทั้งชุดเลขคู่และคู่:

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

หากต้องการใช้สิ่งนี้ให้ทำตาม 3 ขั้นตอนง่าย ๆ เหล่านี้:

  1. แทนที่ "median_table" (เกิดขึ้น 2 ครั้ง) ในรหัสด้านบนด้วยชื่อตารางของคุณ
  2. แทนที่ "median_column" (3 ครั้ง) ด้วยชื่อคอลัมน์ที่คุณต้องการหาค่ามัธยฐาน
  3. หากคุณมีเงื่อนไขตำแหน่งให้แทนที่ "ตำแหน่งที่ 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 จาก data เรียงตาม val limit @middlevalue, 1
Bryan

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 

IMHO อันนี้ดีที่สุดสำหรับสถานการณ์ที่คุณต้องการค่ามัธยฐานจากเซตย่อยที่ซับซ้อน (ฉันจำเป็นต้องคำนวณค่ามัธยฐานแยกต่างหากของชุดข้อมูลย่อยจำนวนมาก)
mblackwell8 8

ทำงานได้ดีสำหรับฉัน 5.6.14 เซิร์ฟเวอร์ชุมชน MySQL ตารางที่มีระเบียน 11M (ประมาณ 20Gb บนดิสก์) มีสองดัชนีหลักที่ไม่ (model_id ราคา) ในตาราง (หลังจากการกรอง) เรามีบันทึก 500K เพื่อคำนวณค่ามัธยฐานสำหรับ ในผลลัพธ์เรามีบันทึก 30K (model_id, median_price) ระยะเวลาการสืบค้นคือ 1.5-2 วินาที ความเร็วนั้นเร็วสำหรับฉัน
Mikl

7

ติดตั้งและใช้ฟังก์ชันทางสถิติ mysql นี้: http://www.xarg.org/2012/07/statistical-functions-in-mysql/

หลังจากนั้นคำนวณค่ามัธยฐานเป็นเรื่องง่าย:

SELECT median(val) FROM data;

1
ฉันแค่ลองเองและสิ่งที่คุ้มค่าการติดตั้งมันเร็วมาก / ง่ายและมันก็ทำงานได้ตามที่โฆษณารวมถึงการจัดกลุ่มเช่น "เลือกชื่อมัธยฐาน (x) จากกลุ่ม t1 ตามชื่อ" - แหล่ง github ที่นี่: github.com/infusion/udf_infusion
Kem Mason

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

สร้างคำตอบของเวลโครสำหรับคนที่คุณต้องทำสิ่งที่มีค่ามัธยฐานของสิ่งที่จัดกลุ่มตามพารามิเตอร์อื่น:

SELECT grp_field , t1 val จาก( เลือกgrp_field , @ rownum : = IF (@ s = grp_field , @ rownum + 1 , 0 ) AS , @ s : = IF (@ s = grp_field , @ rownum : = 0 , @ s : = 0 ) r
   ORDER BY grp_field , d 
         row_number
     @ s , grp_field ) AS วินาที, d . val
   FROM data d , เลือก    (   . val
 ) เป็นt1 JOIN ( เลือกgrp_field , นับ(*) เป็นtotal_rows
   จากdata d
   GROUP BY grp_field
  
     ) เป็นT2
 ON t1 grp_field = t2 grp_field
 WHERE t1  row_number= floor( total_rows / 2 ) +1 ;


3

คุณสามารถใช้ฟังก์ชั่นที่ผู้ใช้กำหนดว่าพบที่นี่


3
สิ่งนี้มีประโยชน์มากที่สุด แต่ฉันไม่ต้องการติดตั้งซอฟต์แวร์อัลฟ่าที่ไม่เสถียรซึ่งอาจทำให้ mysql เกิดความผิดพลาดบนเซิร์ฟเวอร์ที่ใช้งานจริงของฉัน :(
davr

6
ดังนั้นศึกษาแหล่งที่มาของฟังก์ชั่นที่น่าสนใจแก้ไขหรือปรับเปลี่ยนได้ตามต้องการและติดตั้ง "ของคุณเอง" ที่เสถียรและไม่ใช่อัลฟ่าเมื่อคุณสร้างมันขึ้นมา คุณจะใช้ SO? -)
Alex Martelli

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จำกัด ไว้ที่ 1,023 ตัวอักษรแม้ว่าจะใช้ในฟังก์ชั่นอื่นเช่นนี้
Rob Van Dam

2

คุณสามารถเลือกทำเช่นนี้ได้ในขั้นตอนการจัดเก็บ:

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

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

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


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

ขอบคุณสำหรับสิ่งนี้! ผู้ใช้ควรทราบว่าค่าที่หายไป (NULL) ถือเป็นค่า เพื่อหลีกเลี่ยงปัญหานี้เพิ่ม 'x IS NULL ที่เงื่อนไข
giordano

1
@giordano x IS NOT NULLควรเพิ่มรหัสใดในบรรทัด
Przemyslaw Remin

1
@PrzemyslawRemin ขออภัยฉันไม่ชัดเจนในคำแถลงของฉันและฉันรู้แล้วว่า SP ได้พิจารณากรณีของค่าที่หายไปแล้ว 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/en/server-system-variables.html#sysvar_group_concat_max_len


2

คำตอบอื่น ๆ ของ Velcrow แต่ใช้ตารางกลางเดียวและใช้ประโยชน์จากตัวแปรที่ใช้ในการกำหนดหมายเลขแถวเพื่อรับจำนวนแทนที่จะดำเนินการแบบสอบถามพิเศษเพื่อคำนวณ เริ่มต้นการนับด้วยเพื่อให้แถวแรกคือแถว 0 เพื่อให้สามารถใช้งาน Floor และ Ceil เพื่อเลือกแถวค่ามัธยฐานได้

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

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

ดูเหมือนว่าจะใช้งานได้สำหรับฉัน


มันไม่ได้กลับมาเฉลี่ยที่ถูกต้องสำหรับจำนวนคู่ของค่าตัวอย่างเช่นค่ามัธยฐานของ{98,102,102,98}มีแต่รหัสของคุณจะช่วยให้100 102มันทำงานได้ดีสำหรับตัวเลขคี่
Nomiluks

1

ฉันใช้วิธีการสืบค้นสองแบบ:

  • คนแรกที่จะได้รับการนับนาทีสูงสุดและเฉลี่ย
  • ประโยคที่สอง (คำสั่งที่เตรียมไว้) ที่มี "LIMIT @ count / 2, 1" และคำสั่ง "ORDER BY .. " เพื่อรับค่ามัธยฐาน

สิ่งเหล่านี้ถูกห่อหุ้มอยู่ในฟังก์ชั่น defn ดังนั้นค่าทั้งหมดสามารถส่งคืนจากการโทรครั้งเดียว

หากช่วงของคุณเป็นแบบคงที่และข้อมูลของคุณไม่เปลี่ยนแปลงบ่อยครั้งอาจมีประสิทธิภาพมากกว่าในการคำนวณล่วงหน้า / เก็บค่าเหล่านี้และใช้ค่าที่เก็บไว้แทนการสอบถามตั้งแต่เริ่มต้นทุกครั้ง


1

เนื่องจากฉันต้องการโซลูชันมัธยฐานและเปอร์เซ็นไทล์ฉันได้สร้างฟังก์ชันที่เรียบง่ายและมีความยืดหยุ่นมากตามการค้นพบในหัวข้อนี้ ฉันรู้ว่าฉันมีความสุขถ้าฉันพบฟังก์ชั่น "readymade" ที่ง่ายต่อการรวมไว้ในโครงการของฉันดังนั้นฉันจึงตัดสินใจที่จะแบ่งปันอย่างรวดเร็ว:

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ถ้าคุณ substitude มัน:

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

บ่อยครั้งที่เราอาจต้องคำนวณค่ามัธยฐานไม่เพียง แต่สำหรับทั้งตาราง แต่สำหรับมวลรวมที่เกี่ยวข้องกับ ID ของเรา ในคำอื่น ๆ คำนวณค่ามัธยฐานสำหรับแต่ละ ID ในตารางของเราที่แต่ละ ID มีหลายระเบียน (ประสิทธิภาพที่ดีและทำงานได้ใน 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 (*)" เพื่อแยกแบบสอบถามย่อย
Slava Murygin

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

ใช้ในกรณีที่คุณมีจำนวนรายการที่เท่ากัน

หากคุณต้องการหาค่ามัธยฐานต่อกลุ่มคุณก็แค่แบ่งพาร์ทิชั่นตามกลุ่มในข้อ OVER ของคุณ

ปล้น


1
ไม่, ไม่มีROW_NUMBER OVER, ไม่มีส่วนร่วม, ไม่มีสิ่งนั้น; นี่คือ MySql ไม่ใช่เอ็นจิน DB จริงเช่น PostgreSQL, IBM DB2, MS SQL Server และอื่น ๆ ;-)
Alex Martelli

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 ด้วยตัวเองและรวมผลรวมก่อนหน้าทั้งหมดลงในคอลัมน์ 'สะสม' ซึ่งใช้เพื่อกำหนดว่า value มี precentile ที่ระบุ ตัวอย่างข้างต้นคำนวณค่า p10, p25, p50 (ค่ามัธยฐาน), p75 และ p90

คริส


0

นำมาจาก: http://mdb-blog.blogspot.com/2015/06/mysql-find-median-nth-element-without.html

ฉันจะแนะนำวิธีอื่นโดยไม่ต้องเข้าร่วมแต่ทำงานกับสตริง

ฉันไม่ได้ตรวจสอบกับตารางที่มีข้อมูลขนาดใหญ่ แต่ตารางขนาดเล็ก / ขนาดกลางก็ใช้ได้ดี

สิ่งที่ดีที่นี่มันทำงานได้โดยการจัดกลุ่มเพื่อให้สามารถส่งค่ามัธยฐานสำหรับหลายรายการ

นี่คือรหัสทดสอบสำหรับตารางทดสอบ:

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

SELECT 'note', 11 UNION ALL

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

และรหัสสำหรับการหาค่ามัธยฐานสำหรับแต่ละกลุ่ม:

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

เอาท์พุท:

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

คุณไม่คิดว่าค่ามัธยฐานของ "{22,26}" ควรเป็น 24 หรือไม่?
Nomiluks

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;

$ มัธยฐานที่ส่งคืนจะเป็นผลลัพธ์ที่ต้องการ :-)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.