وظيفة لحساب الوسيط في SQL Server
-
20-09-2019 - |
سؤال
وفق MSDN, ، الوسيط غير متوفر كدالة مجمعة في Transact-SQL.ومع ذلك، أود معرفة ما إذا كان من الممكن إنشاء هذه الوظيفة (باستخدام ملف إنشاء التجميع وظيفة، وظيفة محددة من قبل المستخدم، أو طريقة أخرى).
ما هي أفضل طريقة (إن أمكن) للقيام بذلك - السماح بحساب القيمة المتوسطة (بافتراض وجود نوع بيانات رقمي) في استعلام مجمع؟
المحلول
هناك العديد من الطرق للقيام بذلك، مع أداء متفاوت بشكل كبير.إليك أحد الحلول المُحسّنة بشكل خاص، من الوسائط وROW_NUMBER والأداء.يعد هذا الحل الأمثل بشكل خاص عندما يتعلق الأمر بعمليات الإدخال/الإخراج الفعلية التي يتم إنشاؤها أثناء التنفيذ - فهو يبدو أكثر تكلفة من الحلول الأخرى، ولكنه في الواقع أسرع بكثير.
تحتوي هذه الصفحة أيضًا على مناقشة للحلول الأخرى وتفاصيل اختبار الأداء.لاحظ استخدام عمود فريد كأداة توضيح في حالة وجود صفوف متعددة بنفس قيمة العمود المتوسط.
كما هو الحال مع جميع سيناريوهات أداء قاعدة البيانات، حاول دائمًا اختبار الحل باستخدام بيانات حقيقية على أجهزة حقيقية - فأنت لا تعرف أبدًا متى يؤدي التغيير في مُحسِّن 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;
نصائح أخرى
إذا كنت تستخدم 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
في 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
إجابتي السريعة الأصلية كانت:
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
سيعطيك هذا النطاق المتوسط والربيعي بضربة واحدة.إذا كنت تريد حقًا صفًا واحدًا فقط يمثل الوسيط، فقم بإلغاء التعليق على جملة المكان.
عندما تلتزم بذلك في خطة شرح، فإن 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
يجب أن يقوم هذا بحساب القيم المتوسطة والنسب المئوية الصحيحة عندما يكون لديك عدد زوجي من عناصر البيانات.مرة أخرى، قم بإلغاء التعليق على جملة المكان النهائية إذا كنت تريد الوسيط فقط وليس التوزيع المئوي بأكمله.
حتى أفضل:
SELECT @Median = AVG(1.0 * val)
FROM
(
SELECT o.val, rn = ROW_NUMBER() OVER (ORDER BY o.val), c.c
FROM dbo.EvenRows AS o
CROSS JOIN (SELECT c = COUNT(*) FROM dbo.EvenRows) AS c
) AS x
WHERE rn IN ((c + 1)/2, (c + 2)/2);
من السيد نفسه إيتسيك بن غان!
يحتوي MS SQL Server 2012 (والإصدارات الأحدث) على وظيفة PERCENTILE_DISC التي تحسب نسبة مئوية محددة للقيم التي تم فرزها.PERCENTILE_DISC (0.5) سيحسب الوسيط - https://msdn.microsoft.com/en-us/library/hh231327.aspx
بسيطة وسريعة ودقيقة
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)
إذا كنت تريد استخدام وظيفة إنشاء التجميع في 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
يمكنك بعد ذلك كتابة استعلام لحساب الوسيط كما يلي:حدد dbo.Median(Field) من الجدول
لقد صادفت هذه الصفحة للتو أثناء البحث عن حل قائم على مجموعة للوسيط.بعد النظر في بعض الحلول هنا، توصلت إلى ما يلي.الأمل يساعد/يعمل.
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
يقوم الاستعلام التالي بإرجاع الوسيط من قائمة القيم في عمود واحد.لا يمكن استخدامه كدالة مجمعة أو معها، ولكن لا يزال بإمكانك استخدامها كاستعلام فرعي مع عبارة WHERE في التحديد الداخلي.
خادم SQL 2005+:
SELECT TOP 1 value from
(
SELECT TOP 50 PERCENT value
FROM table_name
ORDER BY value
)for_median
ORDER BY value DESC
على الرغم من أن حل Justin Grant يبدو قويًا، فقد وجدت أنه عندما يكون لديك عدد من القيم المكررة داخل مفتاح قسم معين، فإن أرقام الصفوف الخاصة بقيم 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
مثال جاستن أعلاه جيد جدًا.ولكن ينبغي ذكر هذه الحاجة الأساسية بشكل واضح للغاية.لقد رأيت هذا الرمز في البرية بدون المفتاح وكانت النتائج سيئة.
الشكوى التي أتلقاها بشأن 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
في UDF، اكتب:
Select Top 1 medianSortColumn from Table T
Where (Select Count(*) from Table
Where MedianSortColumn <
(Select Count(*) From Table) / 2)
Order By medianSortColumn
شاهد الحلول الأخرى لحساب المتوسط في SQL هنا:"طريقة بسيطة لحساب الوسيط باستخدام MySQL" (الحلول في الغالب مستقلة عن البائع).
بالنسبة للمتغير/القياس المستمر "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
أردت التوصل إلى حل بنفسي، لكن عقلي تعثر وسقط في الطريق.أنا يفكر إنه يعمل، لكن لا تطلب مني أن أشرحه في الصباح.:ص
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)
--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]
يعمل هذا مع 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
)
بالنسبة للمبتدئين مثلي الذين يتعلمون الأساسيات، فأنا شخصيًا أجد هذا المثال أسهل في المتابعة، حيث أنه من الأسهل فهم ما يحدث بالضبط ومن أين تأتي القيم المتوسطة...
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
في رهبة مطلقة من بعض الرموز المذكورة أعلاه !!!
هذه إجابة بسيطة بقدر ما أستطيع التوصل إليها.عملت بشكل جيد مع البيانات الخاصة بي.إذا كنت تريد استبعاد قيم معينة، فما عليك سوى إضافة عبارة "حيث" إلى التحديد الداخلي.
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
الحل التالي يعمل في ظل هذه الافتراضات:
- لا توجد قيم مكررة
- لا توجد قيم خالية
شفرة:
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) ;
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
أحاول باستخدام العديد من البدائل، ولكن نظرًا لأن سجلات البيانات الخاصة بي تحتوي على قيم متكررة، يبدو أن الإصدارات 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;
بناءً على إجابة Jeff Atwood أعلاه، يتم استخدام GROUP BY واستعلام فرعي مرتبط للحصول على المتوسط لكل مجموعة.
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
في كثير من الأحيان، قد نحتاج إلى حساب الوسيط ليس فقط للجدول بأكمله، ولكن للتجميعات فيما يتعلق ببعض المعرفات.بمعنى آخر، احسب الوسيط لكل معرف في جدولنا، حيث يحتوي كل معرف على العديد من السجلات.(استنادًا إلى الحل الذي تم تحريره بواسطة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;
نأمل أن يساعد.
بالنسبة لسؤالك، قدم جيف أتوود بالفعل الحل البسيط والفعال.ولكن، إذا كنت تبحث عن طريقة بديلة لحساب الوسيط، فإن كود 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، فهذا رابط جيثب سيكون مفيدا.
هذا هو الحل الأمثل للعثور على الوسيطات التي يمكنني التفكير فيها.الأسماء في المثال مبنية على مثال جاستن.تأكد من وجود فهرس لمبيعات الجدول.
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
تحديث
لم أكن متأكدًا بعض الشيء بشأن الطريقة التي تتمتع بأفضل أداء، لذلك أجريت مقارنة بين طريقتي Justin Grants وJeff Atwoods عن طريق تشغيل الاستعلام استنادًا إلى الطرق الثلاثة جميعها في دفعة واحدة وكانت تكلفة الدفعة لكل استعلام:
بدون فهرس:
- منجم 30%
- جاستن جرانتس 13%
- جيف أتوودز 58%
ومع الفهرس
- الألغام 3%.
- جاستن جرانتس 10%
- جيف أتوودز 87%
حاولت معرفة مدى جودة حجم الاستعلامات إذا كان لديك فهرس عن طريق إنشاء المزيد من البيانات من حوالي 14000 صف بعامل 2 حتى 512 وهو ما يعني في النهاية حوالي 7.2 مليون صف.لاحظ أنني تأكدت من أن حقل CustomeId فريد في كل مرة أقوم فيها بعمل نسخة واحدة، لذلك ظلت نسبة الصفوف مقارنة بالمثيل الفريد لـ CustomerId ثابتة.أثناء قيامي بذلك، قمت بتنفيذ عمليات تنفيذ حيث قمت بإعادة بناء الفهرس بعد ذلك، ولاحظت استقرار النتائج عند عامل 128 تقريبًا مع البيانات التي كانت لدي لهذه القيم:
- الألغام 3%.
- جاستن جرانتس 5%
- جيف أتوودز 92%
لقد تساءلت كيف يمكن أن يتأثر الأداء بزيادة عدد الصفوف مع الحفاظ على ثبات معرف العميل الفريد، لذلك قمت بإعداد اختبار جديد حيث قمت بذلك بالضبط.الآن بدلاً من الاستقرار، ظلت نسبة تكلفة الدُفعة متباينة، أيضًا بدلاً من حوالي 20 صفًا لكل معرف عميل لكل متوسط، كان لدي في النهاية حوالي 10000 صف لكل معرف فريد من نوعه.الأرقام حيث:
- منجم 4%
- جاستن 60%
- جيفز 35%
لقد تأكدت من أنني قمت بتنفيذ كل طريقة بشكل صحيح من خلال مقارنة النتائج.استنتاجي هو أن الطريقة التي استخدمتها تكون أسرع بشكل عام طالما أن الفهرس موجود.لاحظت أيضًا أن هذه الطريقة هي ما يوصى به لهذه المشكلة تحديدًا في هذه المقالة https://www.microsoftpressstore.com/articles/article.aspx?p=2314819&seqNum=5
إحدى الطرق لتحسين أداء الاستدعاءات اللاحقة لهذا الاستعلام بشكل أكبر هي الاستمرار في معلومات العد في جدول مساعد.يمكنك أيضًا الحفاظ عليه من خلال وجود مشغل يقوم بالتحديث ويحتفظ بالمعلومات المتعلقة بعدد صفوف SalesOrderHeader التي تعتمد على CustomerId، وبالطبع يمكنك بعد ذلك تخزين الوسيط أيضًا.
بالنسبة لمجموعات البيانات واسعة النطاق، يمكنك تجربة GIST هذا:
https://Gist.github.com/chrisknoll/1b38761ce8c5016ec5b2
وهو يعمل عن طريق تجميع القيم المميزة التي قد تجدها في مجموعتك (مثل الأعمار أو سنة الميلاد، وما إلى ذلك)، ويستخدم وظائف نافذة SQL لتحديد أي موضع مئوي تحدده في الاستعلام.
العثور على الوسيط
هذه هي أبسط طريقة للعثور على الوسيط للسمة.
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)