ฟังก์ชั่นการคำนวณค่ามัธยฐานใน SQL Server


227

ตามMSDN , ค่ามัธยฐานไม่พร้อมใช้งานเป็นฟังก์ชันรวมใน Transact-SQL อย่างไรก็ตามฉันต้องการค้นหาว่าเป็นไปได้ที่จะสร้างฟังก์ชันนี้หรือไม่ (โดยใช้ฟังก์ชั่นสร้างการรวมฟังก์ชันที่ผู้ใช้กำหนดเองหรือวิธีอื่น)

อะไรจะเป็นวิธีที่ดีที่สุด (ถ้าเป็นไปได้) ในการทำเช่นนี้ - อนุญาตให้มีการคำนวณค่ามัธยฐาน (สมมติว่าเป็นชนิดข้อมูลตัวเลข) ในแบบสอบถามรวม?


คำตอบ:


145

2019 UPDATE:ใน 10 ปีนับตั้งแต่ที่ฉันเขียนคำตอบนี้ได้มีการค้นพบคำตอบเพิ่มเติมที่อาจให้ผลลัพธ์ที่ดีกว่า นอกจากนี้ SQL Server เปิดตัวตั้งแต่นั้นมา (โดยเฉพาะ SQL 2012) ได้เปิดตัวฟีเจอร์ T-SQL ใหม่ที่สามารถใช้ในการคำนวณค่ามัธยฐาน SQL Server รีลีสได้ปรับปรุงตัวเพิ่มประสิทธิภาพการสืบค้นซึ่งอาจส่งผลกระทบต่อการแก้ปัญหาต่าง ๆ Net-net โพสต์ต้นฉบับปี 2009 ของฉันยังคงใช้ได้ แต่อาจมีวิธีแก้ปัญหาที่ดีกว่าสำหรับแอพ SQL Server ที่ทันสมัย ดูบทความนี้จาก 2012 ซึ่งเป็นแหล่งข้อมูลที่ดี: https://sqlperformance.com/2012/08/t-sql-queries/median

บทความนี้พบว่ารูปแบบต่อไปนี้จะเร็วกว่าทางเลือกอื่น ๆ อย่างน้อยที่สุดบนสคีมาอย่างง่ายที่ทดสอบ โซลูชันนี้เร็วกว่า 373x (!!!) เร็วกว่าPERCENTILE_CONTโซลูชันที่ช้าที่สุด ( ) โปรดทราบว่าเคล็ดลับนี้ต้องใช้แบบสอบถามสองชุดแยกกันซึ่งอาจไม่สามารถใช้ได้ในทุกกรณี นอกจากนี้ยังต้องการ SQL 2012 หรือใหม่กว่า

DECLARE @c BIGINT = (SELECT COUNT(*) FROM dbo.EvenRows);

SELECT AVG(1.0 * val)
FROM (
    SELECT val FROM dbo.EvenRows
     ORDER BY val
     OFFSET (@c - 1) / 2 ROWS
     FETCH NEXT 1 + (1 - @c % 2) ROWS ONLY
) AS x;

แน่นอนเพียงเพราะการทดสอบหนึ่งครั้งในหนึ่งสคีมาในปี 2012 ให้ผลลัพธ์ที่ยอดเยี่ยมระยะทางของคุณอาจแตกต่างกันไปโดยเฉพาะอย่างยิ่งถ้าคุณใช้ SQL Server 2014 หรือใหม่กว่า หาก perf เป็นสิ่งสำคัญสำหรับการคำนวณค่ามัธยฐานของคุณฉันขอแนะนำให้คุณลองและทดสอบตัวเลือกหลายอย่างที่แนะนำในบทความนั้นเพื่อให้แน่ใจว่าคุณได้พบสิ่งที่ดีที่สุดสำหรับสคีมาของคุณ

ฉันยังต้องระมัดระวังเป็นพิเศษในการใช้ฟังก์ชัน (ใหม่ใน SQL Server 2012) PERCENTILE_CONTที่แนะนำในคำตอบอื่นสำหรับคำถามนี้เนื่องจากบทความที่ลิงก์ด้านบนพบว่าฟังก์ชันในตัวนี้ช้ากว่าโซลูชันที่เร็วที่สุด 373 เท่า เป็นไปได้ว่าความแตกต่างนี้ได้รับการปรับปรุงใน 7 ปีตั้งแต่ แต่โดยส่วนตัวแล้วฉันจะไม่ใช้ฟังก์ชั่นนี้บนโต๊ะขนาดใหญ่จนกว่าฉันจะตรวจสอบประสิทธิภาพกับโซลูชั่นอื่น ๆ

ORIGINAL 2009 POST อยู่ด้านล่าง:

มีหลายวิธีในการทำเช่นนี้ด้วยประสิทธิภาพที่แตกต่างกันอย่างมาก นี่คือหนึ่งในวิธีการแก้ปัญหาโดยเฉพาะอย่างยิ่งที่ดีที่สุดจากมีเดีย, ROW_NUMBERs และประสิทธิภาพการทำงาน นี่เป็นทางออกที่ดีที่สุดโดยเฉพาะเมื่อพูดถึง I / O จริงที่เกิดขึ้นระหว่างการดำเนินการ - มันดูมีราคาแพงกว่าโซลูชันอื่น ๆ แต่จริงๆแล้วเร็วกว่ามาก

หน้านั้นยังมีการอภิปรายของโซลูชั่นอื่น ๆ และรายละเอียดการทดสอบประสิทธิภาพ สังเกตการใช้คอลัมน์ที่ไม่ซ้ำกันเป็น disambiguator ในกรณีที่มีหลายแถวที่มีค่าเดียวกันของคอลัมน์ค่ามัธยฐาน

เช่นเดียวกับสถานการณ์ฐานข้อมูลประสิทธิภาพทั้งหมดพยายามทดสอบวิธีแก้ปัญหาด้วยข้อมูลจริงบนฮาร์ดแวร์จริง - คุณไม่มีทางรู้ว่าการเปลี่ยนออพติไมเซอร์ของ SQL Server หรือลักษณะเฉพาะในสภาพแวดล้อมของคุณจะทำให้โซลูชันรวดเร็วขึ้นตามปกติ

SELECT
   CustomerId,
   AVG(TotalDue)
FROM
(
   SELECT
      CustomerId,
      TotalDue,
      -- SalesOrderId in the ORDER BY is a disambiguator to break ties
      ROW_NUMBER() OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue ASC, SalesOrderId ASC) AS RowAsc,
      ROW_NUMBER() OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue DESC, SalesOrderId DESC) AS RowDesc
   FROM Sales.SalesOrderHeader SOH
) x
WHERE
   RowAsc IN (RowDesc, RowDesc - 1, RowDesc + 1)
GROUP BY CustomerId
ORDER BY CustomerId;

12
ฉันไม่คิดว่ามันจะใช้งานได้หากคุณมีข้อมูลแบบคู่โดยเฉพาะอย่างยิ่งจำนวนมากในข้อมูลของคุณ คุณไม่สามารถรับประกันแถว row_number ได้ คุณสามารถได้คำตอบที่บ้าจริง ๆ สำหรับค่ามัธยฐานของคุณหรือแย่กว่านั้นโดยไม่มีค่ามัธยฐานเลย
Jonathan Beerhalter

26
นั่นเป็นเหตุผลที่การมี disambiguator (SalesOrderId ในตัวอย่างโค้ดด้านบน) มีความสำคัญดังนั้นคุณจึงสามารถมั่นใจได้ว่าลำดับของแถวชุดผลลัพธ์มีความสอดคล้องกันทั้งไปข้างหลังและไปข้างหน้า บ่อยครั้งที่คีย์หลักที่ไม่ซ้ำกันทำให้ disambiguator ในอุดมคติเพราะสามารถใช้ได้โดยไม่ต้องค้นหาดัชนีแยกต่างหาก หากไม่มีคอลัมน์ disambiguation (ตัวอย่างเช่นถ้าตารางไม่มีคีย์ที่ไม่ซ้ำกัน) คุณต้องใช้วิธีอื่นในการคำนวณค่ามัธยฐานเนื่องจากคุณชี้ให้เห็นอย่างถูกต้องหากคุณไม่สามารถรับประกันได้ว่าหมายเลขแถว DESC เป็นภาพสะท้อนของ หมายเลขแถว ASC จากนั้นผลลัพธ์จะไม่สามารถคาดเดาได้
Justin Grant

4
ขอบคุณเมื่อเปลี่ยนคอลัมน์ไปยังฐานข้อมูลของฉันฉันลดความน่าเชื่อถือโดยคิดว่ามันไม่เกี่ยวข้อง ในกรณีนี้วิธีนี้ใช้งานได้ดีจริงๆ
Jonathan Beerhalter

8
ฉันขอแนะนำให้เพิ่มความคิดเห็นในโค้ดเองโดยอธิบายถึงความต้องการตัวแก้ปัญหา
hoffmanc

4
! น่ากลัว ฉันรู้จักความสำคัญมานานแล้ว แต่ตอนนี้ฉันสามารถตั้งชื่อ ... disambiguator! ขอบคุณจัสติน!
CodeMonkey

204

หากคุณกำลังใช้ SQL 2005 หรือดีกว่านี่เป็นการคำนวณค่ามัธยฐานที่ใช้ง่ายสำหรับคอลัมน์เดียวในตาราง:

SELECT
(
 (SELECT MAX(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts ORDER BY Score) AS BottomHalf)
 +
 (SELECT MIN(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts ORDER BY Score DESC) AS TopHalf)
) / 2 AS Median

62
นั่นเป็นเรื่องที่ฉลาดและค่อนข้างง่ายเนื่องจากไม่มีฟังก์ชั่นรวม Median () แต่มันไม่มีฟังก์ชั่น Median () ได้อย่างไร!? ฉันเป็น FLOOR () ed ตรงไปตรงมา
Charlie Kilian

ดีดีและง่าย แต่ usualy select gid, median(score) from T group by gidคุณจำเป็นต้องได้เฉลี่ยต่อหมวดหมู่บางกลุ่มเช่นเช่น คุณต้องการแบบสอบถามย่อยที่มีความสัมพันธ์สำหรับสิ่งนั้นหรือไม่?
TMS

1
... ฉันหมายถึงชอบในกรณีนี้ (ข้อความค้นหาที่สองชื่อ "ผู้ใช้ที่มีคะแนนคำตอบเฉลี่ยสูงสุด")
TMS

โทมัส - คุณจัดการเพื่อแก้ปัญหา "ตามหมวดหมู่บางกลุ่ม" ของคุณหรือไม่? เช่นฉันมีปัญหาเดียวกัน ขอบคุณ
Stu Harper

3
วิธีใช้โซลูชันนี้กับ GROUP BY
Przemyslaw Remin

82

ใน SQL Server 2012 คุณควรใช้PERCENTILE_CONT :

SELECT SalesOrderID, OrderQty,
    PERCENTILE_CONT(0.5) 
        WITHIN GROUP (ORDER BY OrderQty)
        OVER (PARTITION BY SalesOrderID) AS MedianCont
FROM Sales.SalesOrderDetail
WHERE SalesOrderID IN (43670, 43669, 43667, 43663)
ORDER BY SalesOrderID DESC

ดูเพิ่มเติมที่: http://blog.sqlauthority.com/2011/11/20/sql-server-introduction-to-percentile_cont-analytic-functions-introduced-in-sql-server-2012/


12
การวิเคราะห์โดยผู้เชี่ยวชาญนี้สร้างข้อโต้แย้งที่น่าสนใจสำหรับฟังก์ชัน PERCENTILE เนื่องจากประสิทธิภาพต่ำ sqlperformance.com/2012/08/t-sql-queries/median
carl.anderson

4
คุณไม่จำเป็นต้องเพิ่มDISTINCTหรือGROUPY BY SalesOrderID? มิฉะนั้นคุณจะมีแถวที่ซ้ำกันจำนวนมาก
Konstantin

1
นี่คือคำตอบ ไม่รู้ว่าทำไมฉันต้องเลื่อนไปไกลขนาดนี้
FistOfFury

นอกจากนี้ยังมีรุ่นที่ใช้อย่างระมัดระวังPERCENTILE_DISC
johnDanger

เน้นจุดของ @ carl.anderson ด้านบน: โซลูชัน PERCENTILE_CONT วัดได้ช้ากว่า 373x (!!!!) เมื่อเทียบกับโซลูชันที่เร็วที่สุดที่ทดสอบบน SQL Server 2012 ในสคีมาทดสอบเฉพาะของพวกเขา อ่านบทความที่ carl เชื่อมโยงเพื่อดูรายละเอียดเพิ่มเติม
Justin Grant

21

คำตอบด่วนต้นฉบับของฉันคือ:

select  max(my_column) as [my_column], quartile
from    (select my_column, ntile(4) over (order by my_column) as [quartile]
         from   my_table) i
--where quartile = 2
group by quartile

สิ่งนี้จะทำให้คุณมีค่ามัธยฐานและพิสัยระหว่างควอไทล์ในการลดลงหนึ่งครั้ง หากคุณต้องการเพียงหนึ่งแถวนั่นคือค่ามัธยฐานแล้ว uncomment ในส่วนคำสั่งที่

เมื่อคุณยึดติดกับแผนการอธิบาย 60% ของงานเรียงลำดับข้อมูลที่ไม่สามารถหลีกเลี่ยงได้เมื่อคำนวณสถิติขึ้นอยู่กับตำแหน่งเช่นนี้

ฉันได้แก้ไขคำตอบให้ปฏิบัติตามคำแนะนำที่ยอดเยี่ยมจาก Robert Ševčík-Robajz ในความคิดเห็นด้านล่าง:

;with PartitionedData as
  (select my_column, ntile(10) over (order by my_column) as [percentile]
   from   my_table),
MinimaAndMaxima as
  (select  min(my_column) as [low], max(my_column) as [high], percentile
   from    PartitionedData
   group by percentile)
select
  case
    when b.percentile = 10 then cast(b.high as decimal(18,2))
    else cast((a.low + b.high)  as decimal(18,2)) / 2
  end as [value], --b.high, a.low,
  b.percentile
from    MinimaAndMaxima a
  join  MinimaAndMaxima b on (a.percentile -1 = b.percentile) or (a.percentile = 10 and b.percentile = 10)
--where b.percentile = 5

สิ่งนี้ควรคำนวณค่ามัธยฐานและค่าเปอร์เซ็นต์ไทล์ที่ถูกต้องเมื่อคุณมีรายการข้อมูลจำนวนเท่ากัน อีกครั้ง uncomment สุดท้ายที่ข้อถ้าคุณต้องการค่ามัธยฐานและไม่ใช่การกระจายเปอร์เซ็นต์ทั้งหมด


1
วิธีนี้ใช้งานได้ดีและช่วยให้สามารถแบ่งข้อมูลได้
Jonathan Beerhalter

3
หากตกลงที่จะปิดโดยหนึ่งแล้วแบบสอบถามข้างต้นก็โอเค แต่ถ้าคุณต้องการค่ามัธยฐานที่แน่นอนคุณจะมีปัญหา ตัวอย่างเช่นสำหรับลำดับ (1,3,5,7) ค่ามัธยฐานคือ 4 แต่แบบสอบถามด้านบนส่งคืน 3 สำหรับ (1,2,3,503,603,703) ค่ามัธยฐานคือ 258 แต่แบบสอบถามข้างต้นส่งคืน 503
Justin Grant

1
คุณสามารถแก้ไขข้อบกพร่องของความไม่แน่นอนโดยการหาค่าสูงสุดและค่าต่ำสุดของแต่ละควอไทล์ในแบบสอบถามย่อยแล้ว AVGing MAX ของก่อนหน้านี้และ MIN ต่อไปหรือไม่
Rbjz



4

ง่ายรวดเร็วถูกต้อง

SELECT x.Amount 
FROM   (SELECT amount, 
               Count(1) OVER (partition BY 'A')        AS TotalRows, 
               Row_number() OVER (ORDER BY Amount ASC) AS AmountOrder 
        FROM   facttransaction ft) x 
WHERE  x.AmountOrder = Round(x.TotalRows / 2.0, 0)  

4

หากคุณต้องการใช้ฟังก์ชั่นสร้างการรวมใน SQL Server นี่คือวิธีการทำ การทำเช่นนี้มีประโยชน์ที่จะสามารถเขียนคำค้นหาที่สะอาด โปรดทราบว่ากระบวนการนี้สามารถปรับเปลี่ยนเพื่อคำนวณค่าเปอร์เซ็นไทล์ได้อย่างง่ายดาย

สร้างโครงการ Visual Studio ใหม่และตั้งค่ากรอบงานเป้าหมายเป็น. NET 3.5 (สำหรับ SQL 2008 อาจแตกต่างกันใน SQL 2012) จากนั้นสร้างไฟล์คลาสและใส่รหัสต่อไปนี้หรือเทียบเท่า c #:

Imports Microsoft.SqlServer.Server
Imports System.Data.SqlTypes
Imports System.IO

<Serializable>
<SqlUserDefinedAggregate(Format.UserDefined, IsInvariantToNulls:=True, IsInvariantToDuplicates:=False, _
  IsInvariantToOrder:=True, MaxByteSize:=-1, IsNullIfEmpty:=True)>
Public Class Median
  Implements IBinarySerialize
  Private _items As List(Of Decimal)

  Public Sub Init()
    _items = New List(Of Decimal)()
  End Sub

  Public Sub Accumulate(value As SqlDecimal)
    If Not value.IsNull Then
      _items.Add(value.Value)
    End If
  End Sub

  Public Sub Merge(other As Median)
    If other._items IsNot Nothing Then
      _items.AddRange(other._items)
    End If
  End Sub

  Public Function Terminate() As SqlDecimal
    If _items.Count <> 0 Then
      Dim result As Decimal
      _items = _items.OrderBy(Function(i) i).ToList()
      If _items.Count Mod 2 = 0 Then
        result = ((_items((_items.Count / 2) - 1)) + (_items(_items.Count / 2))) / 2@
      Else
        result = _items((_items.Count - 1) / 2)
      End If

      Return New SqlDecimal(result)
    Else
      Return New SqlDecimal()
    End If
  End Function

  Public Sub Read(r As BinaryReader) Implements IBinarySerialize.Read
    'deserialize it from a string
    Dim list = r.ReadString()
    _items = New List(Of Decimal)

    For Each value In list.Split(","c)
      Dim number As Decimal
      If Decimal.TryParse(value, number) Then
        _items.Add(number)
      End If
    Next

  End Sub

  Public Sub Write(w As BinaryWriter) Implements IBinarySerialize.Write
    'serialize the list to a string
    Dim list = ""

    For Each item In _items
      If list <> "" Then
        list += ","
      End If      
      list += item.ToString()
    Next
    w.Write(list)
  End Sub
End Class

จากนั้นรวบรวมและคัดลอกไฟล์ DLL และ PDB ไปยังเครื่อง SQL Server ของคุณและเรียกใช้คำสั่งต่อไปนี้ใน SQL Server:

CREATE ASSEMBLY CustomAggregate FROM '{path to your DLL}'
WITH PERMISSION_SET=SAFE;
GO

CREATE AGGREGATE Median(@value decimal(9, 3))
RETURNS decimal(9, 3) 
EXTERNAL NAME [CustomAggregate].[{namespace of your DLL}.Median];
GO

จากนั้นคุณสามารถเขียนแบบสอบถามเพื่อคำนวณค่ามัธยฐานดังนี้: SELECT dbo.Median (Field) จากตาราง


3

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

DECLARE @test TABLE(
    i int identity(1,1),
    id int,
    score float
)

INSERT INTO @test (id,score) VALUES (1,10)
INSERT INTO @test (id,score) VALUES (1,11)
INSERT INTO @test (id,score) VALUES (1,15)
INSERT INTO @test (id,score) VALUES (1,19)
INSERT INTO @test (id,score) VALUES (1,20)

INSERT INTO @test (id,score) VALUES (2,20)
INSERT INTO @test (id,score) VALUES (2,21)
INSERT INTO @test (id,score) VALUES (2,25)
INSERT INTO @test (id,score) VALUES (2,29)
INSERT INTO @test (id,score) VALUES (2,30)

INSERT INTO @test (id,score) VALUES (3,20)
INSERT INTO @test (id,score) VALUES (3,21)
INSERT INTO @test (id,score) VALUES (3,25)
INSERT INTO @test (id,score) VALUES (3,29)

DECLARE @counts TABLE(
    id int,
    cnt int
)

INSERT INTO @counts (
    id,
    cnt
)
SELECT
    id,
    COUNT(*)
FROM
    @test
GROUP BY
    id

SELECT
    drv.id,
    drv.start,
    AVG(t.score)
FROM
    (
        SELECT
            MIN(t.i)-1 AS start,
            t.id
        FROM
            @test t
        GROUP BY
            t.id
    ) drv
    INNER JOIN @test t ON drv.id = t.id
    INNER JOIN @counts c ON t.id = c.id
WHERE
    t.i = ((c.cnt+1)/2)+drv.start
    OR (
        t.i = (((c.cnt+1)%2) * ((c.cnt+2)/2))+drv.start
        AND ((c.cnt+1)%2) * ((c.cnt+2)/2) <> 0
    )
GROUP BY
    drv.id,
    drv.start

3

แบบสอบถามต่อไปนี้จะส่งกลับค่ามัธยฐานจากรายการค่าในหนึ่งคอลัมน์ มันไม่สามารถใช้เป็นหรือพร้อมกับฟังก์ชั่นรวม แต่คุณยังสามารถใช้มันเป็นแบบสอบถามย่อยที่มีส่วนคำสั่ง WHERE ในการเลือกภายใน

SQL Server 2005+:

SELECT TOP 1 value from
(
    SELECT TOP 50 PERCENT value 
    FROM table_name 
    ORDER BY  value
)for_median
ORDER BY value DESC

3

แม้ว่าวิธีแก้ปัญหาของจัสตินแกรนท์จะปรากฏว่าเป็นของแข็งฉันพบว่าเมื่อคุณมีค่าซ้ำกันจำนวนหนึ่งภายในพาร์ติชันคีย์ที่กำหนดหมายเลขแถวสำหรับค่า ASC ที่ซ้ำกันจะสิ้นสุดลงตามลำดับดังนั้นจึงไม่สอดคล้องกัน

นี่คือส่วนจากผลลัพธ์ของฉัน:

KEY VALUE ROWA ROWD  

13  2     22   182
13  1     6    183
13  1     7    184
13  1     8    185
13  1     9    186
13  1     10   187
13  1     11   188
13  1     12   189
13  0     1    190
13  0     2    191
13  0     3    192
13  0     4    193
13  0     5    194

ฉันใช้รหัสของจัสตินเป็นพื้นฐานสำหรับการแก้ปัญหานี้ แม้ว่าจะไม่ได้มีประสิทธิภาพเท่าที่ได้รับจากการใช้หลายตารางที่ได้รับ แต่มันก็ช่วยแก้ไขปัญหาการสั่งซื้อแถวที่ฉันพบ การปรับปรุงใด ๆ ที่จะได้รับการต้อนรับเนื่องจากฉันไม่ได้มีประสบการณ์ใน T-SQL

SELECT PKEY, cast(AVG(VALUE)as decimal(5,2)) as MEDIANVALUE
FROM
(
  SELECT PKEY,VALUE,ROWA,ROWD,
  'FLAG' = (CASE WHEN ROWA IN (ROWD,ROWD-1,ROWD+1) THEN 1 ELSE 0 END)
  FROM
  (
    SELECT
    PKEY,
    cast(VALUE as decimal(5,2)) as VALUE,
    ROWA,
    ROW_NUMBER() OVER (PARTITION BY PKEY ORDER BY ROWA DESC) as ROWD 

    FROM
    (
      SELECT
      PKEY, 
      VALUE,
      ROW_NUMBER() OVER (PARTITION BY PKEY ORDER BY VALUE ASC,PKEY ASC ) as ROWA 
      FROM [MTEST]
    )T1
  )T2
)T3
WHERE FLAG = '1'
GROUP BY PKEY
ORDER BY PKEY

2

ตัวอย่างของจัสตินข้างต้นนั้นดีมาก แต่ความต้องการคีย์หลักนั้นควรระบุไว้อย่างชัดเจน ฉันได้เห็นรหัสใน wild โดยไม่มีคีย์และผลลัพธ์ไม่ถูกต้อง

การร้องเรียนที่ฉันได้รับเกี่ยวกับ Percentile_Cont คือมันจะไม่ให้มูลค่าที่แท้จริงจากชุดข้อมูล ในการรับ "ค่ามัธยฐาน" ที่เป็นค่าจริงจากชุดข้อมูลให้ใช้ Percentile_Disc

SELECT SalesOrderID, OrderQty,
    PERCENTILE_DISC(0.5) 
        WITHIN GROUP (ORDER BY OrderQty)
        OVER (PARTITION BY SalesOrderID) AS MedianCont
FROM Sales.SalesOrderDetail
WHERE SalesOrderID IN (43670, 43669, 43667, 43663)
ORDER BY SalesOrderID DESC

2

ใน UDF ให้เขียน:

 Select Top 1 medianSortColumn from Table T
  Where (Select Count(*) from Table
         Where MedianSortColumn <
           (Select Count(*) From Table) / 2)
  Order By medianSortColumn

7
ในกรณีที่มีจำนวนรายการเท่ากันค่ามัธยฐานคือค่าเฉลี่ยของรายการกลางสองค่าซึ่งไม่ได้ครอบคลุมโดย UDF นี้
Yaakov Ellis

1
คุณสามารถเขียนมันซ้ำใน UDF ทั้งหมดได้ไหม?
Przemyslaw Remin

2

ค่ามัธยฐานการค้น

นี่เป็นวิธีที่ง่ายที่สุดในการค้นหาค่ามัธยฐานของแอตทริบิวต์

Select round(S.salary,4) median from employee S where (select count(salary) from station where salary < S.salary ) = (select count(salary) from station where salary > S.salary)

จะจัดการกับกรณีได้อย่างไรเมื่อจำนวนแถวเท่ากัน?
priojeet priyom


1

สำหรับตัวแปร / วัดอย่างต่อเนื่อง 'col1' จาก 'table1'

select col1  
from
    (select top 50 percent col1, 
    ROW_NUMBER() OVER(ORDER BY col1 ASC) AS Rowa,
    ROW_NUMBER() OVER(ORDER BY col1 DESC) AS Rowd
    from table1 ) tmp
where tmp.Rowa = tmp.Rowd

1

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

จำนวนแถวที่จะข้ามคือ (@cnt - 1) / 2 เป็นที่ชัดเจนว่าสำหรับการนับคี่การคำนวณนี้ถูกต้องเพราะคุณลบ 1 ครั้งแรกสำหรับค่ากลางเดียวก่อนที่คุณจะหารด้วย 2

สิ่งนี้ยังทำงานได้อย่างถูกต้องสำหรับการนับคู่เนื่องจากการหารที่ใช้ในนิพจน์คือการหารจำนวนเต็ม ดังนั้นเมื่อลบ 1 จากการนับคู่คุณจะเหลือค่าคี่

เมื่อหารค่าคี่นั้นด้วย 2 ส่วนเศษส่วนของผลลัพธ์ (.5) จะถูกปัดเศษ จำนวนแถวที่จะดึงข้อมูลได้ 2 - (@cnt% 2) แนวคิดคือเมื่อการนับเป็นเลขคี่ผลลัพธ์ของการดำเนินการแบบโมดูโลคือ 1 และคุณต้องดึงข้อมูล 1 แถว เมื่อการนับเป็นผลลัพธ์ของการดำเนินการ modulo คือ 0 และคุณต้องดึงข้อมูล 2 แถว ด้วยการลบผลลัพธ์ 1 หรือ 0 ของการดำเนินการโมดูโลจาก 2 คุณจะได้รับ 1 หรือ 2 ตามลำดับ สุดท้ายเพื่อคำนวณปริมาณมัธยฐานให้ใช้ปริมาณผลลัพธ์หนึ่งหรือสองปริมาณและใช้ค่าเฉลี่ยหลังจากแปลงค่าจำนวนเต็มอินพุตเป็นตัวเลขหนึ่งดังต่อไปนี้:

DECLARE @cnt AS INT = (SELECT COUNT(*) FROM [Sales].[production].[stocks]);
SELECT AVG(1.0 * quantity) AS median
FROM ( SELECT quantity
FROM [Sales].[production].[stocks]
ORDER BY quantity
OFFSET (@cnt - 1) / 2 ROWS FETCH NEXT 2 - @cnt % 2 ROWS ONLY ) AS D;

0

ฉันต้องการหาทางออกด้วยตนเอง แต่สมองของฉันสะดุดและล้มลงระหว่างทาง ฉันคิดว่ามันใช้งานได้ แต่อย่าขอให้ฉันอธิบายในตอนเช้า : P

DECLARE @table AS TABLE
(
    Number int not null
);

insert into @table select 2;
insert into @table select 4;
insert into @table select 9;
insert into @table select 15;
insert into @table select 22;
insert into @table select 26;
insert into @table select 37;
insert into @table select 49;

DECLARE @Count AS INT
SELECT @Count = COUNT(*) FROM @table;

WITH MyResults(RowNo, Number) AS
(
    SELECT RowNo, Number FROM
        (SELECT ROW_NUMBER() OVER (ORDER BY Number) AS RowNo, Number FROM @table) AS Foo
)
SELECT AVG(Number) FROM MyResults WHERE RowNo = (@Count+1)/2 OR RowNo = ((@Count+1)%2) * ((@Count+2)/2)

0
--Create Temp Table to Store Results in
DECLARE @results AS TABLE 
(
    [Month] datetime not null
 ,[Median] int not null
);

--This variable will determine the date
DECLARE @IntDate as int 
set @IntDate = -13


WHILE (@IntDate < 0) 
BEGIN

--Create Temp Table
DECLARE @table AS TABLE 
(
    [Rank] int not null
 ,[Days Open] int not null
);

--Insert records into Temp Table
insert into @table 

SELECT 
    rank() OVER (ORDER BY DATEADD(mm, DATEDIFF(mm, 0, DATEADD(ss, SVR.close_date, '1970')), 0), DATEDIFF(day,DATEADD(ss, SVR.open_date, '1970'),DATEADD(ss, SVR.close_date, '1970')),[SVR].[ref_num]) as [Rank]
 ,DATEDIFF(day,DATEADD(ss, SVR.open_date, '1970'),DATEADD(ss, SVR.close_date, '1970')) as [Days Open]
FROM
 mdbrpt.dbo.View_Request SVR
 LEFT OUTER JOIN dbo.dtv_apps_systems vapp 
 on SVR.category = vapp.persid
 LEFT OUTER JOIN dbo.prob_ctg pctg 
 on SVR.category = pctg.persid
 Left Outer Join [mdbrpt].[dbo].[rootcause] as [Root Cause] 
 on [SVR].[rootcause]=[Root Cause].[id]
 Left Outer Join [mdbrpt].[dbo].[cr_stat] as [Status]
 on [SVR].[status]=[Status].[code]
 LEFT OUTER JOIN [mdbrpt].[dbo].[net_res] as [net] 
 on [net].[id]=SVR.[affected_rc]
WHERE
 SVR.Type IN ('P') 
 AND
 SVR.close_date IS NOT NULL 
 AND
 [Status].[SYM] = 'Closed'
 AND
 SVR.parent is null
 AND
 [Root Cause].[sym] in ( 'RC - Application','RC - Hardware', 'RC - Operational', 'RC - Unknown')
 AND
 (
  [vapp].[appl_name] in ('3PI','Billing Rpts/Files','Collabrent','Reports','STMS','STMS 2','Telco','Comergent','OOM','C3-BAU','C3-DD','DIRECTV','DIRECTV Sales','DIRECTV Self Care','Dealer Website','EI Servlet','Enterprise Integration','ET','ICAN','ODS','SB-SCM','SeeBeyond','Digital Dashboard','IVR','OMS','Order Services','Retail Services','OSCAR','SAP','CTI','RIO','RIO Call Center','RIO Field Services','FSS-RIO3','TAOS','TCS')
 OR
  pctg.sym in ('Systems.Release Health Dashboard.Problem','DTV QA Test.Enterprise Release.Deferred Defect Log')
 AND  
  [Net].[nr_desc] in ('3PI','Billing Rpts/Files','Collabrent','Reports','STMS','STMS 2','Telco','Comergent','OOM','C3-BAU','C3-DD','DIRECTV','DIRECTV Sales','DIRECTV Self Care','Dealer Website','EI Servlet','Enterprise Integration','ET','ICAN','ODS','SB-SCM','SeeBeyond','Digital Dashboard','IVR','OMS','Order Services','Retail Services','OSCAR','SAP','CTI','RIO','RIO Call Center','RIO Field Services','FSS-RIO3','TAOS','TCS')
 )
 AND
 DATEADD(mm, DATEDIFF(mm, 0, DATEADD(ss, SVR.close_date, '1970')), 0) = DATEADD(mm, DATEDIFF(mm,0,DATEADD(mm,@IntDate,getdate())), 0)
ORDER BY [Days Open]



DECLARE @Count AS INT
SELECT @Count = COUNT(*) FROM @table;

WITH MyResults(RowNo, [Days Open]) AS
(
    SELECT RowNo, [Days Open] FROM
        (SELECT ROW_NUMBER() OVER (ORDER BY [Days Open]) AS RowNo, [Days Open] FROM @table) AS Foo
)


insert into @results
SELECT 
 DATEADD(mm, DATEDIFF(mm,0,DATEADD(mm,@IntDate,getdate())), 0) as [Month]
 ,AVG([Days Open])as [Median] FROM MyResults WHERE RowNo = (@Count+1)/2 OR RowNo = ((@Count+1)%2) * ((@Count+2)/2) 


set @IntDate = @IntDate+1
DELETE FROM @table
END

select *
from @results
order by [Month]

0

สิ่งนี้ใช้ได้กับ SQL 2000:

DECLARE @testTable TABLE 
( 
    VALUE   INT
)
--INSERT INTO @testTable -- Even Test
--SELECT 3 UNION ALL
--SELECT 5 UNION ALL
--SELECT 7 UNION ALL
--SELECT 12 UNION ALL
--SELECT 13 UNION ALL
--SELECT 14 UNION ALL
--SELECT 21 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 29 UNION ALL
--SELECT 40 UNION ALL
--SELECT 56

--
--INSERT INTO @testTable -- Odd Test
--SELECT 3 UNION ALL
--SELECT 5 UNION ALL
--SELECT 7 UNION ALL
--SELECT 12 UNION ALL
--SELECT 13 UNION ALL
--SELECT 14 UNION ALL
--SELECT 21 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 23 UNION ALL
--SELECT 29 UNION ALL
--SELECT 39 UNION ALL
--SELECT 40 UNION ALL
--SELECT 56


DECLARE @RowAsc TABLE
(
    ID      INT IDENTITY,
    Amount  INT
)

INSERT INTO @RowAsc
SELECT  VALUE 
FROM    @testTable 
ORDER BY VALUE ASC

SELECT  AVG(amount)
FROM @RowAsc ra
WHERE ra.id IN
(
    SELECT  ID 
    FROM    @RowAsc
    WHERE   ra.id -
    (
        SELECT  MAX(id) / 2.0 
        FROM    @RowAsc
    ) BETWEEN 0 AND 1

)

0

สำหรับมือใหม่เช่นฉันที่กำลังเรียนรู้พื้นฐานมากฉันพบตัวอย่างนี้ง่ายกว่าที่จะติดตามเพราะเป็นการง่ายกว่าที่จะเข้าใจว่าเกิดอะไรขึ้นและค่ามัธยฐานมาจากไหน ...

select
 ( max(a.[Value1]) + min(a.[Value1]) ) / 2 as [Median Value1]
,( max(a.[Value2]) + min(a.[Value2]) ) / 2 as [Median Value2]

from (select
    datediff(dd,startdate,enddate) as [Value1]
    ,xxxxxxxxxxxxxx as [Value2]
     from dbo.table1
     )a

ด้วยความน่าสะพรึงกลัวของรหัสข้างต้นบางอย่าง !!!


0

นี่เป็นคำตอบที่ง่ายอย่างที่ฉันคิดได้ ทำงานได้ดีกับข้อมูลของฉัน หากคุณต้องการยกเว้นค่าบางอย่างเพียงเพิ่มส่วนคำสั่งย่อยที่ส่วนภายในเลือก

SELECT TOP 1 
    ValueField AS MedianValue
FROM
    (SELECT TOP(SELECT COUNT(1)/2 FROM tTABLE)
        ValueField
    FROM 
        tTABLE
    ORDER BY 
        ValueField) A
ORDER BY
    ValueField DESC

0

โซลูชันต่อไปนี้ทำงานภายใต้สมมติฐานเหล่านี้:

  • ไม่มีค่าซ้ำกัน
  • ไม่มีค่า NULL

รหัส:

IF OBJECT_ID('dbo.R', 'U') IS NOT NULL
  DROP TABLE dbo.R

CREATE TABLE R (
    A FLOAT NOT NULL);

INSERT INTO R VALUES (1);
INSERT INTO R VALUES (2);
INSERT INTO R VALUES (3);
INSERT INTO R VALUES (4);
INSERT INTO R VALUES (5);
INSERT INTO R VALUES (6);

-- Returns Median(R)
select SUM(A) / CAST(COUNT(A) AS FLOAT)
from R R1 
where ((select count(A) from R R2 where R1.A > R2.A) = 
      (select count(A) from R R2 where R1.A < R2.A)) OR
      ((select count(A) from R R2 where R1.A > R2.A) + 1 = 
      (select count(A) from R R2 where R1.A < R2.A)) OR
      ((select count(A) from R R2 where R1.A > R2.A) = 
      (select count(A) from R R2 where R1.A < R2.A) + 1) ; 

0
DECLARE @Obs int
DECLARE @RowAsc table
(
ID      INT IDENTITY,
Observation  FLOAT
)
INSERT INTO @RowAsc
SELECT Observations FROM MyTable
ORDER BY 1 
SELECT @Obs=COUNT(*)/2 FROM @RowAsc
SELECT Observation AS Median FROM @RowAsc WHERE ID=@Obs

0

ฉันลองด้วยตัวเลือกหลายตัว แต่เนื่องจากบันทึกข้อมูลของฉันมีค่าซ้ำ ๆ กันดูเหมือนว่ารุ่น ROW_NUMBER จะไม่ใช่ตัวเลือกสำหรับฉัน ดังนั้นนี่แบบสอบถามฉันใช้ (รุ่นที่มี NTILE):

SELECT distinct
   CustomerId,
   (
       MAX(CASE WHEN Percent50_Asc=1 THEN TotalDue END) OVER (PARTITION BY CustomerId)  +
       MIN(CASE WHEN Percent50_desc=1 THEN TotalDue END) OVER (PARTITION BY CustomerId) 
   )/2 MEDIAN
FROM
(
   SELECT
      CustomerId,
      TotalDue,
     NTILE(2) OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue ASC) AS Percent50_Asc,
     NTILE(2) OVER (
         PARTITION BY CustomerId
         ORDER BY TotalDue DESC) AS Percent50_desc
   FROM Sales.SalesOrderHeader SOH
) x
ORDER BY CustomerId;

0

การสร้างคำตอบของ Jeff Atwood ด้านบนนี่คือกลุ่มตามและแบบสอบถามย่อยที่มีความสัมพันธ์กันเพื่อรับค่ามัธยฐานสำหรับแต่ละกลุ่ม

SELECT TestID, 
(
 (SELECT MAX(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts WHERE TestID = Posts_parent.TestID ORDER BY Score) AS BottomHalf)
 +
 (SELECT MIN(Score) FROM
   (SELECT TOP 50 PERCENT Score FROM Posts WHERE TestID = Posts_parent.TestID ORDER BY Score DESC) AS TopHalf)
) / 2 AS MedianScore,
AVG(Score) AS AvgScore, MIN(Score) AS MinScore, MAX(Score) AS MaxScore
FROM Posts_parent
GROUP BY Posts_parent.TestID

0

บ่อยครั้งเราอาจต้องคำนวณค่ามัธยฐานไม่เพียง แต่สำหรับทั้งตาราง แต่สำหรับการรวมที่เกี่ยวข้องกับ ID บางอย่าง ในคำอื่น ๆ คำนวณค่ามัธยฐานสำหรับแต่ละ ID ในตารางของเราที่แต่ละ ID มีหลายระเบียน (ขึ้นอยู่กับโซลูชันที่แก้ไขโดย @gdoron: ประสิทธิภาพที่ดีและทำงานใน SQL จำนวนมาก)

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 rnk
  FROM our_table
) AS x
WHERE rnk IN ((cnt + 1)/2, (cnt + 2)/2) GROUP BY our_id;

หวังว่ามันจะช่วย


0

สำหรับคำถามของคุณ Jeff Atwood ได้ให้วิธีการแก้ปัญหาที่ง่ายและมีประสิทธิภาพแล้ว แต่ถ้าคุณกำลังมองหาวิธีการอื่นในการคำนวณค่ามัธยฐานต่ำกว่าโค้ด SQL จะช่วยคุณได้

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;

declare @odd_even int; declare @cnt int; declare @middle_no int;


set @cnt=(select count(*) from employees); set @middle_no=(@cnt/2)+1; select @odd_even=case when (@cnt%2=0) THEN -1 ELse 0 END ;


 select AVG(tbl.salary) from  (select  salary,ROW_NUMBER() over (order by salary) as rno from employees group by salary) tbl  where tbl.rno=@middle_no or tbl.rno=@middle_no+@odd_even;

หากคุณต้องการคำนวณค่ามัธยฐานใน MySQL ลิงค์ GitHubนี้จะมีประโยชน์


0

นี่เป็นทางออกที่ดีที่สุดในการค้นหามีเดียที่ฉันนึกได้ ชื่อในตัวอย่างขึ้นอยู่กับตัวอย่างของจัสติน ตรวจสอบให้แน่ใจว่ามีดัชนีสำหรับตาราง Sales.SalesOrderHeader อยู่กับคอลัมน์ดัชนี CustomerId และ TotalDue ตามลำดับนั้น

SELECT
 sohCount.CustomerId,
 AVG(sohMid.TotalDue) as TotalDueMedian
FROM 
(SELECT 
  soh.CustomerId,
  COUNT(*) as NumberOfRows
FROM 
  Sales.SalesOrderHeader soh 
GROUP BY soh.CustomerId) As sohCount
CROSS APPLY 
    (Select 
       soh.TotalDue
    FROM 
    Sales.SalesOrderHeader soh 
    WHERE soh.CustomerId = sohCount.CustomerId 
    ORDER BY soh.TotalDue
    OFFSET sohCount.NumberOfRows / 2 - ((sohCount.NumberOfRows + 1) % 2) ROWS 
    FETCH NEXT 1 + ((sohCount.NumberOfRows + 1) % 2) ROWS ONLY
    ) As sohMid
GROUP BY sohCount.CustomerId

UPDATE

ฉันไม่แน่ใจเกี่ยวกับวิธีการที่มีประสิทธิภาพที่ดีที่สุดดังนั้นฉันจึงทำการเปรียบเทียบระหว่างวิธีของฉัน Justin Grants และ Jeff Atwoods โดยการเรียกใช้แบบสอบถามตามวิธีการทั้งสามในชุดเดียวและชุดค่าใช้จ่ายของแต่ละแบบสอบถามคือ:

ไม่มีดัชนี:

  • เหมือง 30%
  • Justin ให้เงิน 13%
  • Jeff Atwoods 58%

และมีดัชนี

  • เหมือง 3%
  • Justin ให้ 10%
  • Jeff Atwoods 87%

ฉันพยายามที่จะดูว่าแบบสอบถามมีขนาดเท่าไรถ้าคุณมีดัชนีโดยการสร้างข้อมูลเพิ่มเติมจากประมาณ 14,000 แถวโดยปัจจัย 2 ถึง 512 ซึ่งหมายความว่าในท้ายที่สุดประมาณ 7,2 ล้านแถว หมายเหตุฉันทำให้แน่ใจว่าฟิลด์ CustomeId ซึ่งไม่ซ้ำกันในแต่ละครั้งที่ฉันทำสำเนาเดียวดังนั้นสัดส่วนของแถวเทียบกับอินสแตนซ์เฉพาะของ CustomerId นั้นคงที่ ในขณะที่ฉันกำลังทำสิ่งนี้ฉันรันการประหารชีวิตที่ฉันสร้างดัชนีขึ้นใหม่หลังจากนั้นและฉันสังเกตเห็นว่าผลลัพธ์มีความเสถียรที่ประมาณ 128 ด้วยข้อมูลที่ฉันมีต่อค่าเหล่านี้:

  • เหมือง 3%
  • จัสตินมอบ 5%
  • Jeff Atwoods 92%

ฉันสงสัยว่าประสิทธิภาพการทำงานอาจได้รับผลกระทบจากการปรับจำนวนแถว แต่รักษาค่าคงที่ CustomerId ที่ไม่ซ้ำกันดังนั้นฉันจึงตั้งค่าการทดสอบใหม่ที่ฉันทำเช่นนี้ ตอนนี้แทนที่จะทำให้มีเสถียรภาพอัตราส่วนค่าใช้จ่ายชุดยังคงแตกต่างกันแทนประมาณ 20 แถวต่อ CustomerId ต่อค่าเฉลี่ยที่ฉันมีในตอนท้ายประมาณ 10,000 แถวต่อรหัสที่ไม่ซ้ำกันดังกล่าว ตัวเลขที่:

  • เหมือง 4%
  • Justins 60%
  • Jeffs 35%

ฉันทำให้แน่ใจว่าฉันใช้วิธีการแต่ละวิธีที่ถูกต้องโดยการเปรียบเทียบผลลัพธ์ ข้อสรุปของฉันคือวิธีที่ฉันใช้โดยทั่วไปจะเร็วกว่าตราบใดที่ดัชนียังมีอยู่ สังเกตว่าวิธีนี้เป็นวิธีที่แนะนำสำหรับปัญหานี้ในบทความนี้https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5

วิธีที่จะปรับปรุงประสิทธิภาพของการเรียกใช้คิวรีต่อไปนี้ให้ดียิ่งขึ้นคือการยืนยันข้อมูลการนับในตารางเสริม คุณสามารถรักษาได้ด้วยการทริกเกอร์ที่อัปเดตและเก็บข้อมูลเกี่ยวกับจำนวนแถว SalesOrderHeader ที่ขึ้นอยู่กับ CustomerId ซึ่งแน่นอนว่าคุณสามารถจัดเก็บค่ามัธยฐานได้โดยง่าย


0

สำหรับชุดข้อมูลขนาดใหญ่คุณสามารถลอง GIST นี้ได้:

https://gist.github.com/chrisknoll/1b38761ce8c5016ec5b2

มันทำงานโดยรวมค่าที่แตกต่างที่คุณจะพบในชุดของคุณ (เช่นอายุหรือปีเกิด ฯลฯ ) และใช้ฟังก์ชั่นหน้าต่าง SQL เพื่อค้นหาตำแหน่งเปอร์เซ็นต์ไทล์ที่คุณระบุในแบบสอบถาม

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