Рекурсия: сколько строк добавляется на каждой итерации из recur. member'а ?

Сельский учитель
Дата: 22.01.2009 22:54:10
Всем привет. Заранее прошу пардон: тут очень длинное предисловие, но без него будет просто непонятен вопрос.

Есть большой список электронных контрольных работ, сформирован один раз и НЕ пополняется.
Он делится на "пачки" разного размера и они передаются проверяющему автомату с интервалом в 5 минут. Например, в момент времени=100 передаётся на проверку 4 работы, в момент времени=105 - 7 работ и т.п. Каждая работа имеет "штамп" времени (dts_stamp), когда она в первый раз была подана на проверку. Кроме того, она имеет id автора (person_id), который присваивается студентам в хронологическом порядке.

Автомат определяет одну из работ пачки как "лучшую" и начинает выполнять с ней другие манипуляции (например, подсчет слов и т.п.), которые длятся ровно 5 минут.
Остальные работы "заворачиваются" и через эти самые 5 минут снова идут "в горнило" автомата, но уже в составе НОВОЙ пачки, т.е. вместе с ними - еще и другие работы. Так длится до тех пор, пока автомат не прожует все работы списка. Хочу обратить внимание, что на каждой итерации список получается разным, в нём есть как "старые", так и "новые" работы, т.е. выбор лучшая работа определяется как из повторно проверяемых "старых", так и из "новых".

Критерий отбора "лучшей" работы из пачки:
1) в ней содержится меньше всего ошибок (faults_qty)
2) при равенстве числа ошибок в >=2 работах выбирается та, которая начинала проверяться раньше, т.е. имеет меньший dts_stamp
3) при равенстве по пп 1 и 2 - та, у которой меньший person_id.

Поскольку сам список с момента первой проверки уже НЕ пополняется, то когда-то наступит момент, при котором НОВЫЕ работы поступать уже не будут, т.е. будут только перепроверяться ранее выполнявшиеся. В результате список будет весь переработан.

ТРЕБУЕТСЯ: для каждого person_id, по имеющимся данным о числе ошибок в его работе (faults_qty) и моменту начала её первой проверки (dts_stamp) определить, когда автомат "заберёт" её как "лучшую".

Мы тут с коллегами поспорили: можно ли эту задачу решить одним селектом (видимо, рекурсивным) ? Вроде, всё должно получиться: определяем anchor-часть как список работ, поступивших на проверку в первой пачке (в примере - отметка времени 100). В этой части находим одну, самую "лучшую", и пишем её person_id в поле passed_Ok. Остальные - в поле delay2next (типа, "до следующей проверки"). Это - первая проверка, поле step=1.
Затем делаем UNION ALL и соединяем общий список (p) с рекурсивной СТЕ (cx) так, чтобы на вторую проверку (step=2) попали:
1) те работы, которые не прошли в первой проверке: p.person_id=cx.delay2next
2) новые работы, которые не были в первой пачке и появились только сейчас:
or cx.passed_Ok>0 and (p.dts_stamp-p.dts_init)/5=cx.step
/*условие cx.passed_Ok>0 означает соединение только с одной записью, чтобы не было дублей строк, условие (p.dts_stamp-p.dts_init)/5=cx.step означает отбор только тех, которые пришли на проверку через 5 минут*/

Так вот: попытки рекурсивного решения привели к "открытию": одно обращение к рекурсивному member'у приводит к тому, что данные, получаемые "внутри" него, добавляются к СТЕ как-то странно. Часть строк выдается "на гора" одна-за-одной, а другая часть - "все скопом". Хотя это ОДИН шаг, ОДНО обращение к рекурсивному члену. В результате, результат выборки, которая указана после UNION ALL, не даёт возможность найти в нём "лучшую" запись.

Вот скрипт:
if object_id('tempdb..#t') is not null drop table tempdb..#t
go
create table #t(person_id int, dts_stamp int, faults_qty int, dts_init int)
insert #t values(9761,100,125,100)
insert #t values(9805,100,125, 100)
insert #t values(9888,100,125, 100)
insert #t values(9890,100,125, 100)

insert #t values(9795,105,114, 100)
insert #t values(9799,105,114, 100)
insert #t values(9803,105,125, 100)
insert #t values(9823,105,124, 100)
insert #t values(9833,105,123, 100)
insert #t values(9843,105,122, 100)
insert #t values(9853,105,121, 100)

insert #t values(9802,110,125, 100)
insert #t values(9790,110,112, 100)
insert #t values(9794,110,114, 100)

insert #t values(9798,115,115, 100)
insert #t values(9800,115,115, 100)
go
--select * from #t


WITH
cteTest as
(
  select person_id
        ,dts_stamp
        ,faults_qty
        ,passed_Ok=
          case when not exists(select * from #t pz 
                                where pz.dts_stamp=p.dts_stamp and 
                                     (pz.faults_qty<p.faults_qty or 
                                      pz.faults_qty=p.faults_qty and pz.person_id<p.person_id 
                                     )
                              ) 
               then person_id 
           end
        ,delay2next=
          case when exists(select * from #t pz 
                            where pz.dts_stamp=p.dts_stamp and 
                                 (pz.faults_qty<p.faults_qty or 
                                  pz.faults_qty=p.faults_qty and pz.person_id<p.person_id 
                                 ) 
                          ) 
               then person_id 
           end
        ,step=1
        ,r_debug=0
        ,q_debug=0
    from #t p where not exists(select * from #t px 
                                where px.dts_stamp<p.dts_stamp or 
                                      px.dts_stamp=p.dts_stamp and px.faults_qty<p.faults_qty  
                              )

  UNION ALL

 select p.person_id
       ,p.dts_stamp
       ,p.faults_qty
       ,passed_Ok=
          case when 1=row_number()over
                      (partition by cx.step+1 order by p.faults_qty,p.dts_stamp,p.person_id) 
               then p.person_id 
           end
       ,delay2next=
          case when 1<row_number()over
                     (partition by cx.step+1 order by p.faults_qty,p.dts_stamp,p.person_id) 
               then p.person_id 
           end
       ,step=cx.step+1
       ,r_debug=cast(row_number()over
                     (partition by cx.step+1 order by p.faults_qty,p.dts_stamp,p.person_id) 
                     as int
                    )
       ,q_debug=count(*)over()
        from #t p
 join cteTest cx
          on 
            p.person_id=cx.delay2next /*работы, не прошедшие в предыдущий раз*/
             or 
            cx.passed_Ok>0 and (p.dts_stamp-p.dts_init)/5=cx.step  /*работы, поступившие сейчас в первый раз*/
)

select * from cteTest order by step

В результате его выполнения будет 70 строк, вот данные только для step=1 и step=2:

person_iddts_stampfaults_qtypassed_Okdelay2nextstepr_debugq_debug
97611001259761NULL100
9805100125NULL9805100
9888100125NULL9888100
9890100125NULL9890100
98901001259890NULL211
98881001259888NULL211
98051001259805NULL211
97951051149795NULL217
9799105114NULL9799227
9853105121NULL9853237
9843105122NULL9843247
9833105123NULL9833257
9823105124NULL9823267
9803105125NULL9803277


Видно, что на первом шаге (step=1) всё Ок: прошла 1 работа, 9761, а остальные (9805, 9888 и 9890) вернулись в общий список. Эти три работы ПЛЮС еще 7 других будут снова отданы автомату на проверку на втором шаге. То есть, на втором шаге автомат должен будет выбрать "лучшую" из ДЕСЯТИ работ. Но таблица показывает, что на самом деле в СТЕ сначала добавляются те самые три, которые были на первом шаге, и только затем туда добавляются 7. Да, при step=2 добавились действительно 10 строк, но они добавились... вроде как не за один присест, а за два ?!
Автомат считает "лучшими" для step=2 не одну, а сразу 4 работы: три с прошлого прохода, и одну (9795) из семи новых.

Получается, для определения "лучшей" из добавленных строк нельзя применить оконную функцию типа row_number()over(partition by step order by faults_qty, dts_stamp, person_id). Она проставит напротив каждой значения, указанные в графе r_debug, т.е. как будто бы добавления шли порознь.

Нельзя также применять группировку - синтаксис не позволяет. Нельзя также определять через where not exists(select * from ctetest cx2 where ...) - вылезет ошибка многократной ссылки на СТЕ внутри неё самой.

Итак, у меня вопрос: действительно ли может так оказаться, что за ОДНО обращение к рекурсивной части СУБД будет дополнять СТЕ строками НЕ за один "присест", а за несколько ?

Спасибо всем, кто дочитал. А тем, кто ответит по существу - большое спасибо! :-)
daw
Дата: 25.01.2009 20:58:48

в 2008-ом вообще запретили использовать оконные функции в рекурсивной
части cte. и на то были причины. почитайте, по-моему, вы как раз на это
попали:
http://social.msdn.microsoft.com/forums/en-US/transactsql/thread/dd87748d-91fb-454b-8c75-87399ac3cdf7/

Posted via ActualForum NNTP Server 1.4