web-dev-qa-db-ja.com

ピボットおよびアンピボットテーブルを使用せずにSQL Server 2008 R2の行から中央値を取得する方法

テーブルスクリプトの作成

create table temp 
(
    id int identity(1,1),
    a decimal(6,2),
    b decimal(6,2),
    c decimal(6,2),
    d decimal(6,2),
    e decimal(6,2),
    f decimal(6,2),
    g decimal(6,2),
    h decimal(6,2),
    i decimal(6,2),
    j decimal(6,2),
    k decimal(6,2),
    l decimal(6,2),
    m decimal(6,2),
    n decimal(6,2),
    o decimal(6,2),
    p decimal(6,2),
    q decimal(6,2),
    r decimal(6,2),
    s decimal(6,2),
    t decimal(6,2),
    u decimal(6,2)
)

スクリプトを挿入

insert into temp
    (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u)
values
    (1,5,6,7,8,2,6,3,4,5,2,1,6,5,7,8,2,7,6,2,8)

insert into temp
    (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u)
values
    (1,5,6,7,8,2,2,3,2,4,2,1,4,5,9,8,2,7,6,2,8)

期待される結果

Median
======
first row  - 5.00
second row - 4.00 

機能しないソリューション

SQL Server 2014で正常に機能しているが、SQL Server 2008 R2で問題がある以下のクエリを試してみました。

select id, avg(val)
from ( 
    select id, val
         , count(*) over (partition by id) as c
         , row_number() over (partition by id order by val) as rn
    from temp unpivot (
             val for col in (a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u)
         ) as x 
) as y
where rn IN ((c + 1)/2, (c + 2)/2) 
group by id;

上記のクエリを2014バージョンで実行したところ、正しく機能していますが、2008 R2では機能しません。 SQL Server 2008 R2でこのエラーが発生します。

キーワード「for」付近の構文が正しくありません

データベースの互換性レベルが80であるためです。ただし、互換性レベルを変更すると、アプリケーションに影響するため、変更できません。

私もこのクエリを試しました:

select id,        
(select cast(Avg(TotAvg)as decimal(6,2)) as Median from (values (convert(decimal(6,2), a)),(convert(decimal(6,2), b)),        
(convert(decimal(6,2), c)),        
(convert(decimal(6,2), d)),(convert(decimal(6,2), e)),        
(convert(decimal(6,2), f)),(convert(decimal(6,2), g)),(convert(decimal(6,2), h)),(convert(decimal(6,2), i)),        
(convert(decimal(6,2), j)),(convert(decimal(6,2), k)),(convert(decimal(6,2), l)),(convert(decimal(6,2), m)),        
(convert(decimal(6,2), n)),(convert(decimal(6,2), o)),(convert(decimal(6,2), p)),(convert(decimal(6,2), q)),        
(convert(decimal(6,2), r)),(convert(decimal(6,2), s)),(convert(decimal(6,2), t)),(convert(decimal(6,2), u))) as Totalavg(TotAvg))         
Median
from tempone

明らかに平均を計算しますが、中央値が必要です。

6
Isai Selvan

PIVOTとUNPIVOTは、互換性レベル80では実際にはサポートされていません。

ただし、ネストされたVALUESコンストラクターを使用して行をアンピボットできます。私の場合、結果のクエリは、二重の入れ子のために少し扱いに​​くいように見えますが、SQL Server 2008ではサポートされている互換性レベルがあれば機能します。

_SELECT
  id,
  median =
  (
    SELECT
      AVG(val)
    FROM
      (
        SELECT
          c   = COUNT(*) OVER (),
          rn  = ROW_NUMBER() OVER (ORDER BY v.val ASC),
          val = v.val
        FROM
          (
            VALUES
            (t.a), (t.b), (t.c), (t.d), (t.e), (t.f), (t.g),
            (t.h), (t.i), (t.j), (t.k), (t.l), (t.m), (t.n),
            (t.o), (t.p), (t.q), (t.r), (t.s), (t.t), (t.u)
          ) AS v (val)
        WHERE
          v.val IS NOT NULL
      ) AS derived
    WHERE
      rn IN ((c + 1) / 2, (c + 2) / 2)
  )
FROM
  temp AS t
;
_

UNPIVOTは自動的にNULL値を除外するため、_v.val IS NOT NULL_フィルタリングはUNPIVOTの動作をより厳密に模倣するためにあります。

カウントと行番号を生成して同じネストレベルで使用する方法が他になかったため、追加のネストが必要でした。

したがって、最も内側のSELECT(SELECT ... FROM (VALUES ...))は行をアン​​ピボットし、行数と行番号を提供しますが、中間層レベルは中央値を計算します。

次のように、CROSS APPLYを使用してネストを減らし、メインクエリでグループ化することができます。

_SELECT
  id,
  median = AVG(x.val)
FROM
  temp AS t
  CROSS APPLY
  (
    SELECT
      c   = COUNT(*) OVER (),
      rn  = ROW_NUMBER() OVER (ORDER BY v.val ASC),
      val = v.val
    FROM
      (
        VALUES
        (t.a), (t.b), (t.c), (t.d), (t.e), (t.f), (t.g),
        (t.h), (t.i), (t.j), (t.k), (t.l), (t.m), (t.n),
        (t.o), (t.p), (t.q), (t.r), (t.s), (t.t), (t.u)
      ) AS v (val)
    WHERE
      v.val IS NOT NULL
  ) AS x
WHERE
  x.rn IN ((x.c + 1) / 2, (x.c + 2) / 2)
GROUP BY
  t.id
;
_

両方の方法のライブデモについては、 このdbfiddle.ukリンク に従ってください。

7
Andriy M

CROSS APPLYを使用するメソッド(互換性レベル80のデータベースで機能すると思います)。これは、これらの列にnullがない場合にのみ正しく機能します。

select t.id, z.val
from temp as t
  cross apply
    ( select top (1) y.val
      from 
        ( select top (11) x.val
          from
            ( values (a),(b),(c),(d),(e),(f),(g),(h),(i),(j),
                     (k),(l),(m),(n),(o),(p),(q),(r),(s),(t),(u)
            ) as x (val)
          order by x.val
        ) as y
      order by y.val desc
    ) as z ;

テスト済み:dbfiddle.uk.

最も「内側の」サブクエリ(x)は、(21)列から(21)行へのアンピボットを実行します。

            ( values (a),(b),(c),(d),(e),(f),(g),(h),(i),(j),
                     (k),(l),(m),(n),(o),(p),(q),(r),(s),(t),(u)
            ) as x (val)

次に、2番目のもの(y)は値でそれらを並べ替え、それらの半分のみを選択します。

        ( select top (11) x.val
          from
            --
            --
               x (val)
          order by x.val
        ) as y

そして、3番目(z)は中央(11行の)の最後を選択して中央値を見つけます。


Nullを処理する別のメソッドは次のとおりです。自己結合とGROUP BYは、小さい値と大きい値の「ローリング」カウントを実行するために使用されます。

select t.id, w.val
from temp as t
  cross apply
    ( select val = avg(val)
      from 
        ( select d.val, 
                 cnt_less = count(case when y.val < d.val then 1 end),
                 cnt_more = count(case when y.val > d.val then 1 end),
                 cnt = count(y.val)
          from
              ( select distinct val
                from 
                  ( values (a),(b),(c),(d),(e),(f),(g),(h),(i),(j),
                           (k),(l),(m),(n),(o),(p),(q),(r),(s),(t),(u)
                  ) as x (val)
                where x.val is not null
              ) as d
            cross join
              ( values (a),(b),(c),(d),(e),(f),(g),(h),(i),(j),
                       (k),(l),(m),(n),(o),(p),(q),(r),(s),(t),(u)
              ) as y (val)
          group by d.val
        ) as z
      where 2 * cnt_less <= cnt
        and 2 * cnt_more <= cnt
    ) as w ;
7
ypercubeᵀᴹ

興味深い質問なので、これが私の解決策です

; with c (id, val) as
(
select id
, val = case columnname 
            when 'a' then a
            when 'b' then b
            when 'c' then c
            when 'd' then d
            when 'e' then e
            when 'f' then f
            when 'g' then g
            when 'h' then h
            when 'i' then i
            when 'j' then j
            when 'k' then k
            when 'l' then l
            when 'm' then m
            when 'n' then n
            when 'o' then o
            when 'p' then p
            when 'q' then q
            when 'r' then r
            when 's' then s
            when 't' then t
            when 'u' then u
        end
from  temp
cross join ( 
select columnname from (values ('a'), ('b'), ('c'), ('d'),('e')
, ('f'), ('g'), ('h'), ('i'),('j')
, ('k'), ('l'), ('m'), ('n'),('o')
, ('p'), ('q'), ('r'), ('s'),('t'), ('u')

) t(columnname)
) Col   
)
, c2 as (
  select id, rn = row_number() over (partition by id order by val), val, cnt=count(*) over (partition by id)
from c)
select id, median=avg(val) from c2
where rn in ((cnt+1)/2, (cnt+2)/2)
group by id

最初のCTE cは行を列にアンピボットし、2番目のCTE c2はIDグループ間のランキングを生成して、クエリの外側が中央値を見つけられるようにします

2
jyao