Làm cách nào tôi có thể viết truy vấn cửa sổ tính tổng một cột để tạo các nhóm rời rạc?


11

Tôi có một bảng bao gồm một cột các giá trị thập phân, chẳng hạn như:

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

Những gì tôi cần phải hoàn thành là một chút khó khăn để mô tả, vì vậy xin vui lòng chịu đựng với tôi. Những gì tôi đang cố gắng làm là tạo một giá trị tổng hợp của sizecột tăng 1 mỗi lần các hàng trước tổng hợp thành 1, khi theo thứ tự giảm dần theo value. Kết quả sẽ trông giống như thế này:

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

Nỗ lực đầu tiên ngây thơ của tôi là tiếp tục chạy SUMvà sau đó CEILINGlà giá trị đó, tuy nhiên nó không xử lý trường hợp một số hồ sơ sizecuối cùng đóng góp vào tổng số hai thùng riêng biệt. Ví dụ dưới đây có thể làm rõ điều này:

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

Như bạn có thể thấy, nếu tôi chỉ đơn giản sử dụng CEILINGtrong crude_sumbản ghi số 8 thì sẽ được gán cho nhóm 2. Điều này xảy ra do các sizebản ghi số 5 và số 8 bị chia thành hai nhóm. Thay vào đó, giải pháp lý tưởng là đặt lại tổng mỗi lần đạt 1, sau đó tăng bucketcột và bắt đầu một SUMthao tác mới bắt đầu từ sizegiá trị của bản ghi hiện tại. Vì thứ tự của các bản ghi rất quan trọng đối với thao tác này, tôi đã bao gồm valuecột, được dự định sắp xếp theo thứ tự giảm dần.

Những nỗ lực ban đầu của tôi đã liên quan đến việc thực hiện nhiều lần truyền dữ liệu, một lần để thực hiện SUMthao tác, một lần nữa cho CEILINGđiều đó, v.v ... Dưới đây là một ví dụ về những gì tôi đã làm để tạo crude_sumcột:

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

Mà đã được sử dụng trong một UPDATEhoạt động để chèn giá trị vào một bảng để làm việc sau này.

Chỉnh sửa: Tôi muốn thực hiện một cú đâm khác để giải thích điều này, vì vậy hãy đến đây. Hãy tưởng tượng mỗi bản ghi là một mục vật lý. Mục đó có giá trị liên quan đến nó và kích thước vật lý nhỏ hơn một. Tôi có một loạt các thùng có dung tích chính xác là 1, và tôi cần xác định mình sẽ cần bao nhiêu thùng và mỗi thùng sẽ đi theo giá trị của vật phẩm, được sắp xếp từ cao nhất đến thấp nhất.

Một vật phẩm vật lý không thể tồn tại ở hai nơi cùng một lúc, vì vậy nó phải ở trong một thùng này hoặc thùng kia. Đây là lý do tại sao tôi không thể thực hiện CEILINGgiải pháp tổng + đang chạy , vì điều đó sẽ cho phép các bản ghi đóng góp kích thước của chúng vào hai nhóm.


Bạn nên thêm SQL của mình để làm rõ những nỗ lực ban đầu của bạn.
mdahlman

Bạn sẽ tổng hợp dữ liệu theo nhóm bạn đang tính toán hay số xô là câu trả lời cuối cùng mà bạn đang tìm kiếm?
Jon Seigel

2
Ack. Tôi có thể đi với một ứng dụng phía máy khách vì điều đó sẽ hỗ trợ truyền trực tiếp các bản ghi tốt hơn so với vòng lặp con trỏ tìm nạp một hàng mỗi lần. Tôi nghĩ miễn là tất cả các bản cập nhật được thực hiện theo đợt, thì nó sẽ hoạt động tốt.
Jon Seigel

1
Như những người khác đã đề cập, yêu cầu xô lệch về distinct_countnhững điều phức tạp. Aaron Bertrand có một bản tóm tắt tuyệt vời về các tùy chọn của bạn trên SQL Server cho loại công việc cửa sổ này. Tôi đã sử dụng phương pháp "cập nhật kỳ quặc" để tính toán distinct_sum, mà bạn có thể thấy ở đây trên SQL Fiddle , nhưng điều này không đáng tin cậy.
Nick Chammas

1
@JonSeigel Chúng ta nên lưu ý rằng vấn đề đặt các mục X trong số lượng nhóm tối thiểu không thể được giải quyết một cách hiệu quả bằng thuật toán hàng theo ngôn ngữ SQL. Ví dụ: các mục có kích thước 0,7; 0,8; 0,3 sẽ cần 2 thùng, nhưng nếu được sắp xếp theo id, chúng sẽ cần 3 thùng.
Stoleg

Câu trả lời:


9

Tôi không chắc chắn loại hiệu suất bạn đang tìm kiếm, nhưng nếu CLR hoặc ứng dụng bên ngoài không phải là một tùy chọn, con trỏ là tất cả những gì còn lại. Trên máy tính xách tay cũ của tôi, tôi nhận được 1.000.000 hàng trong khoảng 100 giây bằng giải pháp sau. Điều tuyệt vời ở đây là nó có tỷ lệ tuyến tính, vì vậy tôi sẽ xem xét khoảng 20 phút để xem toàn bộ nội dung. Với một máy chủ tốt, bạn sẽ nhanh hơn, nhưng không phải là một thứ tự lớn, vì vậy sẽ vẫn mất vài phút để hoàn thành việc này. Nếu đây là một quá trình tắt, có lẽ bạn có thể đủ khả năng chậm. Nếu bạn cần chạy báo cáo này dưới dạng báo cáo hoặc tương tự thường xuyên, bạn có thể muốn lưu trữ các giá trị trong cùng một bảng, không cập nhật chúng khi các hàng mới được thêm vào, ví dụ như trong trình kích hoạt.

Dù sao, đây là mã:

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;

Nó thả và tạo lại bảng MyTable, điền vào nó 1000000 hàng và sau đó đi làm.

Con trỏ sao chép từng hàng vào bảng tạm thời trong khi chạy các phép tính. Cuối cùng, chọn trả về kết quả tính toán. Bạn có thể nhanh hơn một chút nếu bạn không sao chép dữ liệu xung quanh nhưng thay vào đó hãy cập nhật tại chỗ.

Nếu bạn có một tùy chọn để nâng cấp lên SQL 2012, bạn có thể xem tập hợp cửa sổ di chuyển được hỗ trợ của bộ đệm cửa sổ mới, điều đó sẽ mang lại cho bạn hiệu suất tốt hơn.

Mặt khác, nếu bạn có một hội đồng được cài đặt với allow_set = safe, bạn có thể thực hiện nhiều nội dung xấu hơn cho máy chủ có T-SQL tiêu chuẩn so với lắp ráp, vì vậy tôi sẽ tiếp tục xóa bỏ rào cản đó - Bạn có thể sử dụng tốt trường hợp ở đây nơi CLR thực sự sẽ giúp bạn.


Tôi đã chấp nhận điều này do cách dễ dàng thực hiện và tôi có thể dễ dàng thay đổi và gỡ lỗi sau này khi có nhu cầu. Câu trả lời của @ NickChammas cũng đúng và có thể chạy hiệu quả hơn, vì vậy tôi đoán đó là vấn đề ưu tiên cho bất kỳ ai khác gặp phải vấn đề tương tự.
Zike

9

Không có các chức năng cửa sổ mới trong SQL Server 2012, việc tạo cửa sổ phức tạp có thể được thực hiện bằng việc sử dụng các CTE đệ quy. Tôi tự hỏi làm thế nào điều này sẽ thực hiện tốt đối với hàng triệu hàng.

Các giải pháp sau đây bao gồm tất cả các trường hợp bạn mô tả. Bạn có thể thấy nó hoạt động ở đây trên SQL Fiddle .

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

Bây giờ hãy hít một hơi thật sâu. Có hai CTE chính ở đây, mỗi CT trước một nhận xét ngắn gọn. Phần còn lại chỉ là các CTE "dọn dẹp", ví dụ, để kéo các hàng bên phải sau khi chúng tôi xếp hạng chúng.

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

Giải pháp này giả định rằng đó idlà một chuỗi không khoảng cách. Nếu không, bạn sẽ cần tạo chuỗi không khe hở của riêng mình bằng cách thêm CTE bổ sung vào đầu, đánh số các hàng ROW_NUMBER()theo thứ tự mong muốn (ví dụ ROW_NUMBER() OVER (ORDER BY value DESC)).

Fankly, điều này là khá dài dòng.


1
Giải pháp này dường như không giải quyết được trường hợp một hàng có thể đóng góp kích thước của nó cho nhiều nhóm. Tổng tiền là đủ dễ dàng, nhưng tôi cần tổng đó để đặt lại mỗi lần đạt 1. Xem bảng ví dụ cuối cùng trong câu hỏi của tôi và so sánh crude_sumvới distinct_sumcác bucketcột liên quan của chúng để xem ý tôi là gì.
Zike

2
@Zike - Tôi đã giải quyết trường hợp này với giải pháp cập nhật của tôi.
Nick Chammas

Có vẻ như nó nên hoạt động bây giờ. Tôi sẽ làm việc tích hợp nó vào cơ sở dữ liệu của mình để kiểm tra.
Zike

@Zike - Chỉ tò mò, làm thế nào để các giải pháp khác nhau được đăng ở đây thực hiện đối với tập dữ liệu lớn của bạn? Tôi đoán là của tôi là nhanh nhất.
Nick Chammas

5

Điều này cảm thấy giống như một giải pháp ngớ ngẩn, và nó có thể sẽ không mở rộng tốt, vì vậy hãy kiểm tra cẩn thận nếu bạn sử dụng nó. Vì vấn đề chính xuất phát từ "khoảng trống" còn lại trong thùng, trước tiên tôi phải tạo một bản ghi phụ để liên kết vào dữ liệu.

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 Tôi nghĩ rằng điều này có tiềm năng nếu có các chỉ số phù hợp.
Jon Seigel

3

Sau đây là một giải pháp CTE đệ quy khác, mặc dù tôi muốn nói rằng nó đơn giản hơn đề xuất của @ Nick . Nó thực sự gần với con trỏ của @ Sebastian hơn , chỉ có tôi sử dụng sự khác biệt khi chạy thay vì chạy tổng số. .

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
;

Lưu ý: truy vấn này giả định rằng valuecột bao gồm các giá trị duy nhất không có khoảng trống. Nếu đó không phải là trường hợp, bạn sẽ cần phải giới thiệu một cột xếp hạng được tính toán dựa trên thứ tự giảm dần valuevà sử dụng nó trong CTE đệ quy thay vì valuetham gia phần đệ quy với neo.

Một bản demo SQL Fiddle cho truy vấn này có thể được tìm thấy ở đây .


Điều này ngắn hơn nhiều so với những gì tôi đã viết. Công việc tốt đẹp. Có bất kỳ lý do bạn đếm ngược phòng còn lại trong xô hơn là đếm lên?
Nick Chammas

Vâng, có, không chắc chắn nếu nó có ý nghĩa nhiều cho phiên bản tôi đã kết thúc đăng ở đây, mặc dù. Dù sao, lý do là có vẻ dễ dàng / tự nhiên hơn khi so sánh một giá trị với một giá trị duy nhất ( sizevới room_left) so với so sánh so sánh một giá trị với một biểu thức ( 1với running_size+ size). Lúc đầu tôi không sử dụng is_new_bucketcờ nhưng một số CASE WHEN t.size > r.room_left ...thay vào đó ("vài" vì tôi cũng đang tính toán (và trả lại) tổng kích thước, nhưng sau đó nghĩ chống lại nó vì đơn giản), vì vậy tôi nghĩ rằng nó sẽ thanh lịch hơn theo cách đó
Andriy M
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.