[T-SQL] Verdelen van timelogs in blokken van X minuten

Pagina: 1
Acties:

Vraag


Acties:
  • 0 Henk 'm!

  • Jay-P
  • Registratie: Oktober 2003
  • Laatst online: 00:39
Beste Tweakers,

Ik zoek een functie of query om een tabel met logs met een start en eind datum te verdelen in bins/buckets met een interval van X (in dit geval 15 minuten). Ik heb tevens een time tabel met alle 86,400 seconden in een dag waar ik de start en eind tijden van elk interval blok kan joinen per log, samen met een rank van dat blok en het verschil tussen de ranks van het start- en eindblok Ter referentie:

logs
idstartenddurationstatus
12023-10-13 14:58:102023-10-13 14:59:2070Sleeping
22023-10-13 14:59:202023-10-13 15:02:20180Available
32023-10-13 14:02:202023-10-13 15:02:203600Available
42023-10-12 23:59:052023-10-13 00:01:05120Connected


Hierop join ik de time tabel op zowel start als eind timestamp van de log waardoor ik dit resultaat krijg:

logs
idstartenddurationstatusblockStartofStartblockEndofEnddiffStartEndBlock
12023-10-13 14:58:102023-10-13 14:59:2070Sleeping14:45:0015:00:000
22023-10-13 14:59:202023-10-13 15:02:20180Available14:45:0015:15:001
32023-10-13 14:02:202023-10-13 15:02:203600Available14:00:0015:15:004
42023-10-12 23:59:052023-10-13 00:01:05120Connected23:45:0000:15:00-95


Ik wil hier nu graag een tabel van genereren die de missende tussenintervallen genereert en de logs over die intervallen verspreid:


logs
idstartenddurationstatusblockStartofStartblockEndofEnddiffStartEndBlock
12023-10-13 14:58:102023-10-13 14:59:2070Sleeping14:45:0015:00:000
22023-10-13 14:59:202023-10-13 15:00:0040Available14:45:0015:00:000
32023-10-13 15:00:002023-10-13 15:02:20120Available15:00:0015:15:000
42023-10-13 14:02:202023-10-13 15:15:00760Available14:00:0014:15:000
52023-10-13 14:15:002023-10-13 14:30:00900Available14:15:0014:30:000
62023-10-13 14:30:002023-10-13 14:45:00900Available14:30:0014:45:000
72023-10-13 14:45:002023-10-13 15:00:00900Available14:45:0015:00:000
82023-10-13 15:00:002023-10-13 15:02:20140Available15:00:0015:15:000
92023-10-12 23:59:052023-10-13 00:00:0055Connected23:45:0000:00:000
102023-10-13 00:00:002023-10-13 00:01:0565Connected00:00:0000:15:000


Let op het record met -95, omdat de start en eind datum op een andere dag liggen (startblok is 96 (23:45-00:00) en eindblok is 1 (00:00-00:15)). Logs met een duration van langer dan 24uur zullen niet voorkomen dus we kunnen simpel stellen:

CASE diffStartEndBlock
WHEN 0 -->> return gewoon de originele log
WHEN > 0 Voeg het verschil toe en verdeel de logs over hun tijdsblokken
WHEN < 0 Voeg het (96 + verschil) toe en verdeel de logs over hun tijdsblokken

Hoe kan ik dit het beste aaanpakken? Ik weet dat het met de info die ik al heb met een recursieve CTE of een functie vrij makkelijk moet kunnen maar mis even wat SQL skills hier... Het gaat overigens over Azure SQL in dit geval, maar een andere taalspecifieke oplossing mag uiteraard ook!

Alle reacties


Acties:
  • 0 Henk 'm!

  • ASP
  • Registratie: December 2000
  • Laatst online: 03-06 19:15

ASP

Ik heb het door ChatGPT 4 heen gehaald voor je:

Dit is inderdaad een goede use case voor een recursieve CTE in combinatie met een time-tabel die je al hebt. Hier is een SQL-voorstel om het gewenste resultaat te verkrijgen op basis van de gegeven tabellen en structuren:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
WITH RECURSIVE CTE AS (
  SELECT 
    id, 
    start, 
    CASE WHEN DATEPART(MINUTE, end) % 15 <> 0 THEN DATEADD(
      MINUTE, 
      15 * (
        DATEPART(MINUTE, end) / 15
      ), 
      CAST(
        CAST(end AS DATE) AS DATETIME
      )
    ) ELSE end END AS adjusted_end, 
    duration, 
    status, 
    blockStartofStart, 
    blockEndofEnd, 
    diffStartEndBlock, 
    1 AS iteration 
  FROM 
    logs 
  WHERE 
    diffStartEndBlock = 0 
  UNION ALL 
  SELECT 
    id, 
    adjusted_end AS start, 
    DATEADD(MINUTE, 15, adjusted_end) AS adjusted_end, 
    duration, 
    status, 
    DATEADD(MINUTE, 15, blockStartofStart), 
    DATEADD(MINUTE, 15, blockEndofEnd), 
    diffStartEndBlock, 
    iteration + 1 
  FROM 
    CTE 
  WHERE 
    (
      diffStartEndBlock > 0 
      AND iteration <= diffStartEndBlock
    ) 
    OR (
      diffStartEndBlock < 0 
      AND iteration <= 96 + diffStartEndBlock
    )
) 
SELECT 
  ROW_NUMBER() OVER(
    ORDER BY 
      start
  ) AS new_id, 
  start, 
  CASE WHEN adjusted_end > end THEN end ELSE adjusted_end END AS end, 
  DATEDIFF(
    SECOND, start, CASE WHEN adjusted_end > end THEN end ELSE adjusted_end END
  ) AS duration, 
  status, 
  blockStartofStart, 
  blockEndofEnd, 
  diffStartEndBlock 
FROM 
  CTE 
ORDER BY 
  start;


Hier is wat het doet:

We maken een recursieve CTE die start met de originele logs waarvan diffStartEndBlock 0 is.

De recursie voegt voor elke iteratie 15 minuten toe aan de start en end tijden, evenals de blockStartofStart en blockEndofEnd tijdvakken, tot het verschil in blokken bereikt is.

De buitenste SELECT bevat een ROW_NUMBER() functie om een nieuwe ID te genereren voor elk resultaat.

We gebruiken een CASE statement om te zorgen dat de end tijd nooit het originele end moment overschrijdt.

Als er specifieke nuances zijn die ik gemist heb, laat het dan even weten. Het concept is echter dat je de recursieve CTE kunt gebruiken om het uitbreiden van de records te beheren op basis van de diffStartEndBlock waarde.

Acties:
  • +4 Henk 'm!

  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
ASP schreef op vrijdag 13 oktober 2023 @ 18:42:
Ik heb het door ChatGPT 4 heen gehaald voor je:
Vast heel goed bedoeld, maar als mensen een ChatGPT antwoord willen dan kunnen ze hun vraag daar zelf wel doorheen halen. Het is dus niet de bedoeling vragen door ChatGPT te halen en dan hier de antwoorden klakkeloos neer te gooien.

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


Acties:
  • 0 Henk 'm!

  • Voutloos
  • Registratie: Januari 2002
  • Niet online
Even heel suf en vloeken in de kerk: In programmacode is dit ook goed te doen.

Dus als de use case enkel is om dat laatste tabelletje van een dagje te renderen, ben je er misschien ook met een loopje over de kwartieren in de dag, en die eerste 4 rows in chronologische volgorde.

{signature}


Acties:
  • 0 Henk 'm!

  • Voutloos
  • Registratie: Januari 2002
  • Niet online
Overigens buitengewoon knap hoe ChatGPT helemaal zeker weet dat je het over UTC tijden hebt terwijl je het nergens specificeert. Wij stervelingen ervaren toch echt eens per jaar een dag met 92 kwartier en eentje met 100 kwartier. Super dat een taalmodel je op dat soort bugs kan wijzen!
/s

{signature}


Acties:
  • 0 Henk 'm!

  • ASP
  • Registratie: December 2000
  • Laatst online: 03-06 19:15

ASP

RobIII schreef op vrijdag 13 oktober 2023 @ 19:44:
[...]

Vast heel goed bedoeld, maar als mensen een ChatGPT antwoord willen dan kunnen ze hun vraag daar zelf wel doorheen halen. Het is dus niet de bedoeling vragen door ChatGPT te halen en dan hier de antwoorden klakkeloos neer te gooien.
Het was niet goed bedoeld, maar om:
  • TS erop te wijzen dat we in 2023 leven en ChatGPT 4 een uitstekende start kan bieden;
  • TS wellicht geen betaald Pro abo heeft voor ChatGPT 4. Mijn ervaringen met 3.5 en lager of andere LLM zijn niet goed en gevaarlijk om te gebruiken met weinig kennis over de materie (SQL) zelf, Daarom een betaald ChatGPT 4 antwoord;
  • TS een recursieve oplossingen te geven want deze zijn voor ons mensen altijd lastig en complex. Niemand op Tweakers gaat tijd steken in een uitgebreid antwoord zoals ChatGPT 4 dat heeft gedaan. Het dev forum zal z'n langste tijd ook hebben gehad om die reden. Ingehaald door ChatGPT 4.

Acties:
  • 0 Henk 'm!

  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
ASP schreef op zaterdag 14 oktober 2023 @ 11:55:
[...]


Het was niet goed bedoeld
Ook goed. Gewoon niet meer doen :)

[ Voor 64% gewijzigd door RobIII op 14-10-2023 12:27 ]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


Acties:
  • 0 Henk 'm!

  • Cartman!
  • Registratie: April 2000
  • Niet online
ASP schreef op zaterdag 14 oktober 2023 @ 11:55:
TS een recursieve oplossingen te geven want deze zijn voor ons mensen altijd lastig en complex. Niemand op Tweakers gaat tijd steken in een uitgebreid antwoord zoals ChatGPT 4 dat heeft gedaan. Het dev forum zal z'n langste tijd ook hebben gehad om die reden. Ingehaald door ChatGPT 4.
Als je ook kijkt wat een gedrocht van een query eruit komt vraag ik me af of dit een goed idee is... Gewoon lekker in je presentatielaag oplossen zou ik zeggen.

Acties:
  • 0 Henk 'm!

  • Jay-P
  • Registratie: Oktober 2003
  • Laatst online: 00:39
Ik zou het zelf waarschijnlijk ook in pandas of in power query oplossen. Maar aangezien dit data uit een SQL datawarehouse is, het geconsumeerd moet worden door een Power BI rapport dat gebruikt maakt van DirectQuery is dat geen oplossing helaas. Om meerdere redenen. Nogmaals, ik zou het ook anders oplossen maar realiteit dwingt me SQL te gebruiken. Ik ga er nog even over nadenken, volgens mij moet het namelijk ook met een join kunnen, zoiets als:

SQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
select 
    interval_start, 
    sum(seconds_before_trip_ended - seconds_before_trip_started) as seconds
from (
    select 
        interval_start,
        greatest(0, DATEDIFF(SECOND, '19700101', timestampStart - interval_start)) as seconds_before_trip_started,
        least(3600, DATEDIFF(SECOND, '19700101', timestampEnd) - interval_start) as seconds_before_trip_ended
    from (
        select * from GENERATE_SERIES(
            (select min(DATE_BUCKET(hour, 1, timestampStart)) from timesheet.timelogs),
            (select max(DATE_BUCKET(hour, 1, timestampEnd)) from timesheet.timelogs),
            '1 hour') as interval_start) i
    join timesheet.timelogs l
        on l.start_date <= DATEADD(hour, 1, i.interval_start)
        and coalesce(l.end_date, '2022-03-10 06:00:00') >= interval_start
    ) subq
group by interval_start
order by interval_start;


I know it´s ugly en niet werkend, maar ben even met wat oplossingen van SO aan het spelen. Beter op die manier met vallen en opstaan leren dan klakkeloos een chatgpt oplossing te copy pasten.

Acties:
  • +1 Henk 'm!

  • mbe81
  • Registratie: Juni 2008
  • Laatst online: 13:13
Kan je geen tabel/query generen met alle blokken (bijv met de generate_series functie)en dan als volgt joinen:

log.start <= blok.eind
log.eind > blok.start

Je hebt dan alle blokken bij een logregel en kan per blok de gebruikte tijd uitrekenen.

[ Voor 9% gewijzigd door mbe81 op 14-10-2023 20:18 ]


Acties:
  • 0 Henk 'm!

  • Voutloos
  • Registratie: Januari 2002
  • Niet online
Dat werkt. En is veel leesbaarder dan recursive cte met een magische 96.

{signature}


Acties:
  • 0 Henk 'm!

  • mbe81
  • Registratie: Juni 2008
  • Laatst online: 13:13
Voutloos schreef op zaterdag 14 oktober 2023 @ 22:37:
Dat werkt. En is veel leesbaarder dan recursive cte met een magische 96.
Klopt! Ik heb net een werkende versie in PostgeSQL gemaakt. Zal net iets anders zijn maar de query is in basis vrij simpel zo.

@Jay-P Mocht je er niet uitkomen, dan kan ik deze met je delen.

Acties:
  • 0 Henk 'm!

  • Jay-P
  • Registratie: Oktober 2003
  • Laatst online: 00:39
@mbe81 Ben ik zeker in geinterreseerd. Temeer omdat ik inmiddels de Recursive CTE versie aan de praat heb gekregen en benieuwd ben naar performance verschil. Vraag me wel af of en hoe er met de joins rekening gehouden kan worden met een eindtijd en starttijd die op verschillende dagen liggen. Aangezien mijn Time table enkel per dag is zou ik denk ik ook moeten joinen om mijn Date table. In ieder geval hoe ik het opgelost heb:


SQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
CREATE VIEW timesheet.timeLogsBuckets AS
WITH l1 as (SELECT l.id, l.userName , l.[type] , l.intervalsTotal , l.timestampStart, l.timestampStart as timestampStartOrig, l.timestampEnd as timestampEndOrig, l.timestampEnd, l.intervalBlockDayStart , l.intervalBlockDayEnd, intervalStart, intervalEnd  
FROM timesheet.timeLogs l
WHERE l.intervalsTotal = 1)
,multibin_logs AS (
        SELECT l.id, l.userName , l.[type] ,
        CASE WHEN l.intervalsTotal < 0 then (l.intervalsTotal + 96) ELSE l.intervalsTotal END as intervalsTotal,
        DATEADD(MINUTE, -15, DATETIME2FROMPARTS(YEAR(l.timestampEnd), MONTH(l.timestampEnd), DAY(l.timestampEnd), DATEPART(HOUR, l.intervalEnd), (DATEPART(MINUTE, l.intervalEnd)), 0, 0, 0)) as timestampStart,
        l.timestampEnd,
        l.timestampStart as timestampStartOrig, l.timestampEnd as timestampEndOrig,
        l.intervalBlockDayEnd as intervalBlockDayStart , l.intervalBlockDayEnd, intervalStart, intervalEnd  
        FROM timesheet.timeLogs l
        WHERE intervalsTotal <> 1
    UNION ALL
        SELECT mb.id, mb.userName , mb.[type] , mb.intervalsTotal-1 ,
        CASE mb.intervalsTotal WHEN 2 THEN mb.timestampStartOrig
        ELSE DATEADD(MINUTE,(mb.intervalsTotal-2) * 15, DATETIME2FROMPARTS(YEAR(mb.timestampStartOrig), MONTH(mb.timestampStartOrig), DAY(mb.timestampStartOrig), DATEPART(HOUR, mb.intervalStart), DATEPART(MINUTE, mb.intervalStart), 0, 0, 0)) 
        END as timestampStart,
        DATEADD(MINUTE,(mb.intervalsTotal-1) * 15, DATETIME2FROMPARTS(YEAR(mb.timestampStartOrig), MONTH(mb.timestampStartOrig), DAY(mb.timestampStartOrig), DATEPART(HOUR, mb.intervalStart), DATEPART(MINUTE, mb.intervalStart), 0, 0, 0)) as timestampEnd, 
        timestampStartOrig, timestampEndOrig,
              CASE WHEN mb.intervalBlockDayStart = 1 THEN CAST(96 as TINYINT) ELSE CAST(mb.intervalBlockDayStart - 1 as TINYINT) END as intervalBlockDayStart,
              CASE WHEN mb.intervalBlockDayEnd = 1 THEN CAST(96 as TINYINT) ELSE CAST(mb.intervalBlockDayEnd - 1 as TINYINT) END as intervalBlockDayEnd,
              intervalStart, intervalEnd
        FROM multibin_logs mb
        WHERE intervalsTotal > 1
)
SELECT id, userName , [type] , intervalsTotal, timestampStart, timestampEnd, timestampStartOrig, timestampEndOrig, intervalBlockDayStart , intervalBlockDayEnd, intervalStart, intervalEnd  
FROM l1
UNION ALL
SELECT id, userName , [type] , intervalsTotal, timestampStart, timestampEnd, timestampStartOrig, timestampEndOrig, intervalBlockDayStart , intervalBlockDayEnd, intervalStart, intervalEnd  
FROM multibin_logs;


In principe pakt hij nu eerst alle logs die binnen een 15min slot vallen (intervalsTotal=1) en daarna met een recursieve CTE over alle logs gaan die meer dan een interval overspannen.

Bijv bron:

idtypeuserNametimestampEnddurationtimestampStarttimeStarttimeEnd
4651AFKtest@test.com2023-10-01 19:47:00.0004262023-10-01 19:39:53.00019:39:5319:47:00
4653Exittest@test.com2023-10-01 21:01:06.0003122023-10-01 20:55:54.00020:55:5421:01:06
4654AppConnectedtest@test.com2023-10-01 21:04:46.00002023-10-01 21:04:46.00021:04:4621:04:46
4655Exittest@test.com2023-10-01 21:05:08.000222023-10-01 21:04:46.00021:04:4621:05:08
4664AFKtest@test.com2023-10-01 21:16:13.000522023-10-01 21:15:21.00021:15:2121:16:13
4665Exittest@test.com2023-10-01 21:20:41.0002682023-10-01 21:16:13.00021:16:1321:20:41
4668Exittest@test.com2023-10-02 00:09:00.000100282023-10-01 21:21:51.00021:21:5100:09:00



Resultaat:

iduserNametypeintervalsTotaltimestampStarttimestampEndtimestampStartOrigtimestampEndOrigintervalBlockDayStartintervalBlockDayEndintervalStartintervalEnd
4651test@test.comAFK12023-10-01 19:39:53.0002023-10-01 19:45:00.0002023-10-01 19:39:53.0002023-10-01 19:47:00.000797919:30:0020:00:00
4651test@test.comAFK22023-10-01 19:45:00.0002023-10-01 19:47:00.0002023-10-01 19:39:53.0002023-10-01 19:47:00.000808019:30:0020:00:00
4653test@test.comExit12023-10-01 20:55:54.0002023-10-01 21:00:00.0002023-10-01 20:55:54.0002023-10-01 21:01:06.000848420:45:0021:15:00
4653test@test.comExit22023-10-01 21:00:00.0002023-10-01 21:01:06.0002023-10-01 20:55:54.0002023-10-01 21:01:06.000858520:45:0021:15:00
4654test@test.comAppConnected12023-10-01 21:04:46.0002023-10-01 21:04:46.0002023-10-01 21:04:46.0002023-10-01 21:04:46.000858521:00:0021:15:00
4655test@test.comExit12023-10-01 21:04:46.0002023-10-01 21:05:08.0002023-10-01 21:04:46.0002023-10-01 21:05:08.000858521:00:0021:15:00
4664test@test.comAFK12023-10-01 21:15:21.0002023-10-01 21:16:13.0002023-10-01 21:15:21.0002023-10-01 21:16:13.000868621:15:0021:30:00
4665test@test.comExit12023-10-01 21:16:13.0002023-10-01 21:20:41.0002023-10-01 21:16:13.0002023-10-01 21:20:41.000868621:15:0021:30:00
4668test@test.comExit12023-10-01 21:21:51.0002023-10-01 21:30:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000868621:15:0000:15:00
4668test@test.comExit22023-10-01 21:30:00.0002023-10-01 21:45:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000878721:15:0000:15:00
4668test@test.comExit32023-10-01 21:45:00.0002023-10-01 22:00:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000888821:15:0000:15:00
4668test@test.comExit42023-10-01 22:00:00.0002023-10-01 22:15:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000898921:15:0000:15:00
4668test@test.comExit52023-10-01 22:15:00.0002023-10-01 22:30:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000909021:15:0000:15:00
4668test@test.comExit62023-10-01 22:30:00.0002023-10-01 22:45:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000919121:15:0000:15:00
4668test@test.comExit72023-10-01 22:45:00.0002023-10-01 23:00:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000929221:15:0000:15:00
4668test@test.comExit82023-10-01 23:00:00.0002023-10-01 23:15:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000939321:15:0000:15:00
4668test@test.comExit92023-10-01 23:15:00.0002023-10-01 23:30:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000949421:15:0000:15:00
4668test@test.comExit102023-10-01 23:30:00.0002023-10-01 23:45:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000959521:15:0000:15:00
4668test@test.comExit112023-10-01 23:45:00.0002023-10-02 00:00:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.000969621:15:0000:15:00
4668test@test.comExit122023-10-02 00:00:00.0002023-10-02 00:09:00.0002023-10-01 21:21:51.0002023-10-02 00:09:00.0001121:15:0000:15:00

Acties:
  • 0 Henk 'm!

  • mbe81
  • Registratie: Juni 2008
  • Laatst online: 13:13
Hierbij de (PostgreSQL) query die ik heb gemaakt:

SQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
select 
  id,
  type,
  username,
  timestampStart,
  timestampEnd,
  blockEnd,
  blockStart,
  least(timestampEnd, blockEnd) - greatest(timestampStart, blockStart) duration
from (
  select 
    date_trunc('hour', timestampStart) + (q * interval '15 minute') blockStart, 
    date_trunc('hour', timestampStart) + (q + 1) * interval '15 minute' blockEnd,
    t.*
  from timelog t, 
    generate_series(0, 96) as q
) d
where timestampEnd >= blockStart
and timestampStart < blockEnd
order by id, blockStart


Reden dat ik PostgeSQL heb genomen is dat dit de enige database was op SQL Fiddle met support for generate_series die je in de eerste query had zitten. Het rekenen met timestamps zal in T-SQL net even anders moeten.

Hierbij ook de link naar de SQL Fiddle: http://sqlfiddle.com/#!17/6bf30/6
Pagina: 1