Günlük programı [Başlangıç ​​tarihi; Bitiş tarihi] hafta günleri listesiyle aralıklarla


18

Verileri iki sistem arasında dönüştürmem gerekiyor.

İlk sistem, programları basit bir tarih listesi olarak saklar. Programa dahil edilen her tarih bir satırdır. Tarihler dizisinde çeşitli boşluklar olabilir (hafta sonları, resmi tatiller ve daha uzun aralar, haftanın bazı günleri programdan çıkarılabilir). Hiç boşluk olmayabilir, hafta sonları bile dahil edilebilir. Program 2 yıla kadar olabilir. Genellikle birkaç hafta sürer.

Hafta sonları hariç iki hafta süren basit bir program örneği (aşağıdaki senaryoda daha karmaşık örnekler var):

+----+------------+------------+---------+--------+
| ID | ContractID |     dt     | dowChar | dowInt |
+----+------------+------------+---------+--------+
| 10 |          1 | 2016-05-02 | Mon     |      2 |
| 11 |          1 | 2016-05-03 | Tue     |      3 |
| 12 |          1 | 2016-05-04 | Wed     |      4 |
| 13 |          1 | 2016-05-05 | Thu     |      5 |
| 14 |          1 | 2016-05-06 | Fri     |      6 |
| 15 |          1 | 2016-05-09 | Mon     |      2 |
| 16 |          1 | 2016-05-10 | Tue     |      3 |
| 17 |          1 | 2016-05-11 | Wed     |      4 |
| 18 |          1 | 2016-05-12 | Thu     |      5 |
| 19 |          1 | 2016-05-13 | Fri     |      6 |
+----+------------+------------+---------+--------+

IDbenzersizdir, ancak mutlaka sıralı değildir (birincil anahtardır). Tarihler her Sözleşme içinde benzersizdir (tarihinde benzersiz bir indeks vardır (ContractID, dt)).

İkinci sistem, programları, programın parçası olan hafta günleri listesiyle aralıklarla saklar. Her aralık, başlangıç ​​ve bitiş tarihleri ​​(dahil) ve programa dahil edilen hafta günlerinin bir listesi ile tanımlanır. Bu formatta, Mon-Wed gibi tekrarlayan haftalık kalıpları etkili bir şekilde tanımlayabilirsiniz, ancak bir kalıp bozulduğunda, örneğin resmi tatil için bir acı haline gelir.

Yukarıdaki basit örnek şöyle görünecektir:

+------------+------------+------------+----------+----------------------+
| ContractID |  StartDT   |   EndDT    | DayCount |       WeekDays       |
+------------+------------+------------+----------+----------------------+
|          1 | 2016-05-02 | 2016-05-13 |       10 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+

[StartDT;EndDT] Aynı Sözleşmeye ait aralıklar çakışmamalıdır.

İlk sistemden ikinci sistem tarafından kullanılan biçime veri dönüştürmek gerekiyor. Şu anda verilen tek sözleşme için C # istemci tarafında bu çözüyorum, ancak toplu işlem ve sunucular arasında ihracat / ithalat için sunucu tarafında T-SQL yapmak istiyorum. Büyük olasılıkla, CLR UDF kullanılarak yapılabilir, ancak bu aşamada SQLCLR'yi kullanamıyorum.

Buradaki zorluk, aralıkların listesini mümkün olduğunca kısa ve insan dostu hale getirmektir.

Örneğin, bu program:

+-----+------------+------------+---------+--------+
| ID  | ContractID |     dt     | dowChar | dowInt |
+-----+------------+------------+---------+--------+
| 223 |          2 | 2016-05-05 | Thu     |      5 |
| 224 |          2 | 2016-05-06 | Fri     |      6 |
| 225 |          2 | 2016-05-09 | Mon     |      2 |
| 226 |          2 | 2016-05-10 | Tue     |      3 |
| 227 |          2 | 2016-05-11 | Wed     |      4 |
| 228 |          2 | 2016-05-12 | Thu     |      5 |
| 229 |          2 | 2016-05-13 | Fri     |      6 |
| 230 |          2 | 2016-05-16 | Mon     |      2 |
| 231 |          2 | 2016-05-17 | Tue     |      3 |
+-----+------------+------------+---------+--------+

bu olmalı:

+------------+------------+------------+----------+----------------------+
| ContractID |  StartDT   |   EndDT    | DayCount |       WeekDays       |
+------------+------------+------------+----------+----------------------+
|          2 | 2016-05-05 | 2016-05-17 |        9 | Mon,Tue,Wed,Thu,Fri, |
+------------+------------+------------+----------+----------------------+

,bu değil:

+------------+------------+------------+----------+----------------------+
| ContractID |  StartDT   |   EndDT    | DayCount |       WeekDays       |
+------------+------------+------------+----------+----------------------+
|          2 | 2016-05-05 | 2016-05-06 |        2 | Thu,Fri,             |
|          2 | 2016-05-09 | 2016-05-13 |        5 | Mon,Tue,Wed,Thu,Fri, |
|          2 | 2016-05-16 | 2016-05-17 |        2 | Mon,Tue,             |
+------------+------------+------------+----------+----------------------+

gaps-and-islandsBu soruna bir yaklaşım uygulamaya çalıştım . Bunu iki geçişte yapmaya çalıştım. İlk geçişte basit ardışık gün adaları buluyorum, yani adanın sonu, hafta sonu, resmi tatil veya başka bir şey olsun, gün dizisindeki herhangi bir boşluk. Bu tür bulunan her ada için virgülle ayrılmış ayrı bir liste hazırlarım WeekDays. İkinci geçişte I grubu, hafta sayıları sıralamasındaki boşluğa veya WeekDays.

Bu yaklaşımla, her kısmi hafta yukarıda gösterildiği gibi ekstra bir aralık olarak sona erer, çünkü hafta sayıları ardışık olsa da, WeekDaysdeğişiklik. Ayrıca, bir hafta içinde düzenli boşluklar olabilir ( ContractID=3sadece verileri olan örnek verilere bakın Mon,Wed,Fri,) ve bu yaklaşım, bu programda her gün için ayrı aralıklar oluşturacaktır. Parlak tarafta, programın hiç boşluğu yoksa ( ContractID=7hafta sonlarını içeren örnek verilerde bakın ) bir aralık oluşturur ve bu durumda başlangıç ​​veya bitiş haftasının kısmi olması önemli değildir.

Neyin peşinde olduğum hakkında daha iyi bir fikir edinmek için lütfen aşağıdaki koddaki diğer örneklere bakın. Hafta sonlarının genellikle hariç tutulduğunu, ancak haftanın diğer günlerinin de hariç tutulabileceğini görebilirsiniz. Örnek 3 Sadece olarak Mon, Wedve Frizamanlama bir parçasıdır. Ayrıca, hafta sonları, örnek 7'deki gibi dahil edilebilir. Çözüm, haftanın tüm günlerine eşit muamele etmelidir. Haftanın herhangi bir günü programa dahil edilebilir veya programın dışında bırakılabilir.

Oluşturulan aralık listesinin verilen zamanlamayı doğru bir şekilde açıkladığını doğrulamak için aşağıdaki sahte kodu kullanabilirsiniz:

  • tüm aralıklarla döngü
  • Başlangıç ​​ve Bitiş tarihleri ​​(dahil) arasındaki tüm takvim tarihleri ​​arasında her aralık döngüsü için.
  • her tarih için haftanın gününün WeekDays. Evet ise, bu tarih programa dahil edilir.

Umarım, bu hangi durumlarda yeni bir aralık yaratılması gerektiğini netleştirir. 4. ve 5. örneklerde, bir Pazartesi ( 2016-05-09) zamanlamanın ortasından kaldırılır ve bu zamanlama tek bir aralıkla temsil edilemez. Örnek 6'da çizelgede uzun bir boşluk vardır, bu nedenle iki aralık gereklidir.

Aralıklar, programdaki haftalık kalıpları temsil eder ve bir kalıp bozulduğunda / değiştirildiğinde yeni aralık eklenmelidir. Örnek 11'de ilk üç hafta bir örüntüye sahiptir Tue, daha sonra bu örüntü olarak değişir Thu. Sonuç olarak, bu programı tanımlamak için iki aralığa ihtiyacımız var.


Şu anda SQL Server 2008 kullanıyorum, bu yüzden çözüm bu sürümde çalışmalıdır. SQL Server 2008 için bir çözüm daha sonraki sürümlerden özellikler kullanılarak basitleştirilebilir / iyileştirilebilirse, bu bir bonus, lütfen bunu da gösterin.

Bir Calendartablo (tarih listesi) ve Numberstablo (1'den başlayan tam sayıların listesi) var, bu yüzden gerekirse bunları kullanmak için sorun yok. Geçici tablolar oluşturmak ve verileri birkaç aşamada işleyen birkaç sorguya sahip olmak da iyidir. Algoritmadaki aşama sayısı sabitlenmelidir, imleçler ve açık WHILEdöngüler doğru değildir.


Örnek veriler ve beklenen sonuçlar için komut dosyası

-- @Src is sample data
-- @Dst is expected result

DECLARE @Src TABLE (ID int PRIMARY KEY, ContractID int, dt date, dowChar char(3), dowInt int);
INSERT INTO @Src (ID, ContractID, dt, dowChar, dowInt) VALUES

-- simple two weeks (without weekend)
(110, 1, '2016-05-02', 'Mon', 2),
(111, 1, '2016-05-03', 'Tue', 3),
(112, 1, '2016-05-04', 'Wed', 4),
(113, 1, '2016-05-05', 'Thu', 5),
(114, 1, '2016-05-06', 'Fri', 6),
(115, 1, '2016-05-09', 'Mon', 2),
(116, 1, '2016-05-10', 'Tue', 3),
(117, 1, '2016-05-11', 'Wed', 4),
(118, 1, '2016-05-12', 'Thu', 5),
(119, 1, '2016-05-13', 'Fri', 6),

-- a partial end of the week, the whole week, partial start of the week (without weekends)
(223, 2, '2016-05-05', 'Thu', 5),
(224, 2, '2016-05-06', 'Fri', 6),
(225, 2, '2016-05-09', 'Mon', 2),
(226, 2, '2016-05-10', 'Tue', 3),
(227, 2, '2016-05-11', 'Wed', 4),
(228, 2, '2016-05-12', 'Thu', 5),
(229, 2, '2016-05-13', 'Fri', 6),
(230, 2, '2016-05-16', 'Mon', 2),
(231, 2, '2016-05-17', 'Tue', 3),

-- only Mon, Wed, Fri are included across two weeks plus partial third week
(310, 3, '2016-05-02', 'Mon', 2),
(311, 3, '2016-05-04', 'Wed', 4),
(314, 3, '2016-05-06', 'Fri', 6),
(315, 3, '2016-05-09', 'Mon', 2),
(317, 3, '2016-05-11', 'Wed', 4),
(319, 3, '2016-05-13', 'Fri', 6),
(330, 3, '2016-05-16', 'Mon', 2),

-- a whole week (without weekend), in the second week Mon is not included
(410, 4, '2016-05-02', 'Mon', 2),
(411, 4, '2016-05-03', 'Tue', 3),
(412, 4, '2016-05-04', 'Wed', 4),
(413, 4, '2016-05-05', 'Thu', 5),
(414, 4, '2016-05-06', 'Fri', 6),
(416, 4, '2016-05-10', 'Tue', 3),
(417, 4, '2016-05-11', 'Wed', 4),
(418, 4, '2016-05-12', 'Thu', 5),
(419, 4, '2016-05-13', 'Fri', 6),

-- three weeks, but without Mon in the second week (no weekends)
(510, 5, '2016-05-02', 'Mon', 2),
(511, 5, '2016-05-03', 'Tue', 3),
(512, 5, '2016-05-04', 'Wed', 4),
(513, 5, '2016-05-05', 'Thu', 5),
(514, 5, '2016-05-06', 'Fri', 6),
(516, 5, '2016-05-10', 'Tue', 3),
(517, 5, '2016-05-11', 'Wed', 4),
(518, 5, '2016-05-12', 'Thu', 5),
(519, 5, '2016-05-13', 'Fri', 6),
(520, 5, '2016-05-16', 'Mon', 2),
(521, 5, '2016-05-17', 'Tue', 3),
(522, 5, '2016-05-18', 'Wed', 4),
(523, 5, '2016-05-19', 'Thu', 5),
(524, 5, '2016-05-20', 'Fri', 6),

-- long gap between two intervals
(623, 6, '2016-05-05', 'Thu', 5),
(624, 6, '2016-05-06', 'Fri', 6),
(625, 6, '2016-05-09', 'Mon', 2),
(626, 6, '2016-05-10', 'Tue', 3),
(627, 6, '2016-05-11', 'Wed', 4),
(628, 6, '2016-05-12', 'Thu', 5),
(629, 6, '2016-05-13', 'Fri', 6),
(630, 6, '2016-05-16', 'Mon', 2),
(631, 6, '2016-05-17', 'Tue', 3),
(645, 6, '2016-06-06', 'Mon', 2),
(646, 6, '2016-06-07', 'Tue', 3),
(647, 6, '2016-06-08', 'Wed', 4),
(648, 6, '2016-06-09', 'Thu', 5),
(649, 6, '2016-06-10', 'Fri', 6),
(655, 6, '2016-06-13', 'Mon', 2),
(656, 6, '2016-06-14', 'Tue', 3),
(657, 6, '2016-06-15', 'Wed', 4),
(658, 6, '2016-06-16', 'Thu', 5),
(659, 6, '2016-06-17', 'Fri', 6),

-- two weeks, no gaps between days at all, even weekends are included
(710, 7, '2016-05-02', 'Mon', 2),
(711, 7, '2016-05-03', 'Tue', 3),
(712, 7, '2016-05-04', 'Wed', 4),
(713, 7, '2016-05-05', 'Thu', 5),
(714, 7, '2016-05-06', 'Fri', 6),
(715, 7, '2016-05-07', 'Sat', 7),
(716, 7, '2016-05-08', 'Sun', 1),
(725, 7, '2016-05-09', 'Mon', 2),
(726, 7, '2016-05-10', 'Tue', 3),
(727, 7, '2016-05-11', 'Wed', 4),
(728, 7, '2016-05-12', 'Thu', 5),
(729, 7, '2016-05-13', 'Fri', 6),

-- no gaps between days at all, even weekends are included, with partial weeks
(805, 8, '2016-04-30', 'Sat', 7),
(806, 8, '2016-05-01', 'Sun', 1),
(810, 8, '2016-05-02', 'Mon', 2),
(811, 8, '2016-05-03', 'Tue', 3),
(812, 8, '2016-05-04', 'Wed', 4),
(813, 8, '2016-05-05', 'Thu', 5),
(814, 8, '2016-05-06', 'Fri', 6),
(815, 8, '2016-05-07', 'Sat', 7),
(816, 8, '2016-05-08', 'Sun', 1),
(825, 8, '2016-05-09', 'Mon', 2),
(826, 8, '2016-05-10', 'Tue', 3),
(827, 8, '2016-05-11', 'Wed', 4),
(828, 8, '2016-05-12', 'Thu', 5),
(829, 8, '2016-05-13', 'Fri', 6),
(830, 8, '2016-05-14', 'Sat', 7),

-- only Mon-Wed included, two weeks plus partial third week
(910, 9, '2016-05-02', 'Mon', 2),
(911, 9, '2016-05-03', 'Tue', 3),
(912, 9, '2016-05-04', 'Wed', 4),
(915, 9, '2016-05-09', 'Mon', 2),
(916, 9, '2016-05-10', 'Tue', 3),
(917, 9, '2016-05-11', 'Wed', 4),
(930, 9, '2016-05-16', 'Mon', 2),
(931, 9, '2016-05-17', 'Tue', 3),

-- only Thu-Sun included, three weeks
(1013,10,'2016-05-05', 'Thu', 5),
(1014,10,'2016-05-06', 'Fri', 6),
(1015,10,'2016-05-07', 'Sat', 7),
(1016,10,'2016-05-08', 'Sun', 1),
(1018,10,'2016-05-12', 'Thu', 5),
(1019,10,'2016-05-13', 'Fri', 6),
(1020,10,'2016-05-14', 'Sat', 7),
(1021,10,'2016-05-15', 'Sun', 1),
(1023,10,'2016-05-19', 'Thu', 5),
(1024,10,'2016-05-20', 'Fri', 6),
(1025,10,'2016-05-21', 'Sat', 7),
(1026,10,'2016-05-22', 'Sun', 1),

-- only Tue for first three weeks, then only Thu for the next three weeks
(1111,11,'2016-05-03', 'Tue', 3),
(1116,11,'2016-05-10', 'Tue', 3),
(1131,11,'2016-05-17', 'Tue', 3),
(1123,11,'2016-05-19', 'Thu', 5),
(1124,11,'2016-05-26', 'Thu', 5),
(1125,11,'2016-06-02', 'Thu', 5),

-- one week, then one week gap, then one week
(1210,12,'2016-05-02', 'Mon', 2),
(1211,12,'2016-05-03', 'Tue', 3),
(1212,12,'2016-05-04', 'Wed', 4),
(1213,12,'2016-05-05', 'Thu', 5),
(1214,12,'2016-05-06', 'Fri', 6),
(1215,12,'2016-05-16', 'Mon', 2),
(1216,12,'2016-05-17', 'Tue', 3),
(1217,12,'2016-05-18', 'Wed', 4),
(1218,12,'2016-05-19', 'Thu', 5),
(1219,12,'2016-05-20', 'Fri', 6);

SELECT ID, ContractID, dt, dowChar, dowInt
FROM @Src
ORDER BY ContractID, dt;


DECLARE @Dst TABLE (ContractID int, StartDT date, EndDT date, DayCount int, WeekDays varchar(255));
INSERT INTO @Dst (ContractID, StartDT, EndDT, DayCount, WeekDays) VALUES
(1, '2016-05-02', '2016-05-13', 10, 'Mon,Tue,Wed,Thu,Fri,'),
(2, '2016-05-05', '2016-05-17',  9, 'Mon,Tue,Wed,Thu,Fri,'),
(3, '2016-05-02', '2016-05-16',  7, 'Mon,Wed,Fri,'),
(4, '2016-05-02', '2016-05-06',  5, 'Mon,Tue,Wed,Thu,Fri,'),
(4, '2016-05-10', '2016-05-13',  4, 'Tue,Wed,Thu,Fri,'),
(5, '2016-05-02', '2016-05-06',  5, 'Mon,Tue,Wed,Thu,Fri,'),
(5, '2016-05-10', '2016-05-20',  9, 'Mon,Tue,Wed,Thu,Fri,'),
(6, '2016-05-05', '2016-05-17',  9, 'Mon,Tue,Wed,Thu,Fri,'),
(6, '2016-06-06', '2016-06-17', 10, 'Mon,Tue,Wed,Thu,Fri,'),
(7, '2016-05-02', '2016-05-13', 12, 'Sun,Mon,Tue,Wed,Thu,Fri,Sat,'),
(8, '2016-04-30', '2016-05-14', 15, 'Sun,Mon,Tue,Wed,Thu,Fri,Sat,'),
(9, '2016-05-02', '2016-05-17',  8, 'Mon,Tue,Wed,'),
(10,'2016-05-05', '2016-05-22', 12, 'Sun,Thu,Fri,Sat,'),
(11,'2016-05-03', '2016-05-17',  3, 'Tue,'),
(11,'2016-05-19', '2016-06-02',  3, 'Thu,'),
(12,'2016-05-02', '2016-05-06',  5, 'Mon,Tue,Wed,Thu,Fri,'),
(12,'2016-05-16', '2016-05-20',  5, 'Mon,Tue,Wed,Thu,Fri,');

SELECT ContractID, StartDT, EndDT, DayCount, WeekDays
FROM @Dst
ORDER BY ContractID, StartDT;

Cevapların karşılaştırılması

Tabloyu @Srcsahip 403,555olan satırlar 15,857tat ContractIDs. Tüm cevaplar doğru sonuç verir (en azından verilerim için) ve hepsi oldukça hızlıdır, ancak optimallik bakımından farklılık gösterir. Ne kadar az aralık üretilirse o kadar iyidir. Ben sadece merak için çalışma süreleri dahil. Ana odak doğru değil, doğru sonuç, hız değil (çok uzun sürmediği sürece - Ziggy Crueltyfree Zeitgeister tarafından özyinelemeyen sorguyu 10 dakika sonra durdurdum).

+--------------------------------------------------------+-----------+---------+
|                         Answer                         | Intervals | Seconds |
+--------------------------------------------------------+-----------+---------+
| Ziggy Crueltyfree Zeitgeister                          |     25751 |    7.88 |
| While loop                                             |           |         |
|                                                        |           |         |
| Ziggy Crueltyfree Zeitgeister                          |     25751 |    8.27 |
| Recursive                                              |           |         |
|                                                        |           |         |
| Michael Green                                          |     25751 |   22.63 |
| Recursive                                              |           |         |
|                                                        |           |         |
| Geoff Patterson                                        |     26670 |    4.79 |
| Weekly gaps-and-islands with merging of partial weeks  |           |         |
|                                                        |           |         |
| Vladimir Baranov                                       |     34560 |    4.03 |
| Daily, then weekly gaps-and-islands                    |           |         |
|                                                        |           |         |
| Mikael Eriksson                                        |     35840 |    0.65 |
| Weekly gaps-and-islands                                |           |         |
+--------------------------------------------------------+-----------+---------+
| Vladimir Baranov                                       |     25751 |  121.51 |
| Cursor                                                 |           |         |
+--------------------------------------------------------+-----------+---------+

(11,'2016-05-03', '2016-05-17', 3, 'Tue,'), (11,'2016-05-19', '2016-06-02', 3, 'Thu,');@Dst ile bir satır olmamalı mı Tue, Thu,?
Kin Shah

@Kin, Örnek 11 (en az) iki aralığa (iki satır giriş @Dst) sahip olmalıdır. Programın ilk iki haftasında sadece bu haftalar Tueolamaz WeekDays=Tue,Thu,. Programın son iki haftası sadece Thubu yüzden WeekDays=Tue,Thu,bu haftalar için tekrar sahip olamazsınız . Bunun için en uygun olmayan çözüm üç sıra olacaktır: sadece Tueilk iki hafta için, daha sonra Tue,Thu,her ikisine de sahip olan üçüncü hafta Tueve Thudaha sonra sadece Thuson iki hafta için.
Vladimir Baranov

1
Sözleşmenin 11 en iyi şekilde iki aralığa ayrıldığı algoritmayı açıklar mısınız? Bunu C # uygulamasında gerçekleştirdiniz mi? Nasıl?
Michael Green

@MichaelGreen, üzgünüm daha önce cevap veremedim. Evet, C # kodu Sözleşme 11'i iki aralığa böler. Kaba algoritma: Zamanlanmış tarihler arasında birer birer dolaşıyorum, aralığın başlangıcından bu yana bugüne kadar karşılaştığım haftanın hangi günlerini not et ve yeni bir aralık başlatmam gerekip gerekmediğini belirle: bir ContractIDdeğişiklik olursa, aralıklıysa planlanan günler listesinde bir boşluk varsa, 7 günden fazladır ve yeni hafta günü daha önce görülmemiştir.
Vladimir Baranov

@MichaelGreen, C # kodumu sadece gerçek verilerdeki diğer çözümlerle nasıl karşılaştırdığını görmek için imleç tabanlı bir algoritmaya dönüştürdüm. Cevap için kaynak kodunu ve sonuçları sorudaki özet tabloya ekledim.
Vladimir Baranov

Yanıtlar:


6

Bu özyinelemeli bir CTE kullanır. Sonuç, sorudaki örnekle aynıdır . Bu bir kabus gibiydi ... Kod, kıvrık mantığıyla kolaylaştırmak için yorumlar içeriyor.

SET DATEFIRST 1 -- Make Monday weekday=1

DECLARE @Ranked TABLE (RowID int NOT NULL IDENTITY PRIMARY KEY,                   -- Incremental uninterrupted sequence in the right order
                       ID int NOT NULL UNIQUE, ContractID int NOT NULL, dt date,  -- Original relevant values (ID is not really necessary)
                       WeekNo int NOT NULL, dowBit int NOT NULL);                 -- Useful to find gaps in days or weeks
INSERT INTO @Ranked
SELECT ID, ContractID, dt,
       DATEDIFF(WEEK, '1900-01-01', DATEADD(DAY, 1-DATEPART(dw, dt), dt)) AS WeekNo,
       POWER(2, DATEPART(dw, dt)-1) AS dowBit
FROM @Src
ORDER BY ContractID, WeekNo, dowBit

/*
Each evaluated date makes part of the carried sequence if:
  - this is not a new contract, and
    - sequence started this week, or
    - same day last week was part of the sequence, or
    - sequence started last week and today is a lower day than the accumulated weekdays list
  - and there are no sequence gaps since previous day
(otherwise it does not make part of the old sequence, so it starts a new one) */

DECLARE @RankedRanges TABLE (RowID int NOT NULL PRIMARY KEY, WeekDays int NOT NULL, StartRowID int NULL);

WITH WeeksCTE AS -- Needed for building the sequence gradually, and comparing the carried sequence (and previous day) with a current evaluated day
( 
    SELECT RowID, ContractID, dowBit, WeekNo, RowID AS StartRowID, WeekNo AS StartWN, dowBit AS WeekDays, dowBit AS StartWeekDays
    FROM @Ranked
    WHERE RowID = 1 
    UNION ALL
    SELECT RowID, ContractID, dowBit, WeekNo, StartRowID,
           CASE WHEN StartRowID IS NULL THEN StartWN ELSE WeekNo END AS WeekNo,
           CASE WHEN StartRowID IS NULL THEN WeekDays | dowBit ELSE dowBit END AS WeekDays,
           CASE WHEN StartRowID IS NOT NULL THEN dowBit WHEN WeekNo = StartWN THEN StartWeekDays | dowBit ELSE StartWeekDays END AS StartWeekDays
    FROM (
        SELECT w.*, pre.StartWN, pre.WeekDays, pre.StartWeekDays,
               CASE WHEN w.ContractID <> pre.ContractID OR     -- New contract always break the sequence
                         NOT (w.WeekNo = pre.StartWN OR        -- Same week as a new sequence always keeps the sequence
                              w.dowBit & pre.WeekDays > 0 OR   -- Days in the sequence keep the sequence (provided there are no gaps, checked later)
                              (w.WeekNo = pre.StartWN+1 AND (w.dowBit-1) & pre.StartWeekDays = 0)) OR -- Days in the second week when less than a week passed since the sequence started remain in sequence
                         (w.WeekNo > pre.StartWN AND -- look for gap after initial week
                          w.WeekNo > pre.WeekNo+1 OR -- look for full-week gaps
                          (w.WeekNo = pre.WeekNo AND                            -- when same week as previous day,
                           ((w.dowBit-1) ^ (pre.dowBit*2-1)) & pre.WeekDays > 0 -- days between this and previous weekdays, compared to current series
                          ) OR
                          (w.WeekNo > pre.WeekNo AND                                   -- when following week of previous day,
                           ((-1 ^ (pre.dowBit*2-1)) | (w.dowBit-1)) & pre.WeekDays > 0 -- days between this and previous weekdays, compared to current series
                          )) THEN w.RowID END AS StartRowID
        FROM WeeksCTE pre
        JOIN @Ranked w ON (w.RowID = pre.RowID + 1)
        ) w
) 
INSERT INTO @RankedRanges -- days sequence and starting point of each sequence
SELECT RowID, WeekDays, StartRowID
--SELECT *
FROM WeeksCTE
OPTION (MAXRECURSION 0)

--SELECT * FROM @RankedRanges

DECLARE @Ranges TABLE (RowNo int NOT NULL IDENTITY PRIMARY KEY, RowID int NOT NULL);

INSERT INTO @Ranges       -- @RankedRanges filtered only by start of each range, with numbered rows to easily find the end of each range
SELECT StartRowID
FROM @RankedRanges
WHERE StartRowID IS NOT NULL
ORDER BY 1

-- Final result putting everything together
SELECT rs.ContractID, rs.dt AS StartDT, re.dt AS EndDT, re.RowID-rs.RowID+1 AS DayCount,
       CASE WHEN rr.WeekDays & 64 > 0 THEN 'Sun,' ELSE '' END +
       CASE WHEN rr.WeekDays & 1 > 0 THEN 'Mon,' ELSE '' END +
       CASE WHEN rr.WeekDays & 2 > 0 THEN 'Tue,' ELSE '' END +
       CASE WHEN rr.WeekDays & 4 > 0 THEN 'Wed,' ELSE '' END +
       CASE WHEN rr.WeekDays & 8 > 0 THEN 'Thu,' ELSE '' END +
       CASE WHEN rr.WeekDays & 16 > 0 THEN 'Fri,' ELSE '' END +
       CASE WHEN rr.WeekDays & 32 > 0 THEN 'Sat,' ELSE '' END AS WeekDays
FROM (
    SELECT r.RowID AS StartRowID, COALESCE(pos.RowID-1, (SELECT MAX(RowID) FROM @Ranked)) AS EndRowID
    FROM @Ranges r
    LEFT JOIN @Ranges pos ON (pos.RowNo = r.RowNo + 1)
    ) g
JOIN @Ranked rs ON (rs.RowID = g.StartRowID)
JOIN @Ranked re ON (re.RowID = g.EndRowID)
JOIN @RankedRanges rr ON (rr.RowID = re.RowID)


Başka bir strateji

SQL Server 2008'deki yavaş sınırlı özyinelemeli CTE'ye dayanmadığından, bir öncekinden önemli ölçüde daha hızlı olmalıdır, ancak aynı stratejiyi az ya da çok uygular.

Bir WHILEdöngü var (bundan kaçınmak için bir yol tasarlayamadım), ancak daha az sayıda yineleme (herhangi bir sözleşmede en fazla dizi (eksi bir)) için gidiyor.

Bu basit bir stratejidir ve bir haftadan daha kısa veya daha uzun sekanslar için kullanılabilir (başka bir sayı için sabit 7'nin herhangi bir oluşumunun yerini alır ve bunun yerine dowBitMODULUS x'ten hesaplanır ) ve 32'ye kadar.DayNoDATEPART(wk)

SET DATEFIRST 1 -- Make Monday weekday=1

-- Get the minimum information needed to calculate sequences
DECLARE @Days TABLE (ContractID int NOT NULL, dt date, DayNo int NOT NULL, dowBit int NOT NULL, PRIMARY KEY (ContractID, DayNo));
INSERT INTO @Days
SELECT ContractID, dt, CAST(CAST(dt AS datetime) AS int) AS DayNo, POWER(2, DATEPART(dw, dt)-1) AS dowBit
FROM @Src

DECLARE @RangeStartFirstPass TABLE (ContractID int NOT NULL, DayNo int NOT NULL, PRIMARY KEY (ContractID, DayNo))

-- Calculate, from the above list, which days are not present in the previous 7
INSERT INTO @RangeStartFirstPass
SELECT r.ContractID, r.DayNo
FROM @Days r
LEFT JOIN @Days pr ON (pr.ContractID = r.ContractID AND pr.DayNo BETWEEN r.DayNo-7 AND r.DayNo-1) -- Last 7 days
GROUP BY r.ContractID, r.DayNo, r.dowBit
HAVING r.dowBit & COALESCE(SUM(pr.dowBit), 0) = 0

-- Update the previous list with all days that occur right after a missing day
INSERT INTO @RangeStartFirstPass
SELECT *
FROM (
    SELECT DISTINCT ContractID, (SELECT MIN(DayNo) FROM @Days WHERE ContractID = d.ContractID AND DayNo > d.DayNo + 7) AS DayNo
    FROM @Days d
    WHERE NOT EXISTS (SELECT 1 FROM @Days WHERE ContractID = d.ContractID AND DayNo = d.DayNo + 7)
    ) d
WHERE DayNo IS NOT NULL AND
      NOT EXISTS (SELECT 1 FROM @RangeStartFirstPass WHERE ContractID = d.ContractID AND DayNo = d.DayNo)

DECLARE @RangeStart TABLE (ContractID int NOT NULL, DayNo int NOT NULL, PRIMARY KEY (ContractID, DayNo));

-- Fetch the first sequence for each contract
INSERT INTO @RangeStart
SELECT ContractID, MIN(DayNo)
FROM @RangeStartFirstPass
GROUP BY ContractID

-- Add to the list above the next sequence for each contract, until all are added
-- (ensure no sequence is added with less than 7 days)
WHILE @@ROWCOUNT > 0
  INSERT INTO @RangeStart
  SELECT f.ContractID, MIN(f.DayNo)
  FROM (SELECT ContractID, MAX(DayNo) AS DayNo FROM @RangeStart GROUP BY ContractID) s
  JOIN @RangeStartFirstPass f ON (f.ContractID = s.ContractID AND f.DayNo > s.DayNo + 7)
  GROUP BY f.ContractID

-- Summarise results
SELECT ContractID, StartDT, EndDT, DayCount,
       CASE WHEN WeekDays & 64 > 0 THEN 'Sun,' ELSE '' END +
       CASE WHEN WeekDays & 1 > 0 THEN 'Mon,' ELSE '' END +
       CASE WHEN WeekDays & 2 > 0 THEN 'Tue,' ELSE '' END +
       CASE WHEN WeekDays & 4 > 0 THEN 'Wed,' ELSE '' END +
       CASE WHEN WeekDays & 8 > 0 THEN 'Thu,' ELSE '' END +
       CASE WHEN WeekDays & 16 > 0 THEN 'Fri,' ELSE '' END +
       CASE WHEN WeekDays & 32 > 0 THEN 'Sat,' ELSE '' END AS WeekDays
FROM (
    SELECT r.ContractID,
           MIN(d.dt) AS StartDT,
           MAX(d.dt) AS EndDT,
           COUNT(*) AS DayCount,
           SUM(DISTINCT d.dowBit) AS WeekDays
    FROM (SELECT *, COALESCE((SELECT MIN(DayNo) FROM @RangeStart WHERE ContractID = rs.ContractID AND DayNo > rs.DayNo), 999999) AS DayEnd FROM @RangeStart rs) r
    JOIN @Days d ON (d.ContractID = r.ContractID AND d.DayNo BETWEEN r.DayNo AND r.DayEnd-1)
    GROUP BY r.ContractID, r.DayNo
    ) d
ORDER BY ContractID, StartDT

@VladimirBaranov Çok daha hızlı olması gereken yeni bir strateji ekledim. Gerçek verilerinizle nasıl derecelendirildiğini bana bildirin!
Ziggy Crueltyfree Zeitgeister

2
@ZiggyCrueltyfreeZeitgeister, son çözümünüzü kontrol ettim ve sorudaki tüm cevapların listesine ekledim. Doğru sonuçlar ve tekrarlayan CTE ile aynı sayıda aralık üretir ve hızı da çok yakındır. Dediğim gibi, hız makul olduğu sürece kritik değildir. 1 saniye ya da 10 saniye benim için hiç önemli değil.
Vladimir Baranov

Diğer cevaplar da harika ve kullanışlıdır ve keşke ödülü birden fazla cevaba verebilsem. Bu cevabı seçtim, çünkü cömertliğe başladığımda, özyinelemeli CTE hakkında düşünmedim ve bu cevap ilk öneren ve çalışan bir çözüme sahipti. Kesin olarak, özyinelemeli CTE set tabanlı bir çözüm değildir, ancak en iyi sonuçları verir ve oldukça hızlıdır. Bir cevap @GeoffPatterson tarafından büyük, ama açıkçası yol çok karmaşık, konuşma az optimum sonuçlar ve verir.
Vladimir Baranov

5

Tam olarak aradığınız şey değil, belki de ilginizi çekebilir.

Sorgu, her hafta kullanılan günler için virgülle ayrılmış dizeyle haftalar oluşturur. Daha sonra aynı deseni kullanan ardışık hafta adalarını bulur Weekdays.

with Weeks as
(
  select T.*,
         row_number() over(partition by T.ContractID, T.WeekDays order by T.WeekNumber) as rn
  from (
       select S1.ContractID,
              min(S1.dt) as StartDT,
              max(S1.dt) as EndDT,
              datediff(day, 0, S1.dt) / 7 as WeekNumber, -- Number of weeks since '1900-01-01 (a monday)'
              count(*) as DayCount,
              stuff((
                    select ','+S2.dowChar
                    from @Src as S2
                    where S2.ContractID = S1.ContractID and
                          S2.dt between min(S1.dt) and max(S1.dt)
                    order by S2.dt
                    for xml path('')
                    ), 1, 1, '') as WeekDays
       from @Src as S1
       group by S1.ContractID, 
                datediff(day, 0, S1.dt) / 7
       ) as T
)
select W.ContractID,
       min(W.StartDT) as StartDT,
       max(W.EndDT) as EndDT,
       count(*) * W.DayCount as DayCount,
       W.WeekDays
from Weeks as W
group by W.ContractID,
         W.WeekDays,
         W.DayCount,
         W.rn - W.WeekNumber
order by W.ContractID,
         min(W.WeekNumber);

Sonuç:

ContractID  StartDT    EndDT      DayCount    WeekDays
----------- ---------- ---------- ----------- -----------------------------
1           2016-05-02 2016-05-13 10          Mon,Tue,Wed,Thu,Fri
2           2016-05-05 2016-05-06 2           Thu,Fri
2           2016-05-09 2016-05-13 5           Mon,Tue,Wed,Thu,Fri
2           2016-05-16 2016-05-17 2           Mon,Tue
3           2016-05-02 2016-05-13 6           Mon,Wed,Fri
3           2016-05-16 2016-05-16 1           Mon
4           2016-05-02 2016-05-06 5           Mon,Tue,Wed,Thu,Fri
4           2016-05-10 2016-05-13 4           Tue,Wed,Thu,Fri
5           2016-05-02 2016-05-06 5           Mon,Tue,Wed,Thu,Fri
5           2016-05-10 2016-05-13 4           Tue,Wed,Thu,Fri
5           2016-05-16 2016-05-20 5           Mon,Tue,Wed,Thu,Fri
6           2016-05-05 2016-05-06 2           Thu,Fri
6           2016-05-09 2016-05-13 5           Mon,Tue,Wed,Thu,Fri
6           2016-05-16 2016-05-17 2           Mon,Tue
6           2016-06-06 2016-06-17 10          Mon,Tue,Wed,Thu,Fri
7           2016-05-02 2016-05-08 7           Mon,Tue,Wed,Thu,Fri,Sat,Sun
7           2016-05-09 2016-05-13 5           Mon,Tue,Wed,Thu,Fri
8           2016-04-30 2016-05-01 2           Sat,Sun
8           2016-05-02 2016-05-08 7           Mon,Tue,Wed,Thu,Fri,Sat,Sun
8           2016-05-09 2016-05-14 6           Mon,Tue,Wed,Thu,Fri,Sat
9           2016-05-02 2016-05-11 6           Mon,Tue,Wed
9           2016-05-16 2016-05-17 2           Mon,Tue
10          2016-05-05 2016-05-22 12          Thu,Fri,Sat,Sun
11          2016-05-03 2016-05-10 2           Tue
11          2016-05-17 2016-05-19 2           Tue,Thu
11          2016-05-26 2016-06-02 2           Thu

ContractID = 2sonuçtaki farkın ne istediğinizle karşılaştırıldığını gösterir. İlk ve son hafta WeekDaysfarklı olduğu için ayrı dönemler olarak ele alınacaktır .


Bu fikrim vardı, ama deneme şansım olmadı. Çalışan bir sorgu sağladığınız için teşekkür ederiz. Daha yapılandırılmış bir sonuç vermesini seviyorum. Verileri haftalar halinde gruplarken, aşağı taraf esnekliği azaltır (basit bir günlük boşluklar ve adalar yaklaşımı örnek 7 ve 8 bir aralığa daraltılacaktır), ancak aynı zamanda parlak taraftır - karmaşıklığını azaltırız sorun. Bu nedenle, bu yaklaşımla ilgili en büyük sorun, programın başlangıcında ve sonunda kısmi haftalardır. Böyle kısmi haftalar ekstra bir aralık oluşturur ...
Vladimir Baranov

Bu kısmi haftaları ana programa eklemenin / gruplamanın / birleştirmenin bir yolunu düşünebilir misiniz? Bu aşamada sadece çok belirsiz bir fikrim var. Kısmi haftaları doğru bir şekilde birleştirmenin bir yolunu bulursak, sonuç optimum seviyeye çok yakın olacaktır.
Vladimir Baranov

@VladimirBaranov Bunun nasıl yapılacağından emin değilim. Akla bir şey gelirse cevabı güncelleyeceğim.
Mikael Eriksson

Belirsiz fikrim şudur: Bir haftada sadece 7 gün vardır, bu yüzden WeekDays7 bitlik bir sayıdır. Sadece 128 kombinasyon. Sadece 128 * 128 = 16384 olası çift vardır. Mümkün olan tüm çiftlerle bir geçici tablo oluşturun, ardından hangi çiftlerin birleştirilebileceğini işaretleyen set tabanlı bir algoritma bulun: bir haftalık bir desen, gelecek haftanın bir deseni ile "kapsanır". Mevcut haftalık sonuca kendiniz katılın ( LAG2008'de olmadığı için) ve hangi çiftlerin birleştirileceğini belirlemek için bu geçici tabloyu kullanın ... Bu fikrin bir değeri olup olmadığından emin değilim.
Vladimir Baranov

5

Bu durumda en uygun çözümü veren bir yaklaşımla sonuçlandım ve genel olarak iyi olacağını düşünüyorum. Ancak çözüm oldukça uzundur, bu nedenle başka birinin daha özlü olan farklı bir yaklaşımı olup olmadığını görmek ilginç olacaktır.

İşte tam çözümü içeren bir komut dosyası .

İşte algoritmanın ana hatları:

  • Veri kümesini, her haftayı temsil eden tek bir satır olacak şekilde döndürün
  • Her birindeki hafta adalarını hesaplayın ContractId
  • Aynı içinde kalan herhangi bir komşu hafta birleştirme ContractIdve aynı sahipWeekDays
  • Önceki gruplandırmanın aynı adada olduğu ve WeekDaystek haftanın bir WeekDaysönceki gruplandırmanın önde gelen alt kümesiyle eşleştiği herhangi bir hafta (henüz birleştirilmemiş) için , önceki gruplandırmayla birleştirin
  • Bir sonraki gruplamanın aynı adada olduğu ve WeekDaystek bir haftanın bir WeekDayssonraki gruplamanın son alt kümesiyle eşleştiği tek bir hafta (henüz birleştirilmemiş) için , bir sonraki gruplamaya birleştirin
  • Hiçbirinin birleştirilmediği aynı adada bitişik iki hafta boyunca, her ikisi de birleştirilebilecek kısmi haftalarsa (örneğin, "Pzt, Sal, Çar, Per," ve "Çar, Per, Cmt") birleştirin. )
  • Kalan tek bir hafta boyunca (henüz birleştirilmemiş), mümkünse haftayı iki parçaya ayırın ve her iki parçayı da birleştirin, ilk kısmı aynı adada önceki gruplandırmaya ve ikinci kısmı aynı adada aşağıdaki gruplandırmaya birleştirin

Çalışma çözümünü üretmek için çok büyük çaba harcadığınız için teşekkür ederiz. Dürüst olmak gerekirse biraz ezici. Kısmi haftaları birleştirmenin basit olmayacağından şüphelendim, ancak bunun çok karmaşık olmasını bekleyemezdim. Hala daha kolay yapılabileceğine dair umudum var, ama somut bir fikrim yok.
Vladimir Baranov

Hızlı kontrol, örnek veriler için beklenen sonucu ürettiğini doğrular, ki bu harika, ancak bazı programların en iyi şekilde ele alınmadığını fark ettim. En basit örnek: (1214,12,'2016-05-06', 'Fri', 6), (1225,12,'2016-05-09', 'Mon', 2),. Bir aralık olarak temsil edilebilir, ancak çözümünüz iki tane üretir. İtiraf ediyorum, bu örnek örnek verilerde değildi ve kritik değil. Çözümünüzü gerçek veriler üzerinde çalıştırmaya çalışacağım.
Vladimir Baranov

Cevabınızı takdir ediyorum. Ödül başladığım zaman tekrarlayan CTE hakkında düşünmemiştim ve ilk öneren ve çalışan bir çözüm sunan Ziggy Crueltyfree Zeitgeister oldu. Kesin olarak, özyinelemeli CTE set tabanlı bir çözüm değildir, ancak en iyi sonuçları verir, makul derecede karmaşıktır ve oldukça hızlıdır. Cevabınız temel alınmıştır, ancak pratik olmadığı noktaya kadar çok karmaşıktır. Keşke ödülü bölebilseydim, ama maalesef buna izin verilmiyor.
Vladimir Baranov

@VladimirBaranov Sorun değil, ödül dilediğiniz gibi kullanmak için% 100 sizin. Ödül sorularını sevmemin nedeni, soruyu soran kişinin normal bir sorudan çok daha fazla meşgul olmasıdır. Puanları fazla önemsemeyin. Bu çözümün üretim kodumda kullanacağım bir çözüm olmadığına tamamen katılıyorum; potansiyel bir fikrin keşfi idi, ama sonuçta oldukça karmaşıktı.
Geoff Patterson

3

Boşlukları olan haftaları veya hafta sonları olan haftaları gruplamanın arkasındaki mantığı anlayamadım (örneğin, bir hafta sonu ile iki hafta üst üste olduğunda, hafta sonu hangi hafta geçiyor?).

Aşağıdaki sorgu, yalnızca art arda hafta içi günleri ve Paz-Cts (Paz-Paz yerine) haftaları gruplaması dışında istenen çıktıyı üretir. Tam olarak ne istediğinizi olmasa da, belki bu farklı bir strateji için bazı ipuçları sağlayabilir. Günlerin gruplanması buradan geliyor . Kullanılan pencereleme işlevleri SQLServer 2008 ile çalışmalıdır, ancak gerçekten çalışıp çalışmadığını test etmek için bu sürümü yok.

WITH 
  mysrc AS (
    SELECT *, RANK() OVER (PARTITION BY ContractID ORDER BY DT) AS rank
    FROM @Src
    ),
  prepos AS (
    SELECT s.*, pos.ID AS posid
    FROM mysrc s
    LEFT JOIN mysrc pos ON (pos.ContractID = s.ContractID AND pos.rank = s.rank+1 AND (pos.DowInt = s.DowInt+1 OR pos.DowInt = 2 AND s.DowInt=6))
    ),
  grped AS (
    SELECT TOP 100 *, (SELECT COUNT(CASE WHEN posid IS NULL THEN 1 END) FROM prepos WHERE contractid = p.contractid AND rank < p.rank) as grp
    FROM prepos p
    ORDER BY ContractID, DT
    )
SELECT ContractID, min(dt) AS StartDT, max(dt) AS EndDT, count(*) AS DayCount,
       STUFF( (SELECT ', ' + dowchar
               FROM (
                 SELECT TOP 100 dowint, dowchar 
                 FROM grped 
                 WHERE ContractID = g.ContractID AND grp = g.grp 
                 GROUP BY dowint, dowchar 
                 ORDER BY 1
                 ) a 
               FOR XML PATH(''), TYPE).value('.','varchar(max)'), 1, 2, '') AS WeekDays
FROM grped g
GROUP BY ContractID, grp
ORDER BY 1, 2

Sonuç

+------------+------------+------------+----------+-----------------------------------+
| ContractID | StartDT    | EndDT      | DayCount | WeekDays                          |
+------------+------------+------------+----------+-----------------------------------+
| 1          | 2/05/2016  | 13/05/2016 | 10       | Mon, Tue, Wed, Thu, Fri           |
| 2          | 5/05/2016  | 17/05/2016 | 9        | Mon, Tue, Wed, Thu, Fri           |
| 3          | 2/05/2016  | 2/05/2016  | 1        | Mon                               |
| 3          | 4/05/2016  | 4/05/2016  | 1        | Wed                               |
| 3          | 6/05/2016  | 9/05/2016  | 2        | Mon, Fri                          |
| 3          | 11/05/2016 | 11/05/2016 | 1        | Wed                               |
| 3          | 13/05/2016 | 16/05/2016 | 2        | Mon, Fri                          |
| 4          | 2/05/2016  | 6/05/2016  | 5        | Mon, Tue, Wed, Thu, Fri           |
| 4          | 10/05/2016 | 13/05/2016 | 4        | Tue, Wed, Thu, Fri                |
| 5          | 2/05/2016  | 6/05/2016  | 5        | Mon, Tue, Wed, Thu, Fri           |
| 5          | 10/05/2016 | 20/05/2016 | 9        | Mon, Tue, Wed, Thu, Fri           |
| 6          | 5/05/2016  | 17/05/2016 | 9        | Mon, Tue, Wed, Thu, Fri           |
| 6          | 6/06/2016  | 17/06/2016 | 10       | Mon, Tue, Wed, Thu, Fri           |
| 7          | 2/05/2016  | 7/05/2016  | 6        | Mon, Tue, Wed, Thu, Fri, Sat      |
| 7          | 8/05/2016  | 13/05/2016 | 6        | Sun, Mon, Tue, Wed, Thu, Fri      |
| 8          | 30/04/2016 | 30/04/2016 | 1        | Sat                               |
| 8          | 1/05/2016  | 7/05/2016  | 7        | Sun, Mon, Tue, Wed, Thu, Fri, Sat |
| 8          | 8/05/2016  | 14/05/2016 | 7        | Sun, Mon, Tue, Wed, Thu, Fri, Sat |
| 9          | 2/05/2016  | 4/05/2016  | 3        | Mon, Tue, Wed                     |
| 9          | 9/05/2016  | 10/05/2016 | 2        | Mon, Tue                          |
+------------+------------+------------+----------+-----------------------------------+

Bu cevapla ilgili tartışma sohbete taşındı .
Paul White Reinstate Monica

3

Tamlık uğruna, gaps-and-islandsbu soruyu sormadan önce kendimi denediğim iki geçişli bir yaklaşım.

Gerçek veriler üzerinde test ederken, yanlış sonuçlar ürettiği ve düzelttiği birkaç durum buldum.

İşte algoritma:

  • Ardışık tarih adaları oluşturun (CTE_ContractDays , CTE_DailyRN, CTE_DailyIslands) ve adanın her başlangıç ve bitiş tarih boyunca haftada sayısını hesaplamak. Burada hafta sayısı, Pazartesi'nin haftanın ilk günü olduğu varsayılarak hesaplanır.
  • Zamanlamanın aynı hafta içinde sıralı olmayan tarihleri ​​varsa (örnek 3'teki gibi), önceki aşama aynı hafta için birkaç satır oluşturur. Grup satırlarının haftada yalnızca bir satırı olmalıdır ( CTE_Weeks).
  • Önceki aşamadaki her satır için virgülle ayrılmış bir hafta günleri listesi oluşturun (CTE_FirstResult ).
  • Birbirini izleyen haftalara aynı şekilde gruplamak için boşluklar ve adaların ikinci geçişi WeekDays( CTE_SecondRN, CTE_Schedules).

Haftalık kalıplarda bozulma olmadığı durumlarda iyi durumdadır (1, 7, 8, 10, 12). Desenin ardışık olmayan günleri olduğunda iyi durumları ele alır (3).

Ancak, ne yazık ki, kısmi haftalar için ekstra aralıklar üretir (2, 3, 5, 6, 9, 11).

WITH
CTE_ContractDays
AS
(
    SELECT
         S.ContractID
        ,MIN(S.dt) OVER (PARTITION BY S.ContractID) AS ContractMinDT
        ,S.dt
        ,ROW_NUMBER() OVER (PARTITION BY S.ContractID ORDER BY S.dt) AS rn1
        ,DATEDIFF(day, '2001-01-01', S.dt) AS DayNumber
        ,S.dowChar
        ,S.dowInt
    FROM
        @Src AS S
)
,CTE_DailyRN
AS
(
    SELECT
        DayNumber - rn1 AS WeekGroupNumber
        ,ROW_NUMBER() OVER (
            PARTITION BY
                ContractID
                ,DayNumber - rn1
            ORDER BY dt) AS rn2
        ,ContractID
        ,ContractMinDT
        ,dt
        ,rn1
        ,DayNumber
        ,dowChar
        ,dowInt
    FROM CTE_ContractDays
)
,CTE_DailyIslands
AS
(
    SELECT
        ContractID
        ,ContractMinDT
        ,MIN(dt) AS MinDT
        ,MAX(dt) AS MaxDT
        ,COUNT(*) AS DayCount
        -- '2001-01-01' is Monday
        ,DATEDIFF(day, '2001-01-01', MIN(dt)) / 7 AS WeekNumberMin
        ,DATEDIFF(day, '2001-01-01', MAX(dt)) / 7 AS WeekNumberMax
    FROM CTE_DailyRN
    GROUP BY
        ContractID
        ,rn1-rn2
        ,ContractMinDT
)
,CTE_Weeks
AS
(
    SELECT
        ContractID
        ,ContractMinDT
        ,MIN(MinDT) AS MinDT
        ,MAX(MaxDT) AS MaxDT
        ,SUM(DayCount) AS DayCount
        ,WeekNumberMin
        ,WeekNumberMax
    FROM CTE_DailyIslands
    GROUP BY
        ContractID
        ,ContractMinDT
        ,WeekNumberMin
        ,WeekNumberMax
)
,CTE_FirstResult
AS
(
    SELECT
        ContractID
        ,ContractMinDT
        ,MinDT
        ,MaxDT
        ,DayCount
        ,CA_Data.XML_Value AS DaysOfWeek
        ,WeekNumberMin AS WeekNumber
        ,ROW_NUMBER() OVER(PARTITION BY ContractID ORDER BY MinDT) AS rn1
    FROM
        CTE_Weeks
        CROSS APPLY
        (
            SELECT CAST(CTE_ContractDays.dowChar AS varchar(8000)) + ',' AS dw
            FROM CTE_ContractDays
            WHERE
                    CTE_ContractDays.ContractID = CTE_Weeks.ContractID
                AND CTE_ContractDays.dt >= CTE_Weeks.MinDT
                AND CTE_ContractDays.dt <= CTE_Weeks.MaxDT
            GROUP BY
                CTE_ContractDays.dowChar
                ,CTE_ContractDays.dowInt
            ORDER BY CTE_ContractDays.dowInt
            FOR XML PATH(''), TYPE
        ) AS CA_XML(XML_Value)
        CROSS APPLY
        (
            SELECT CA_XML.XML_Value.value('.', 'VARCHAR(8000)')
        ) AS CA_Data(XML_Value)
)
,CTE_SecondRN
AS
(
    SELECT 
        ContractID
        ,ContractMinDT
        ,MinDT
        ,MaxDT
        ,DayCount
        ,DaysOfWeek
        ,WeekNumber
        ,rn1
        ,WeekNumber - rn1 AS SecondGroupNumber
        ,ROW_NUMBER() OVER (
            PARTITION BY
                ContractID
                ,DaysOfWeek
                ,DayCount
                ,WeekNumber - rn1
            ORDER BY MinDT) AS rn2
    FROM CTE_FirstResult
)
,CTE_Schedules
AS
(
    SELECT
        ContractID
        ,MIN(MinDT) AS StartDT
        ,MAX(MaxDT) AS EndDT
        ,SUM(DayCount) AS DayCount
        ,DaysOfWeek
    FROM CTE_SecondRN
    GROUP BY
        ContractID
        ,DaysOfWeek
        ,rn1-rn2
)
SELECT
    ContractID
    ,StartDT
    ,EndDT
    ,DayCount
    ,DaysOfWeek AS WeekDays
FROM CTE_Schedules
ORDER BY
    ContractID
    ,StartDT
;

Sonuç

+------------+------------+------------+----------+------------------------------+
| ContractID |  StartDT   |   EndDT    | DayCount |           WeekDays           |
+------------+------------+------------+----------+------------------------------+
|          1 | 2016-05-02 | 2016-05-13 |       10 | Mon,Tue,Wed,Thu,Fri,         |
|          2 | 2016-05-05 | 2016-05-06 |        2 | Thu,Fri,                     |
|          2 | 2016-05-09 | 2016-05-13 |        5 | Mon,Tue,Wed,Thu,Fri,         |
|          2 | 2016-05-16 | 2016-05-17 |        2 | Mon,Tue,                     |
|          3 | 2016-05-02 | 2016-05-13 |        6 | Mon,Wed,Fri,                 |
|          3 | 2016-05-16 | 2016-05-16 |        1 | Mon,                         |
|          4 | 2016-05-02 | 2016-05-06 |        5 | Mon,Tue,Wed,Thu,Fri,         |
|          4 | 2016-05-10 | 2016-05-13 |        4 | Tue,Wed,Thu,Fri,             |
|          5 | 2016-05-02 | 2016-05-06 |        5 | Mon,Tue,Wed,Thu,Fri,         |
|          5 | 2016-05-10 | 2016-05-13 |        4 | Tue,Wed,Thu,Fri,             |
|          5 | 2016-05-16 | 2016-05-20 |        5 | Mon,Tue,Wed,Thu,Fri,         |
|          6 | 2016-05-05 | 2016-05-06 |        2 | Thu,Fri,                     |
|          6 | 2016-05-09 | 2016-05-13 |        5 | Mon,Tue,Wed,Thu,Fri,         |
|          6 | 2016-05-16 | 2016-05-17 |        2 | Mon,Tue,                     |
|          6 | 2016-06-06 | 2016-06-17 |       10 | Mon,Tue,Wed,Thu,Fri,         |
|          7 | 2016-05-02 | 2016-05-13 |       12 | Sun,Mon,Tue,Wed,Thu,Fri,Sat, |
|          8 | 2016-04-30 | 2016-05-14 |       15 | Sun,Mon,Tue,Wed,Thu,Fri,Sat, |
|          9 | 2016-05-02 | 2016-05-11 |        6 | Mon,Tue,Wed,                 |
|          9 | 2016-05-16 | 2016-05-17 |        2 | Mon,Tue,                     |
|         10 | 2016-05-05 | 2016-05-22 |       12 | Sun,Thu,Fri,Sat,             |
|         11 | 2016-05-03 | 2016-05-10 |        2 | Tue,                         |
|         11 | 2016-05-17 | 2016-05-19 |        2 | Tue,Thu,                     |
|         11 | 2016-05-26 | 2016-06-02 |        2 | Thu,                         |
|         12 | 2016-05-02 | 2016-05-06 |        5 | Mon,Tue,Wed,Thu,Fri,         |
|         12 | 2016-05-16 | 2016-05-20 |        5 | Mon,Tue,Wed,Thu,Fri,         |
+------------+------------+------------+----------+------------------------------+

İmleç tabanlı çözüm

C # kodumu, gerçek verilerdeki diğer çözümlerle nasıl karşılaştırıldığını görmek için imleç tabanlı bir algoritmaya dönüştürdüm. Diğer küme tabanlı veya özyinelemeli yaklaşımlardan çok daha yavaş olduğunu doğrular, ancak en iyi sonucu verir.

CREATE TABLE #Dst_V2 (ContractID bigint, StartDT date, EndDT date, DayCount int, WeekDays varchar(255) COLLATE SQL_Latin1_General_CP1_CI_AS);

SET NOCOUNT ON;

DECLARE @VarOldDateFirst int = @@DATEFIRST;
SET DATEFIRST 7;

DECLARE @iFS int;
DECLARE @VarCursor CURSOR;
SET @VarCursor = CURSOR FAST_FORWARD
FOR
    SELECT
        ContractID
        ,dt
        ,dowChar
        ,dowInt
    FROM #Src AS S
    ;

OPEN @VarCursor;

DECLARE @CurrContractID bigint = 0;
DECLARE @Currdt date;
DECLARE @CurrdowChar char(3);
DECLARE @CurrdowInt int;


DECLARE @VarCreateNewInterval bit = 0;
DECLARE @VarTempDT date;
DECLARE @VarTempdowInt int;

DECLARE @LastContractID bigint = 0;
DECLARE @LastStartDT date;
DECLARE @LastEndDT date;
DECLARE @LastDayCount int = 0;
DECLARE @LastWeekDays varchar(255);
DECLARE @LastMonCount int;
DECLARE @LastTueCount int;
DECLARE @LastWedCount int;
DECLARE @LastThuCount int;
DECLARE @LastFriCount int;
DECLARE @LastSatCount int;
DECLARE @LastSunCount int;


FETCH NEXT FROM @VarCursor INTO @CurrContractID, @Currdt, @CurrdowChar, @CurrdowInt;
SET @iFS = @@FETCH_STATUS;
IF @iFS = 0
BEGIN
    SET @LastContractID = @CurrContractID;
    SET @LastStartDT = @Currdt;
    SET @LastEndDT = @Currdt;
    SET @LastDayCount = 1;
    SET @LastMonCount = 0;
    SET @LastTueCount = 0;
    SET @LastWedCount = 0;
    SET @LastThuCount = 0;
    SET @LastFriCount = 0;
    SET @LastSatCount = 0;
    SET @LastSunCount = 0;
    IF @CurrdowInt = 1 SET @LastSunCount = @LastSunCount + 1;
    IF @CurrdowInt = 2 SET @LastMonCount = @LastMonCount + 1;
    IF @CurrdowInt = 3 SET @LastTueCount = @LastTueCount + 1;
    IF @CurrdowInt = 4 SET @LastWedCount = @LastWedCount + 1;
    IF @CurrdowInt = 5 SET @LastThuCount = @LastThuCount + 1;
    IF @CurrdowInt = 6 SET @LastFriCount = @LastFriCount + 1;
    IF @CurrdowInt = 7 SET @LastSatCount = @LastSatCount + 1;
END;

WHILE @iFS = 0
BEGIN

    SET @VarCreateNewInterval = 0;

    -- Contract changes -> start new interval
    IF @LastContractID <> @CurrContractID
    BEGIN
        SET @VarCreateNewInterval = 1;
    END;

    IF @VarCreateNewInterval = 0
    BEGIN
        -- check days of week
        -- are we still within the first week of the interval?
        IF DATEDIFF(day, @LastStartDT, @Currdt) > 6
        BEGIN
            -- we are beyond the first week, check day of the week
            -- have we seen @CurrdowInt before?
            -- we should start a new interval if this is the new day of the week that didn't exist in the first week
            IF @CurrdowInt = 1 AND @LastSunCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 2 AND @LastMonCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 3 AND @LastTueCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 4 AND @LastWedCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 5 AND @LastThuCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 6 AND @LastFriCount = 0 SET @VarCreateNewInterval = 1;
            IF @CurrdowInt = 7 AND @LastSatCount = 0 SET @VarCreateNewInterval = 1;

            IF @VarCreateNewInterval = 0
            BEGIN
                -- check the gap between current day and last day of the interval
                -- if the gap between current day and last day of the interval
                -- contains a day of the week that was included in the interval before,
                -- we should create new interval
                SET @VarTempDT = DATEADD(day, 1, @LastEndDT);
                WHILE @VarTempDT < @Currdt
                BEGIN
                    SET @VarTempdowInt = DATEPART(WEEKDAY, @VarTempDT);

                    IF @VarTempdowInt = 1 AND @LastSunCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 2 AND @LastMonCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 3 AND @LastTueCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 4 AND @LastWedCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 5 AND @LastThuCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 6 AND @LastFriCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;
                    IF @VarTempdowInt = 7 AND @LastSatCount > 0 BEGIN SET @VarCreateNewInterval = 1; BREAK; END;

                    SET @VarTempDT = DATEADD(day, 1, @VarTempDT);
                END;
            END;
        END;
        -- else
        -- we are still within the first week, so we can add this day to the interval
    END;

    IF @VarCreateNewInterval = 1
    BEGIN
        -- save the new interval into the final table
        SET @LastWeekDays = '';
        IF @LastSunCount > 0 SET @LastWeekDays = @LastWeekDays + 'Sun,';
        IF @LastMonCount > 0 SET @LastWeekDays = @LastWeekDays + 'Mon,';
        IF @LastTueCount > 0 SET @LastWeekDays = @LastWeekDays + 'Tue,';
        IF @LastWedCount > 0 SET @LastWeekDays = @LastWeekDays + 'Wed,';
        IF @LastThuCount > 0 SET @LastWeekDays = @LastWeekDays + 'Thu,';
        IF @LastFriCount > 0 SET @LastWeekDays = @LastWeekDays + 'Fri,';
        IF @LastSatCount > 0 SET @LastWeekDays = @LastWeekDays + 'Sat,';

        INSERT INTO #Dst_V2 
            (ContractID
            ,StartDT
            ,EndDT
            ,DayCount
            ,WeekDays)
        VALUES
            (@LastContractID
            ,@LastStartDT
            ,@LastEndDT
            ,@LastDayCount
            ,@LastWeekDays);

        -- init the new interval
        SET @LastContractID = @CurrContractID;
        SET @LastStartDT = @Currdt;
        SET @LastEndDT = @Currdt;
        SET @LastDayCount = 1;
        SET @LastMonCount = 0;
        SET @LastTueCount = 0;
        SET @LastWedCount = 0;
        SET @LastThuCount = 0;
        SET @LastFriCount = 0;
        SET @LastSatCount = 0;
        SET @LastSunCount = 0;
        IF @CurrdowInt = 1 SET @LastSunCount = @LastSunCount + 1;
        IF @CurrdowInt = 2 SET @LastMonCount = @LastMonCount + 1;
        IF @CurrdowInt = 3 SET @LastTueCount = @LastTueCount + 1;
        IF @CurrdowInt = 4 SET @LastWedCount = @LastWedCount + 1;
        IF @CurrdowInt = 5 SET @LastThuCount = @LastThuCount + 1;
        IF @CurrdowInt = 6 SET @LastFriCount = @LastFriCount + 1;
        IF @CurrdowInt = 7 SET @LastSatCount = @LastSatCount + 1;

    END ELSE BEGIN

        -- update last interval
        SET @LastEndDT = @Currdt;
        SET @LastDayCount = @LastDayCount + 1;
        IF @CurrdowInt = 1 SET @LastSunCount = @LastSunCount + 1;
        IF @CurrdowInt = 2 SET @LastMonCount = @LastMonCount + 1;
        IF @CurrdowInt = 3 SET @LastTueCount = @LastTueCount + 1;
        IF @CurrdowInt = 4 SET @LastWedCount = @LastWedCount + 1;
        IF @CurrdowInt = 5 SET @LastThuCount = @LastThuCount + 1;
        IF @CurrdowInt = 6 SET @LastFriCount = @LastFriCount + 1;
        IF @CurrdowInt = 7 SET @LastSatCount = @LastSatCount + 1;
    END;


    FETCH NEXT FROM @VarCursor INTO @CurrContractID, @Currdt, @CurrdowChar, @CurrdowInt;
    SET @iFS = @@FETCH_STATUS;
END;

-- save the last interval into the final table
IF @LastDayCount > 0
BEGIN
    SET @LastWeekDays = '';
    IF @LastSunCount > 0 SET @LastWeekDays = @LastWeekDays + 'Sun,';
    IF @LastMonCount > 0 SET @LastWeekDays = @LastWeekDays + 'Mon,';
    IF @LastTueCount > 0 SET @LastWeekDays = @LastWeekDays + 'Tue,';
    IF @LastWedCount > 0 SET @LastWeekDays = @LastWeekDays + 'Wed,';
    IF @LastThuCount > 0 SET @LastWeekDays = @LastWeekDays + 'Thu,';
    IF @LastFriCount > 0 SET @LastWeekDays = @LastWeekDays + 'Fri,';
    IF @LastSatCount > 0 SET @LastWeekDays = @LastWeekDays + 'Sat,';

    INSERT INTO #Dst_V2
        (ContractID
        ,StartDT
        ,EndDT
        ,DayCount
        ,WeekDays)
    VALUES
        (@LastContractID
        ,@LastStartDT
        ,@LastEndDT
        ,@LastDayCount
        ,@LastWeekDays);
END;

CLOSE @VarCursor;
DEALLOCATE @VarCursor;

SET DATEFIRST @VarOldDateFirst;

DROP TABLE #Dst_V2;

2

Vladimir'in imleç çözümünün çok yavaş olduğuna biraz şaşırdım , bu yüzden de bu sürümü optimize etmeye çalıştım. İmleç kullanmanın benim için de çok yavaş olduğunu doğruladım.

Ancak, bir satır kümesi işlenirken bir değişkene eklenerek SQL Server'da belgelenmemiş işlevsellik kullanma pahasına, bu mantığın en iyi sonucu veren ve hem imleçten hem de orijinal çözümümden çok daha hızlı çalışan basitleştirilmiş bir sürümünü oluşturabildim . Bu yüzden kendi sorumluluğunuzda kullanın, ancak ilginizi çekmesi durumunda çözümü sunacağım. Ayrıca, WHILEdöngüyü her döngüde bir sonraki satır numarasına bakarak bir döngüden maksimum satır numarasına kadar bir döngü kullanacak şekilde güncellemek de mümkün olacaktır . Bu, tam olarak belgelenmiş ve güvenilir işlevselliğe bağlı kalacaktır, ancak WHILEdöngülere izin verilmeyen sorunun (biraz yapay) belirtilen kısıtlamasını ihlal edecektir .

SQL 2014 kullanımına izin verildiyse , satır numaralarının üzerinden geçen ve belleğe göre optimize edilmiş bir tablodaki her satır numarasına erişen yerel olarak derlenmiş bir saklı yordamın , daha hızlı çalışacak bu aynı mantığın bir uygulaması olması muhtemeldir.

Deneme verilerinin yaklaşık yarım milyon satıra genişletilmesi de dahil olmak üzere tam çözüm . Yeni çözüm yaklaşık 3 saniye içinde tamamlanıyor ve bence, sunduğum önceki çözümden çok daha özlü ve okunabilir. Burada yer alan üç adımı atacağım:

1.Adım: Ön işleme

Önce veri kümesine bir satır numarası ekliyoruz, sırayla verileri işleyeceğiz. Bunu yaparken, her bir gruplamada hangi günlerin gözlemlendiğini temsil etmek için bir bitmap kullanabilmemiz için her bir dowInt değerini 2 gücüne dönüştürürüz:

IF OBJECT_ID('tempdb..#srcWithRn') IS NOT NULL
    DROP TABLE #srcWithRn
GO
SELECT rn = IDENTITY(INT, 1, 1), ContractId, dt, dowInt,
    POWER(2, dowInt) AS dowPower, dowChar
INTO #srcWithRn
FROM #src
ORDER BY ContractId, dt
GO
ALTER TABLE #srcWithRn
ADD PRIMARY KEY (rn)
GO

Adım 2: Yeni gruplamaları tanımlamak için sözleşme günlerinde döngü

Sıradaki sıraya göre veriler üzerinde bir sonraki döngüye giriyoruz. Yalnızca yeni bir grubun sınırını oluşturan satır numaralarının listesini hesaplıyoruz, ardından bu satır numaralarını bir tabloya çıkarıyoruz:

DECLARE @ContractId INT, @RnList VARCHAR(MAX), @NewGrouping BIT = 0, @DowBitmap INT = 0, @startDt DATE
SELECT TOP 1 @ContractId = ContractId, @startDt = dt, @RnList = ',' + CONVERT(VARCHAR(MAX), rn), @DowBitmap = DowPower
FROM #srcWithRn
WHERE rn = 1

SELECT 
    -- New grouping if new contract, or if we're observing a new day that we did
    -- not observe within the first 7 days of the grouping
    @NewGrouping = CASE
        WHEN ContractId <> @ContractId THEN 1
        WHEN DATEDIFF(DAY, @startDt, dt) > 6
            AND @DowBitmap & dowPower <> dowPower THEN 1
        ELSE 0
        END,
    @ContractId = ContractId,
    -- If this is a newly observed day in an existing grouping, add it to the bitmap
    @DowBitmap = CASE WHEN @NewGrouping = 0 THEN @DowBitmap | DowPower ELSE DowPower END,
    -- If this is a new grouping, reset the start date of the grouping
    @startDt = CASE WHEN @NewGrouping = 0 THEN @startDt ELSE dt END,
    -- If this is a new grouping, add this rn to the list of row numbers that delineate the boundary of a new grouping
    @RnList = CASE WHEN @NewGrouping = 0 THEN @RnList ELSE @RnList + ',' + CONVERT(VARCHAR(MAX), rn) END 
FROM #srcWithRn
WHERE rn >= 2
ORDER BY rn
OPTION (MAXDOP 1)

-- Split the list of grouping boundaries into a table
IF OBJECT_ID('tempdb..#newGroupingRns') IS NOT NULL
    DROP TABLE #newGroupingRns
SELECT splitListId AS rn
INTO #newGroupingRns
FROM dbo.f_delimitedIntListSplitter(SUBSTRING(@RnList, 2, 1000000000), DEFAULT)
GO
ALTER TABLE #newGroupingRns
ADD PRIMARY KEY (rn)
GO

3. Adım: Her bir gruplandırma sınırının satır numaralarına göre nihai sonuçları hesaplama

Daha sonra, her grupta yer alan tüm tarihleri ​​toplamak için yukarıdaki döngüde belirtilen sınırları kullanarak son gruplamaları hesaplıyoruz:

IF OBJECT_ID('tempdb..#finalGroupings') IS NOT NULL
    DROP TABLE #finalGroupings
GO
SELECT MIN(s.ContractId) AS ContractId,
    MIN(dt) AS StartDT,
    MAX(dt) AS EndDT,
    COUNT(*) AS DayCount,
    CASE WHEN MAX(CASE WHEN dowChar = 'Sun' THEN 1 ELSE 0 END) = 1 THEN 'Sun,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Mon' THEN 1 ELSE 0 END) = 1 THEN 'Mon,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Tue' THEN 1 ELSE 0 END) = 1 THEN 'Tue,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Wed' THEN 1 ELSE 0 END) = 1 THEN 'Wed,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Thu' THEN 1 ELSE 0 END) = 1 THEN 'Thu,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Fri' THEN 1 ELSE 0 END) = 1 THEN 'Fri,' ELSE '' END + 
    CASE WHEN MAX(CASE WHEN dowChar = 'Sat' THEN 1 ELSE 0 END) = 1 THEN 'Sat,' ELSE '' END AS WeekDays
INTO #finalGroupings
FROM #srcWithRn s
CROSS APPLY (
    -- For any row, its grouping is the largest boundary row number that occurs at or before this row
    SELECT TOP 1 rn AS groupingRn
    FROM #newGroupingRns grp
    WHERE grp.rn <= s.rn
    ORDER BY grp.rn DESC
) g
GROUP BY g.groupingRn
ORDER BY g.groupingRn
GO

Teşekkür ederim. İmleçler veya WHILEdöngüler kullanmamamı istedim , çünkü imleçle nasıl çözüleceğini zaten biliyordum ve set tabanlı bir çözüm bulmak istedim. Ayrıca, imlecin yavaş olacağından şüpheliydim (özellikle içinde iç içe bir döngü varken). Bu cevap yeni püf noktaları öğrenme açısından çok ilginç ve çabalarınızı takdir ediyorum.
Vladimir Baranov

1

Tartışma kodu takip edecektir.

declare @Helper table(
    rn tinyint,
    dowInt tinyint,
    dowChar char(3));
insert @Helper
values  ( 1,1,'Sun'),
        ( 2,2,'Mon'),
        ( 3,3,'Tue'),
        ( 4,4,'Wed'),
        ( 5,5,'Thu'),
        ( 6,6,'Fri'),
        ( 7,7,'Sat'),
        ( 8,1,'Sun'),
        ( 9,2,'Mon'),
        (10,3,'Tue'),
        (11,4,'Wed'),
        (12,5,'Thu'),
        (13,6,'Fri'),
        (14,7,'Sat');



with MissingDays as
(
    select
        h1.rn as rn1,
        h1.dowChar as StartDay,
        h2.rn as rn2,
        h2.dowInt as FollowingDayInt,
        h2.dowChar as FollowingDayChar
    from @Helper as h1
    inner join @Helper as h2
        on h2.rn > h1.rn
    where h1.rn < 8
    and h2.rn < h1.rn + 8
)
,Numbered as
(
    select
        a.*,
        ROW_NUMBER() over (partition by a.ContractID order by a.dt) as rn
    from #Src as a
)
,Incremented as
(
    select
        b.*,
        convert(varchar(max), b.dowChar)+',' as WeekDays,
        b.dt as IntervalStart
    from Numbered as b
    where b.rn = 1

    union all

    select
        c.*,
        case
            when
                (DATEDIFF(day, d.IntervalStart, c.dt) > 6)      -- interval goes beyond 7 days
            and (
                    (d.WeekDays not like '%'+c.dowChar+'%')     -- the new week day has not been seen before
                or 
                    (DATEDIFF(day, d.dt, c.dt) > 7)
                or 
                    (
                        (DATEDIFF(day, d.dt, c.dt) > 1)
                        and
                        (
                        exists( select
                                    e.FollowingDayChar
                                from MissingDays as e
                                where e.StartDay = d.dowChar
                                and rn2 < (select f.rn2 from MissingDays as f
                                            where f.StartDay = d.dowChar
                                            and f.FollowingDayInt = c.dowInt)
                                and d.WeekDays like '%'+e.FollowingDayChar+'%'
                            )
                        )
                    )
                )
            then convert(varchar(max),c.dowChar)+','
            else
                case
                    when d.WeekDays like '%'+c.dowChar+'%'
                    then d.WeekDays
                    else d.WeekDays+convert(varchar(max),c.dowChar)+','
                end
        end,
        case
            when
                (DATEDIFF(day, d.IntervalStart, c.dt) > 6)      -- interval goes beyond 7 days
            and (
                    (d.WeekDays not like '%'+c.dowChar+'%')     -- the new week day has not been seen before
                or
                    (DATEDIFF(day, d.dt, c.dt) > 7)             -- there is a one week gap
                or 
                    (
                        (DATEDIFF(day, d.dt, c.dt) > 1)         -- there is a gap..
                        and
                        (
                        exists( select                          -- .. and the omitted days are in the preceeding interval
                                    e.FollowingDayChar
                                from MissingDays as e
                                where e.StartDay = d.dowChar
                                and rn2 < (select f.rn2 from MissingDays as f
                                            where f.StartDay = d.dowChar
                                            and f.FollowingDayInt = c.dowInt)
                                and d.WeekDays like '%'+e.FollowingDayChar+'%'
                            )
                        )
                    )
                )
            then c.dt
            else d.IntervalStart
        end
    from Numbered as c
    inner join Incremented as d
    on d.ContractID = c.ContractID
    and d.rn = c.rn - 1
)
select
    g.ContractID,
    g.IntervalStart as StartDT,
    MAX(g.dt) as EndDT,
    COUNT(*) as DayCount,
    MAX(g.WeekDays) as WeekDays
from Incremented as g
group by
    g.ContractID,
    g.IntervalStart
order by
    ContractID,
    StartDT;

@Helper bu kuralla başa çıkmaktır:

Geçerli gün ile aralığın son günü arasındaki boşluk önceki aralığa dahil olan haftanın bir gününü içeriyorsa, yeni aralık oluşturmalıyız

Gün adlarını, verilen herhangi iki gün arasında gün numarası sırasına göre listelememi sağlar. Bu, yeni bir aralığın başlayıp başlamayacağına karar verirken kullanılır. Bir hafta sonu etrafına sarmanın kodlanmasını kolaylaştırmak için iki haftalık değerlerle dolduruyorum.

Bunu uygulamanın daha temiz yolları vardır. Tam bir "tarihler" tablosu bir olacaktır. Muhtemelen gün sayısı ve modulo aritmetiği ile akıllı bir yol var.

CTE MissingDays , verilen herhangi iki gün arasında bir gün adları listesi oluşturmaktır. Özyinelemeli CTE (aşağıdaki) agregalara, TOP () veya diğer operatörlere izin vermediğinden bu karmaşık şekilde işlenir. Bu yetersiz, ama işe yarıyor.

CTE Numbered , veriler üzerinde bilinen, boşluksuz bir sekans uygulamaktır. Daha sonra birçok karşılaştırmayı önler.

CTE Incremented, eylemin gerçekleştiği yerdir. Özünde, verileri gözden geçirmek ve kuralları uygulamak için özyinelemeli bir CTE kullanıyorum. Numbered(Yukarıda) içinde oluşturulan satır numarası , özyinelemeli işlemeyi yürütmek için kullanılır.

Özyinelemeli CTE'nin tohumu, her bir Sözleşme Kimliği için ilk tarihi alır ve yeni bir aralığın gerekli olup olmadığına karar vermek için kullanılacak değerleri başlatır.

Yeni bir aralığın başlayıp başlamayacağına karar vermek için geçerli aralığın başlangıç ​​tarihi, gün listesi ve takvimdeki boşlukların uzunluğu gerekir. Karara bağlı olarak bunlar sıfırlanabilir veya taşınabilir. Bu nedenle, özyinelemeli bölüm ayrıntılı ve biraz tekrarlayıcıdır, çünkü birden fazla sütun değeri için yeni bir aralık başlatıp başlatmaya karar vermemiz gerekir.

Sütunlar için karar mantığı WeekDaysve IntervalStartaynı karar mantığına sahip olmalıdır - aralarında kesilip yapıştırılabilir. Yeni bir aralık başlatma mantığı değişecekse, bu değiştirilecek koddur. İdeal olarak bu nedenle soyutlanacaktır; bunu özyinelemeli bir CTE'de yapmak zor olabilir.

EXISTS()Fıkra bir özyinelemeli CTE toplama işlevlerini kullanabilmek için olmama deşarj olduğunu. Tek yaptığı, bir boşluğa düşen günlerin şimdiki aralıkta olup olmadığını görmek.

Mantık cümleciklerinin yuvalanması konusunda sihir yoktur. Başka bir konformasyonda daha açıksa veya iç içe CASE'ler kullanıyorsa, diyelim ki onu bu şekilde tutmak için hiçbir neden yoktur.

Son SELECT, çıktıyı istenen formatta vermektir.

PK'nın açık Src.IDolması bu yöntem için yararlı değildir. Üzerinde kümelenmiş bir dizin(ContractID,dt) olurdu diye düşünüyorum.

Birkaç pürüzlü kenar var. Günler dow sırasında döndürülmez, ancak takvim sırasında kaynak verilerde görünür. @Helper ile ilgili her şey klunky ve düzeltilebilir. Günde bir bit kullanma ve bunun yerine ikili fonksiyonları kullanma fikrini seviyorum LIKE. Yardımcı CTE'lerin bazılarının uygun indekslerle geçici tabloya ayrılması şüphesiz yardımcı olacaktır.

Bununla ilgili zorluklardan biri, bir "haftanın" standart bir takvimle hizalanmaması, bunun yerine veriler tarafından yönlendirilmesidir ve yeni bir aralığın başlaması gerektiği belirlendiğinde sıfırlanır. Bir "hafta" veya en azından bir aralık, tüm veri kümesini kapsayan bir gün uzunluğunda olabilir.


İlgi alanları için, çeşitli değişikliklerden sonra Geoff'un örnek verilerine (bunun için teşekkürler!) Karşı tahmini maliyetler:

                                             estimated cost

My submission as is w/ CTEs, Geoff's data:      791682
Geoff's data, cluster key on (ContractID, dt):   21156.2
Real table for MissingDays:                      21156.2
Numbered as table UCI=(ContractID, rn):             16.6115    26s elapsed.
                  UCI=(rn, ContractID):             41.9845    26s elapsed.
MissingDays as refactored to simple lookup          16.6477    22s elapsed.
Weekdays as varchar(30)                             13.4013    30s elapsed.

Tahmini ve gerçek satır sayısı çok farklıdır.

Plan, muhtemelen özyinelemeli CTE'nin bir sonucu olarak bir masa spoo'suna sahiptir. Eylemin çoğu bir çalışma masasında geliyor:

Table 'Worktable'.   Scan count       2, logical reads 4 196 269, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'MissingDays'. Scan count 464 116, logical reads   928 232, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Numbered'.    Scan count 484 122, logical reads 1 475 467, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Özyinelemenin uygulandığı gibi, sanırım!


Teşekkür ederim. Örnek veriler üzerinde doğru ve optimum sonuç verir. Şimdi gerçek veriler üzerinde kontrol edeceğim. Bir yan not: MAX(g.IntervalStart)Çünkü, tuhaf görünüyor g.IntervalStartiçindedir GROUP BY. Bir sözdizimi hatası vermesini bekledim, ama işe yarıyor. Sadece g.IntervalStart as StartDTiçinde olmalı mı SELECT? Yoksa içinde g.IntervalStartolmamalı GROUP BYmı?
Vladimir Baranov

Sorguyu gerçek veriler üzerinde çalıştırmayı denedim ve 10 dakika sonra durdurmak zorunda kaldım. CTE'ler MissingDaysve Numbereduygun endekslere sahip geçici tablolarla değiştirilirse, iyi bir performansa sahip olması muhtemeldir. Hangi dizinleri önerirsiniz? Yarın sabah deneyebilirim.
Vladimir Baranov

NumberedBir geçici tablo ve kümelenmiş bir dizin ile değiştirme (ContractID, rn)gitmek değer olacağını düşünürdüm. Karşılık gelen planı oluşturmak için büyük bir veri kümesi olmadan tahmin etmek zordur. MissingDatesDizinlerle fizikselleştirme (StartDay, FollowingDayInt)de iyi olurdu.
Michael Green

Teşekkürler. Şu anda deneyemiyorum, ama yarın sabah yapacağım.
Vladimir Baranov

Bunu yarım milyon satırlık veri setinde denedim (mevcut veri seti, farklı ContractIds ile 4.000 kez çoğaltıldı). Yaklaşık 15 dakikadır çalışıyor ve şu ana kadar 30GB tempdb alan kapladı. Bu yüzden daha fazla optimizasyonun gerekli olabileceğini düşünüyorum. Yararlı bulmanız durumunda genişletilmiş test verileri .
Geoff Patterson
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.