ฉันจะเขียนคิวรีหน้าต่างที่รวมคอลัมน์เพื่อสร้างที่เก็บข้อมูลแยกได้อย่างไร?


11

ฉันมีตารางที่มีคอลัมน์ของค่าทศนิยมเช่นนี้

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

สิ่งที่ฉันต้องทำให้สำเร็จเป็นเรื่องยากที่จะอธิบายดังนั้นโปรดอดทนด้วย สิ่งที่ผมพยายามทำคือการสร้างมูลค่ารวมของsizeคอลัมน์ที่เพิ่มขึ้นโดยที่ 1 ในแต่ละครั้งแถวก่อนหน้านี้รวมถึง 1 valueเมื่อเรียงลำดับตาม ผลลัพธ์จะเป็นดังนี้:

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

ความพยายามครั้งแรกของฉันที่ไร้เดียงสาคือการวิ่งต่อไปSUMและจากนั้นก็มีCEILINGค่านั้น แต่มันก็ไม่ได้จัดการกับกรณีที่บางระเบียนsizeจบลงด้วยการมีส่วนร่วมทั้งหมดสองถังแยกกัน ตัวอย่างด้านล่างอาจอธิบายสิ่งนี้:

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

อย่างที่คุณเห็นถ้าฉันจะใช้เพียงแค่CEILINGในcrude_sumบันทึก # 8 จะได้รับมอบหมายให้ฝากข้อมูล 2 นี้เกิดจากการsizeบันทึก # 5 และ # 8 ถูกแบ่งออกเป็นสองถัง ทางออกที่ดีที่สุดคือการรีเซ็ตผลรวมทุกครั้งที่มาถึง 1 ซึ่งจะเพิ่มbucketคอลัมน์และเริ่มการSUMดำเนินการใหม่โดยเริ่มจากsizeมูลค่าของระเบียนปัจจุบัน เนื่องจากลำดับของเร็กคอร์ดมีความสำคัญต่อการดำเนินการนี้ฉันจึงรวมvalueคอลัมน์ซึ่งมีวัตถุประสงค์เพื่อเรียงลำดับจากมากไปน้อย

ความพยายามเริ่มต้นของฉันเกี่ยวข้องกับการส่งผ่านข้อมูลหลายครั้งเพื่อทำการดำเนินSUMการอีกครั้งไปอีกครั้งCEILINGและนี่คือตัวอย่างของสิ่งที่ฉันทำเพื่อสร้างcrude_sumคอลัมน์:

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

ซึ่งใช้ในการUPDATEดำเนินการเพื่อแทรกค่าลงในตารางเพื่อใช้งานในภายหลัง

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

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


คุณควรเพิ่ม SQL ของคุณเพื่อให้ชัดเจนว่าความพยายามครั้งแรกของคุณรวมอยู่ด้วย
mdahlman

คุณกำลังจะรวบรวมข้อมูลตามที่ฝากข้อมูลที่คุณกำลังคำนวณหรือหมายเลขถังเก็บเป็นคำตอบสุดท้ายที่คุณต้องการหรือไม่
Jon Seigel

2
Ack ฉันอาจไปกับแอปฝั่งไคลเอ็นต์เนื่องจากจะสนับสนุนการสตรีมในเรกคอร์ดที่ดีขึ้นเมื่อเทียบกับเคอร์เซอร์ลูปที่ดึงข้อมูลทีละหนึ่งแถว ฉันคิดว่าตราบใดที่การอัปเดตทั้งหมดเสร็จในแบตช์ก็ควรทำงานได้ดีพอสมควร
Jon Seigel

1
ตามที่คนอื่น ๆ ได้กล่าวไปแล้วความต้องการของการจัดเก็บในdistinct_countสิ่งที่ซับซ้อน Aaron Bertrand มีข้อมูลสรุปที่ยอดเยี่ยมเกี่ยวกับตัวเลือกของคุณบน SQL Serverสำหรับงานประเภทหน้าต่างนี้ ฉันใช้วิธี "quirky update" เพื่อคำนวณdistinct_sumซึ่งคุณสามารถดูได้ที่นี่บน SQL Fiddleแต่นี่ไม่น่าเชื่อถือ
Nick Chammas

1
@JonSeigel เราควรทราบว่าปัญหาของการวางรายการ X ในจำนวนที่น้อยที่สุดของถังไม่สามารถแก้ไขได้อย่างมีประสิทธิภาพโดยใช้อัลกอริทึมแถวโดยแถวของภาษา SQL เช่นรายการขนาด 0.7; 0.8; 0.3 จะต้องมี 2 ถัง แต่ถ้าเรียงตามรหัสพวกเขาจะต้อง 3 ถัง
Stoleg

คำตอบ:


9

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

อย่างไรก็ตามนี่คือรหัส:

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 ขึ้นใหม่เติมเต็ม 1000000 แถวแล้วกลับไปทำงาน

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

หากคุณมีตัวเลือกในการอัปเกรดเป็น SQL 2012 คุณสามารถดูที่ตัวคั่นหน้าต่างใหม่ที่รองรับการรวมหน้าต่างการย้ายซึ่งจะให้ประสิทธิภาพที่ดีขึ้น

ในบันทึกด้านข้างหากคุณมีแอสเซมบลีที่ติดตั้งด้วย permission_set = safe คุณสามารถทำสิ่งที่ไม่ดีต่อเซิร์ฟเวอร์ที่มี T-SQL มาตรฐานมากกว่าแอสเซมบลีได้ดังนั้นฉันจะพยายามกำจัดสิ่งกีดขวางนั้นต่อไป - คุณมีประโยชน์ กรณีที่นี่ที่ CLR จะช่วยคุณจริงๆ


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

9

ไม่มีฟังก์ชันการสร้างหน้าต่างใหม่ใน SQL Server 2012 การทำ Windowing แบบซับซ้อนสามารถทำได้ด้วยการใช้ CTE แบบเรียกซ้ำ ฉันสงสัยว่าวิธีนี้จะทำงานได้ดีกับหลายล้านแถว

โซลูชันต่อไปนี้ครอบคลุมทุกกรณีที่คุณอธิบาย คุณสามารถดูได้ในการดำเนินการที่นี่ใน SQL ซอ

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

ตอนนี้หายใจเข้าลึก ๆ มี CTE สำคัญสองตัวที่นี่ซึ่งแต่ละอันนำหน้าด้วยความคิดเห็นสั้น ๆ ส่วนที่เหลือเป็นเพียง CTE ที่ "ล้าง" เพื่อดึงแถวที่ถูกต้องหลังจากเราได้จัดอันดับแล้ว

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

วิธีแก้ปัญหานี้จะถือว่าidเป็นลำดับที่ไม่มีช่องว่าง ถ้าไม่คุณจะต้องสร้างลำดับที่ไม่มีช่องว่างของคุณเองโดยเพิ่ม CTE เพิ่มเติมในตอนต้นที่กำหนดจำนวนแถวด้วยROW_NUMBER()ตามลำดับที่ต้องการ (เช่นROW_NUMBER() OVER (ORDER BY value DESC))

ตรงไปตรงมานี่เป็นคำพูดที่ค่อนข้างสมบูรณ์


1
วิธีนี้ดูเหมือนจะไม่แก้ปัญหากรณีที่แถวอาจมีส่วนร่วมกับขนาดของถังหลาย ผลรวมสะสมนั้นง่าย แต่ฉันต้องการผลรวมนั้นเพื่อรีเซ็ตทุกครั้งที่มาถึง 1 ดูตารางตัวอย่างสุดท้ายในคำถามของฉันและเปรียบเทียบcrude_sumกับdistinct_sumและbucketคอลัมน์ที่เกี่ยวข้องเพื่อดูว่าฉันหมายถึงอะไร
Zikes

2
@ Zikes - ฉันได้แก้ไขกรณีนี้ด้วยโซลูชันที่อัปเดตแล้วของฉัน
Nick Chammas

ดูเหมือนว่าควรจะใช้งานได้ในขณะนี้ ฉันจะทำงานกับการรวมเข้ากับฐานข้อมูลของฉันเพื่อทดสอบ
Zikes

@ Zikes - เพียงแค่อยากรู้วิธีการแก้ปัญหาต่าง ๆ ที่โพสต์ที่นี่ทำงานกับชุดข้อมูลขนาดใหญ่ของคุณได้อย่างไร ฉันเดาว่า Andriy นั้นเร็วที่สุด
Nick Chammas

5

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

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 ฉันคิดว่านี่อาจเป็นไปได้ถ้ามีดัชนีที่เหมาะสม
Jon Seigel

3

ต่อไปนี้จะเป็นอีกหนึ่งวิธีการแก้ปัญหา CTE recursive แม้ว่าผมว่ามันตรงไปตรงมามากกว่า@ ข้อเสนอแนะของนิค จริง ๆ แล้วมันใกล้กับเคอร์เซอร์ของ @ Sebastianเพียงฉันใช้ความแตกต่างในการวิ่งแทนที่จะใช้ผลรวมทั้งหมด (ตอนแรกฉันคิดว่า @ คำตอบของ Nick จะเป็นไปตามสิ่งที่ฉันแนะนำที่นี่และหลังจากเรียนรู้ว่าจริงๆแล้วเขาเป็นคำถามที่แตกต่างกันมากที่ฉันตัดสินใจเสนอของฉัน)

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
;

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

ของ SQL ซอสาธิตสำหรับการค้นหานี้สามารถพบได้ที่นี่


นี่สั้นกว่าที่ฉันเขียนมาก ทำได้ดีมาก มีเหตุผลใดบ้างที่คุณนับถอยหลังห้องที่เหลืออยู่ในถังแทนที่จะนับ
Nick Chammas

ใช่มีไม่แน่ใจว่าเหมาะสมหรือไม่สำหรับรุ่นที่ฉันลงรายการบัญชีที่นี่ อย่างไรก็ตามเหตุผลก็คือว่ามันง่ายกว่า / เป็นธรรมชาติมากขึ้นในการเปรียบเทียบค่าเดียวกับค่าเดียว ( sizeกับroom_left) เมื่อเทียบกับการเปรียบเทียบค่าเดียวกับการแสดงออก ( 1กับrunning_size+ size) ฉันไม่ได้ใช้is_new_bucketธงในตอนแรก แต่มีหลายคนCASE WHEN t.size > r.room_left ...แทน ("หลายคน" เพราะฉันยังคำนวณ (และกลับ) ขนาดทั้งหมด แต่ก็คิดว่ามันต่อต้านเพื่อความเรียบง่าย) ดังนั้นฉันจึงคิดว่ามันจะสง่างามกว่า ทางนั้น.
Andriy M
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.