Як я можу написати віконний запит, який підсумовує стовпець, щоб створити дискретні відра?


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

Ви збираєтеся агрегувати дані відповідно до відра, яке ви обчислюєте, або номер відра є остаточною відповіддю, яку ви шукаєте?
Джон Сейгель

2
Ак. Ймовірно, я б запускав додаток на стороні клієнта, оскільки це підтримуватиме кращу трансляцію записів, на відміну від циклу курсора, який отримує по одному рядку. Я думаю, що доки всі оновлення виконуються партіями, це повинно працювати досить добре.
Джон Сейгель

1
Як уже згадували інші, вимога призупинення distinct_countскладних речей. Aaron Bertrand має велике резюме ваших варіантів роботи на SQL Server для такого роду роботи з вікон. Я використовував метод "химерного оновлення" для обчислення distinct_sum, який ви можете побачити тут, на SQL Fiddle , але це ненадійно.
Нік Чаммас

1
@JonSeigel Слід зазначити, що проблему розміщення X-елементів у мінімальній кількості відер неможливо ефективно вирішити, використовуючи алгоритм рядка за рядком мови SQL. Напр., Розмірами 0,7; 0,8; 0,3 знадобиться 2 відра, але якщо відсортовано за id, їм знадобляться 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, ви можете ознайомитись з новими агрегатами рухомих вікон, що підтримуються котушкою, які мають забезпечити кращу продуктивність.

Зі сторони, якщо у вас встановлена ​​збірка з дозвілом_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це безперервна послідовність. Якщо ні, вам потрібно буде сформувати власну послідовність без змін, додавши на початку додатковий CTE, який нумерує рядки ROW_NUMBER()відповідно до потрібного порядку (наприклад ROW_NUMBER() OVER (ORDER BY value DESC)).

Це, мабуть, досить багатослівно.


1
Це рішення, схоже, не стосується випадку, коли ряд може сприяти своєму розміру в декількох відрах. Прокатна сума досить легко, але мені потрібна ця сума , щоб скинути кожен раз , коли він досягає 1. Див останнього прикладу таблиці в моєму питанні і порівняти crude_sumз distinct_sumі пов'язаними з ними bucketстовпцями , щоб побачити , що я маю в виду.
Zikes

2
@Zikes - я вирішив цю справу своїм оновленим рішенням.
Нік Чаммас

Схоже, це має працювати зараз. Я буду працювати над інтеграцією його в свою базу даних, щоб перевірити її.
Zikes

@Zikes - Цікаво, як різні розміщені тут рішення відносно вашого великого набору даних? Я здогадуюсь, Андрій найшвидший.
Нік Чаммас

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 Я думаю, що це має потенціал, якщо є відповідні показники.
Джон Сейгель

3

Далі - ще одне рекурсивне рішення CTE, хоча я б сказав, що це більш просто, ніж пропозиція @ Ніка . Це насправді ближче до курсора @ Себастьяна , тільки я використовував запущені відмінності замість запуску підсумків. (Спочатку я навіть подумав, що відповідь @ Ніка буде відповідати тому, що я пропоную тут, і саме після того, як дізнався, що його насправді був зовсім інший запит, я вирішив запропонувати свою.)

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 для цього запиту можна знайти тут .


Це набагато коротше, ніж те, що я написав. Хороша робота. Чи є якась причина, коли ви відраховуєте залишене у відрі приміщення, а не рахуєте?
Нік Шамма

Так, є, не впевнений, чи має багато сенсу версія, яку я закінчив публікувати тут. У будь-якому випадку, причина полягала в тому, що здавалося легше / природніше порівнювати одне значення з одним значенням ( sizeз room_left) на відміну від порівняння одного значення з виразом ( 1з running_size+ size). is_new_bucketСпочатку я не використовував прапор, а декілька CASE WHEN t.size > r.room_left ...("кілька", тому що я також обчислював (і повертав) загальний розмір, але потім подумав проти цього заради простоти), тому подумав, що це буде більш елегантно цей шлях.
Андрій М
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.