개별 버킷을 만들기 위해 열을 합하는 윈도우 쿼리를 작성하는 방법은 무엇입니까?


11

다음과 같은 10 진수 열이 포함 된 테이블이 있습니다.

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

당신이 볼 수 있듯이 단순히 사용하는 것 인 경우에, CEILINGcrude_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보다 작습니다. 볼륨 용량이 정확히 1 인 일련의 버킷이 있으며 필요한 버킷 수와 항목 값에 따라 각 버킷이 들어가는 버킷 수를 결정해야합니다.

물리적 품목은 한 번에 두 곳에 존재할 수 없으므로 한 버킷 또는 다른 버킷에 있어야합니다. 그렇기 CEILING때문에 누적 합계 + 솔루션을 수행 할 수없는 이유 는 레코드가 두 개의 버킷에 크기를 제공 할 수 있기 때문입니다.


초기 시도에 포함 된 내용을 명확하게하기 위해 SQL을 추가해야합니다.
mdahlman 2016 년

계산중인 버킷에 따라 데이터를 집계 할 예정입니까, 아니면 버킷 번호가 원하는 최종 답변입니까?
Jon Seigel 2016 년

2
Ack. 한 번에 한 행씩 가져 오는 커서 루프와 달리 레코드 스트리밍을 더 잘 지원하기 때문에 클라이언트 측 응용 프로그램을 사용했을 것입니다. 모든 업데이트가 일괄 적으로 수행되는 한 합리적으로 잘 수행되어야한다고 생각합니다.
Jon Seigel

1
다른 사람들이 이미 언급했듯이 버킷 팅 요구 사항은 distinct_count복잡합니다. Aaron Bertrand는 이러한 종류의 윈도우 작업에 대한 SQL Server 옵션에 대한 요약을 제공합니다 . "기발한 업데이트"방법을 사용하여 계산 했습니다. SQL Fiddle 에서 distinct_sum볼 수 있지만 신뢰할 수 없습니다.
Nick Chammas 2016 년

1
@JonSeigel 최소 개수의 버킷에 X 항목을 배치하는 문제는 SQL 언어의 행 단위 알고리즘을 사용하여 효율적으로 해결할 수 없습니다. 예를 들어 0.7; 0.8; 0.3 크기의 항목은 2 개의 버킷이 필요하지만 ID별로 정렬 된 경우 3 개의 버킷이 필요합니다.
Stoleg

답변:


9

어떤 유형의 성능을 찾고 있는지 잘 모르겠지만 CLR 또는 외부 앱이 옵션이 아닌 경우 커서 만 남았습니다. 오래된 노트북에서 다음 솔루션을 사용하여 약 100 초 동안 1,000,000 개의 행을 통과합니다. 그것에 대한 좋은 점은 그것이 선형으로 확장된다는 것입니다. 그래서 나는 전체 일을 통과하는 데 약 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 행으로 채운 다음 작동합니다.

커서는 계산을 실행하는 동안 각 행을 임시 테이블에 복사합니다. 마지막에 select는 계산 된 결과를 반환합니다. 데이터를 복사하지 않고 대신 전체 업데이트를 수행하면 약간 더 빠를 수 있습니다.

SQL 2012로 업그레이드 할 수있는 옵션이있는 경우 새로운 창 스풀 지원 이동 창 집합을 보면 성능이 향상됩니다.

참고로, permission_set = safe로 어셈블리를 설치 한 경우 어셈블리보다 표준 T-SQL을 사용하여 서버에 더 나쁜 일을 할 수 있으므로 해당 장벽을 제거하기 위해 계속 노력할 것입니다. CLR이 실제로 당신을 도울 것입니다.


구현하기가 쉽고 필요에 따라 나중에 쉽게 변경하고 디버깅 할 수 있기 때문에이 것을 받아 들였습니다. @ NickChammas의 대답도 정확하고 아마 더 효율적으로 실행될 수 있으므로 비슷한 문제에 대해 다른 사람이 선호하는 것 같습니다.
Zikes

9

SQL Server 2012의 새로운 윈도우 기능이 없으면 재귀 CTE를 사용하여 복잡한 윈도우를 수행 할 수 있습니다. 이것이 수백만 행에 대해 얼마나 잘 수행되는지 궁금합니다.

다음 솔루션은 설명 된 모든 사례를 다룹니다. 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
;

이제 심호흡하십시오. 여기에는 두 가지 주요 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은 이것이 갭리스 시퀀스 라고 가정합니다 . 그렇지 않은 경우 ROW_NUMBER()원하는 순서에 따라 행에 번호를 매기는 추가 CTE를 추가하여 고유 한 간격없는 시퀀스를 생성해야합니다 (예 :) ROW_NUMBER() OVER (ORDER BY value DESC).

기발하게, 이것은 매우 장황하다.


1
이 솔루션은 행이 여러 버킷에 크기를 기여할 수있는 경우를 다루지 않는 것 같습니다. 롤링 합계는 충분히 쉽지만 1에 도달 할 때마다 재설정하려면 해당 합계가 필요합니다. 내 질문의 마지막 예제 테이블 을 참조 하고 관련 열과 비교 crude_sum하여 의미하는 바를 확인하십시오. distinct_sumbucket
Zikes

2
@Zikes-업데이트 된 솔루션 으로이 사례를 해결했습니다.
Nick Chammas

이제 작동하는 것 같습니다. 테스트하기 위해 데이터베이스에 통합하는 작업을합니다.
Zikes

@Zikes-여기에 게시 된 다양한 솔루션이 대규모 데이터 세트에 대해 어떻게 수행됩니까? Andriy가 가장 빠르다고 생각합니다.
Nick Chammas 2016 년

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 2016 년

3

다음은 또 다른 재귀 적 CTE 솔루션이지만 @Nick의 제안 보다 더 간단하다고 말할 수 있습니다. 실제로 @Sebastian의 cursor에 더 가깝습니다 . 계산 합계 대신 달리기 차이 만 사용했습니다. (처음에는 @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 Fiddle 데모는 여기 에서 찾을 수 있습니다 .


이것은 내가 쓴 것보다 훨씬 짧습니다. 잘 했어. 당신이 계산하지 않고 양동이에 방을 남겨둔 이유가 있습니까?
Nick Chammas 2016 년

그래, 내가 여기에 게시 한 버전에 대해 의미가 있는지 확실하지 않습니다. 어쨌든, 이유는 (하나의 값으로 단일 값을 비교하는 것이 더 자연 / 쉽게 듯했다 sizeroom_left(식으로 하나의 값을 비교 반대) 1running_size+ size). 나는 is_new_bucket처음 에는 깃발을 사용하지 않았고 CASE WHEN t.size > r.room_left ...대신에 몇 개를 대신 사용했습니다 (전체 크기를 계산하고 반환하기 때문에 "여러").하지만 단순성을 위해 생각했습니다. 그런 식으로.
Andriy M
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.