Ayrık bölümler oluşturmak için bir sütunu toplayan pencereleme sorgusunu nasıl yazabilirim?


11

Bu gibi ondalık değerler sütunu içeren bir tablo var:

id value size
-- ----- ----
 1   100  .02
 2    99  .38
 3    98  .13
 4    97  .35
 5    96  .15
 6    95  .57
 7    94  .25
 8    93  .15

Yapmam gereken şeyi anlatmak biraz zor, bu yüzden lütfen bana katlan. Ne yapmaya çalışıyorum sizeönceki satır 1'e göre her seferinde 1, her biri göre azalan sırada zaman 1 artar sütun toplam değeri oluşturmaktır value. Sonuç şöyle görünecektir:

id value size bucket
-- ----- ---- ------
 1   100  .02      1
 2    99  .38      1
 3    98  .13      1
 4    97  .35      1
 5    96  .15      2
 6    95  .57      2
 7    94  .25      2
 8    93  .15      3

Naif ilk denemem bir koşuyu SUMve ardından CEILINGbu değeri korumaktı , ancak bazı kayıtların sizetoplamda iki ayrı kovaya katkıda bulunduğu durumla ilgilenmiyor . Aşağıdaki örnek bunu açıklığa kavuşturabilir:

id value size crude_sum crude_bucket distinct_sum bucket
-- ----- ---- --------- ------------ ------------ ------
 1   100  .02       .02            1          .02      1
 2    99  .38       .40            1          .40      1
 3    98  .13       .53            1          .53      1
 4    97  .35       .88            1          .88      1
 5    96  .15      1.03            2          .15      2
 6    95  .57      1.60            2          .72      2
 7    94  .25      1.85            2          .97      2
 8    93  .15      2.00            2          .15      3

Gördüğünüz gibi ben sadece kullanmak olsaydı, CEILINGüzerinde crude_sumkayda 8. Bu kaynaklanır kovaya 2. tayin edileceğini sizeiki kova genelinde kayıtların 5. ve 8. olmanın bölünmüş. Bunun yerine, ideal çözüm, 1'e her ulaştığında toplamı sıfırlamaktır, bu daha sonra bucketsütunu arttırır ve geçerli kaydın değerinden SUMbaşlayarak yeni bir işlem başlatır size. Kayıtların sırası bu işlem için önemli olduğundan value, azalan sırada sıralanması amaçlanan sütunu ekledim.

İlk denemelerim, bir kez SUMişlemi gerçekleştirmek için bir kez daha veri vb. Üzerinde birden fazla geçiş yapmayı içeriyordu CEILING. İşte crude_sumsütun oluşturmak için yaptığımın bir örneği :

SELECT
  id,
  value,
  size,
  (SELECT TOP 1 SUM(size) FROM table t2 WHERE t2.value<=t1.value) as crude_sum
FROM
  table t1

Hangi bir UPDATEişlemde daha sonra çalışmak için bir tabloya değer eklemek için kullanıldı .

Düzenleme: Bunu açıklamak için başka bir bıçak almak istiyorum, işte gidiyor. Her kaydın fiziksel bir öğe olduğunu düşünün. Bu öğenin kendisiyle ilişkilendirilmiş bir değeri ve birden fazla fiziksel boyutu var. Hacim kapasitesi tam olarak 1 olan bir dizi kova var ve bu kovalardan kaçına ihtiyacım olacağını ve her bir öğenin en yüksekten en alta doğru sıralanan öğenin değerine göre gittiğini belirlemem gerekiyor.

Fiziksel bir öğe aynı anda iki yerde bulunamaz, bu nedenle bir kovada veya diğerinde olmalıdır. Bu yüzden çalışan bir toplam + CEILINGçözüm yapamıyorum , çünkü bu kayıtların boyutlarını iki kepçeye katkıda bulunmasına izin verecek.


İlk denemenizin neleri içerdiğini netleştirmek için SQL'inizi eklemelisiniz.
mdahlman

Hesapladığınız gruba göre veri toplayacak mısınız yoksa kova numarası aradığınız son cevap mı?
Jon Seigel

2
Ack. Her seferinde bir satır getiren imleç döngüsünün aksine, kayıtların daha iyi akışını destekleyeceğinden, muhtemelen bir istemci tarafı uygulamasıyla giderdim. Tüm güncellemeler toplu olarak yapıldığı sürece, makul bir şekilde iyi performans göstermesi gerektiğini düşünüyorum.
Jon Seigel

1
Diğerlerinin daha önce de belirttiği gibi, kovalama gereksinimi distinct_countişleri karmaşıklaştırır. Aaron Bertrand, bu tür pencereleme çalışmaları için SQL Server'daki seçeneklerinizin harika bir özetine sahiptir . SQL Fiddle'dadistinct_sum görebileceğiniz hesaplamak için "ilginç güncelleme" yöntemini kullandım , ancak bu güvenilir değil.
Nick Chammas

1
@JonSeigel X öğeyi en az sayıda kovaya yerleştirme sorununun, SQL dilinin satır satır algoritması kullanılarak verimli bir şekilde çözülemeyeceğini unutmamalıyız. Örneğin; 0,7; 0,8; 0,3 boyutundaki ürünler için 2 kova gerekir, ancak kimliğe göre sıralanırsa 3 kova gerekir.
Haziran'da Stoleg

Yanıtlar:


9

Ne tür bir performans aradığınızdan emin değilim, ancak CLR veya harici uygulama bir seçenek değilse, kalan tek imleç kalır. Eski dizüstü bilgisayarımda, aşağıdaki çözümü kullanarak yaklaşık 100 saniyede 1.000.000 satır alıyorum. Bununla ilgili güzel olan şey, doğrusal olarak ölçeklendirilmesidir, bu yüzden tüm şeyleri çalıştırmak için yaklaşık 20 dakikaya bakıyordum. İyi bir sunucu ile daha hızlı olacaksınız, ancak bir büyüklük sırası olmayacaksınız, bu yüzden bunu tamamlamak birkaç dakika sürecektir. Bu bir defalık bir işlemse, muhtemelen yavaşlığı karşılayabilirsiniz. Bunu bir rapor veya benzeri bir şekilde düzenli olarak çalıştırmanız gerekirse, yeni satırlar eklendikçe, örneğin bir tetikleyicide, değerleri güncellemeden aynı tabloda saklamak isteyebilirsiniz.

Her neyse, işte kod:

IF OBJECT_ID('dbo.MyTable') IS NOT NULL DROP TABLE dbo.MyTable;

CREATE TABLE dbo.MyTable(
 Id INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
 v NUMERIC(5,3) DEFAULT ABS(CHECKSUM(NEWID())%100)/100.0
);


MERGE dbo.MyTable T
USING (SELECT TOP(1000000) 1 X FROM sys.system_internals_partition_columns A,sys.system_internals_partition_columns B,sys.system_internals_partition_columns C,sys.system_internals_partition_columns D)X
ON(1=0)
WHEN NOT MATCHED THEN
INSERT DEFAULT VALUES;

--SELECT * FROM dbo.MyTable

DECLARE @st DATETIME2 = SYSUTCDATETIME();
DECLARE cur CURSOR FAST_FORWARD FOR
  SELECT Id,v FROM dbo.MyTable
  ORDER BY Id;

DECLARE @id INT;
DECLARE @v NUMERIC(5,3);
DECLARE @running_total NUMERIC(6,3) = 0;
DECLARE @bucket INT = 1;

CREATE TABLE #t(
 id INT PRIMARY KEY CLUSTERED,
 v NUMERIC(5,3),
 bucket INT,
 running_total NUMERIC(6,3)
);

OPEN cur;
WHILE(1=1)
BEGIN
  FETCH NEXT FROM cur INTO @id,@v;
  IF(@@FETCH_STATUS <> 0) BREAK;
  IF(@running_total + @v > 1)
  BEGIN
    SET @running_total = 0;
    SET @bucket += 1;
  END;
  SET @running_total += @v;
  INSERT INTO #t(id,v,bucket,running_total)
  VALUES(@id,@v,@bucket, @running_total);
END;
CLOSE cur;
DEALLOCATE cur;
SELECT DATEDIFF(SECOND,@st,SYSUTCDATETIME());
SELECT * FROM #t;

GO 
DROP TABLE #t;

MyTable tablosunu düşürür ve yeniden oluşturur, 1000000 satırla doldurur ve sonra işe gider.

İmleç, hesaplamaları çalıştırırken her satırı geçici bir tabloya kopyalar. Sonunda seçim hesaplanan sonuçları döndürür. Verileri kopyalamazsanız, bunun yerine yerinde güncelleme yaparsanız biraz daha hızlı olabilirsiniz.

SQL 2012'ye yükseltme seçeneğiniz varsa, size daha iyi performans vermesi gereken yeni pencere biriktirme destekli hareketli pencere kümelerine bakabilirsiniz.

Yan notta, allow_set = safe ile kurulu bir montajınız varsa, standart T-SQL ile bir sunucuya montajdan daha kötü şeyler yapabilirsiniz, bu yüzden bu engeli kaldırmak için çalışmaya devam edersiniz - İyi bir kullanımınız var Burada CLR'nin size gerçekten yardımcı olacağı bir durum.


Bunu uygulamanın ne kadar kolay olduğu ve ihtiyaç duyuldukça daha sonra değiştirip hata ayıklayabildiğim için bunu kabul ettim. @ NickChammas'ın yanıtı da doğru ve muhtemelen daha verimli çalışıyor, bu yüzden benzer bir konuyla karşılaşan herkes için bir tercih meselesi sanırım.
Zikes

9

SQL Server 2012'de yeni pencereleme işlevleri olmadığında, karmaşık pencereleme, özyinelemeli CTE'lerin kullanımı ile gerçekleştirilebilir. Bunun milyonlarca satıra karşı ne kadar iyi performans göstereceğini merak ediyorum.

Aşağıdaki çözüm, açıkladığınız tüm durumları kapsar. Sen eylem görebilirsiniz burada, SQL Fiddle üzerinde .

-- schema setup
CREATE TABLE raw_data (
    id    INT PRIMARY KEY
  , value INT NOT NULL
  , size  DECIMAL(8,2) NOT NULL
);

INSERT INTO raw_data 
    (id, value, size)
VALUES 
   ( 1,   100,  .02) -- new bucket here
 , ( 2,    99,  .99) -- and here
 , ( 3,    98,  .99) -- and here
 , ( 4,    97,  .03)
 , ( 5,    97,  .04)
 , ( 6,    97,  .05)
 , ( 7,    97,  .40)
 , ( 8,    96,  .70) -- and here
;

Şimdi derin bir nefes alın. Burada, her biri kısa bir yorumdan önce gelen iki kilit CTE vardır. Geri kalanı sadece "temizleme" CTE'lerdir, örneğin onları sıraladıktan sonra doğru satırları çekmek için.

-- calculate the distinct sizes recursively
WITH distinct_size AS (
  SELECT
      id
    , size
    , 0 as level
  FROM raw_data

  UNION ALL

  SELECT 
      base.id
    , CAST(base.size + tower.size AS DECIMAL(8,2)) AS distinct_size
    , tower.level + 1 as level
  FROM 
                raw_data AS base
    INNER JOIN  distinct_size AS tower
      ON base.id = tower.id + 1
  WHERE base.size + tower.size <= 1
)
, ranked_sum AS (
  SELECT 
      id
    , size AS distinct_size
    , level
    , RANK() OVER (PARTITION BY id ORDER BY level DESC) as rank
  FROM distinct_size  
)
, top_level_sum AS (
  SELECT
      id
    , distinct_size
    , level
    , rank
  FROM ranked_sum
  WHERE rank = 1
)
-- every level reset to 0 means we started a new bucket
, bucket AS (
  SELECT
      base.id
    , COUNT(base.id) AS bucket
  FROM 
               top_level_sum base
    INNER JOIN top_level_sum tower
      ON base.id >= tower.id
  WHERE tower.level = 0
  GROUP BY base.id
)
-- join the bucket info back to the original data set
SELECT
    rd.id
  , rd.value
  , rd.size
  , tls.distinct_size
  , b.bucket
FROM 
             raw_data rd
  INNER JOIN top_level_sum tls
    ON rd.id = tls.id
  INNER JOIN bucket   b
    ON rd.id = b.id
ORDER BY
  rd.id
;

Bu çözüm id, boşluksuz bir dizi olduğunu varsayar . Değilse, başlangıçta satırları ROW_NUMBER()istenen sıraya göre numaralandıran ek bir CTE ekleyerek kendi boşluksuz dizinizi oluşturmanız gerekir (örn. ROW_NUMBER() OVER (ORDER BY value DESC)).

Fankly, bu oldukça ayrıntılı.


1
Bu çözüm, bir satırın boyutunun birden çok kovaya katkıda bulunabileceği durumu ele almıyor gibi görünüyor. Yuvarlanan toplamı kolay yeterlidir, ama benim soru bu 1. See son örneği masa ulaştığı her seferinde sıfırlamak için bu toplamı ihtiyaç ve karşılaştırmak crude_sumile distinct_sumve bunlarla ilişkili bucketdemek istediğimi görmek için sütunlar.
Haziran'da Zikes

2
@Zikes - Güncellenmiş çözümümle bu durumu ele aldım.
Nick Chammas

Şimdi çalışmalı gibi görünüyor. Test etmek için veritabanımla entegre etmeye çalışacağım.
Zikes

@Zikes - Merak ediyorum, burada yayınlanan çeşitli çözümler büyük veri kümenize karşı nasıl bir performans sergiliyor? Sanırım Andriy en hızlısı.
Nick Chammas

5

Bu aptalca bir çözüm gibi geliyor ve muhtemelen iyi ölçeklenmeyecek, bu yüzden kullanıyorsanız dikkatli bir şekilde test edin. Asıl sorun, kovada kalan "boşluk" dan geldiğinden, ilk önce verileri birleştirmek için bir dolgu kaydı oluşturmak zorunda kaldım.

with bar as (
select
  id
  ,value
  ,size
  from foo
union all
select
  f.id
  ,value = null
  ,size = 1 - sum(f2.size) % 1
  from foo f
  inner join foo f2
    on f2.id < f.id
  group by f.id
    ,f.value
    ,f.size
  having cast(sum(f2.size) as int) <> cast(sum(f2.size) + f.size as int)
)
select
  f.id
  ,f.value
  ,f.size
  ,bucket = cast(sum(b.size) as int) + 1
  from foo f
  inner join bar b
    on b.id <= f.id
  group by f.id
    ,f.value
    ,f.size

http://sqlfiddle.com/#!3/72ad4/14/0


1
+1 Bence uygun indeksler varsa bunun potansiyeli var.
Jon Seigel

3

Aşağıdaki, başka bir özyinelemeli CTE çözümüdür, ancak @ Nick'in önerisinden daha basit olduğunu söyleyebilirim . Aslında @ Sebastian'ın imlecine daha yakın , toplamları çalıştırmak yerine sadece farklılıkları kullandım. (İlk başta @ Nick'in cevabının burada önerdiğim şeylerin çizgisi boyunca olacağını düşündüm ve öğrendikten sonra benimkini sunmaya karar verdiğim çok farklı bir sorgu olduğunu öğrendim.)

WITH rec AS (
  SELECT TOP 1
    id,
    value,
    size,
    bucket        = 1,
    room_left     = CAST(1.0 - size AS decimal(5,2))
  FROM atable
  ORDER BY value DESC
  UNION ALL
  SELECT
    t.id,
    t.value,
    t.size,
    bucket        = r.bucket + x.is_new_bucket,
    room_left     = CAST(CASE x.is_new_bucket WHEN 1 THEN 1.0 ELSE r.room_left END - t.size AS decimal(5,2))
  FROM atable t
  INNER JOIN rec r ON r.value = t.value + 1
  CROSS APPLY (
    SELECT CAST(CASE WHEN t.size > r.room_left THEN 1 ELSE 0 END AS bit)
  ) x (is_new_bucket)
)
SELECT
  id,
  value,
  size,
  bucket
FROM rec
ORDER BY value DESC
;

Not: Bu sorgu, valuesütunun boşluk içermeyen benzersiz değerlerden oluştuğunu varsayar . Aksi takdirde, azalan sıraya göre hesaplanmış bir sıralama sütunu girmeniz ve özyinelemeli özyinelemeli parçaya katılmak valueyerine özyinelemeli CTE'de kullanmanız gerekir value.

Bu sorgu için bir SQL Fiddle demosu burada bulunabilir .


Bu yazdığımdan çok daha kısa. İyi iş. Yukarı saymak yerine kovada kalan odayı geri saymanın bir nedeni var mı?
Nick Chammas

Evet, burada yayınladığım sürüm için çok anlamlı olup olmadığından emin değilim. Neyse, sebebi (tek bir değere sahip tek bir değeri karşılaştırmak için daha doğal / kolay görünüyordu olmasıydı sizeile room_left(bir ifadeyle tek bir değeri karşılaştırarak aksine) 1ile running_size+ size). is_new_bucketİlk başta bir bayrak kullanmadım ama CASE WHEN t.size > r.room_left ...bunun yerine birkaç tane ("birkaç" çünkü toplam boyutu hesapladım (ve geri döndüm), ancak basitlik uğruna buna karşı düşündüm), bu yüzden daha zarif olacağını düşündüm bu şekilde.
Andriy M
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.