Vinnaren i pepparkakshustävlingen!
2018-05-15, 09:59
  #1
Medlem
Hej,
Jag har knackat ihop lite fulkod och lösningen fungerar idag.
I databasen har jag en massa timestamps.
Ibland så kan "startdate" vara tex. 11:59 medans "enddate" 12:01.
Kan man få ut att 11:e timmen har 1 minut samt att den 12:e timmen har 1 minut?

Idag så har jag ett PHP-script som bearbetar tabellen med jämna mellanrum.
Däremot så känns det som att det borde finnas en smidigare/bättre lösning.

Så här ser det ut i tabellen, kolumnerna är fetmarkerade.
Citat:
{"id":"68","machid":"1","startdate":"2018-02-23 12:09:44","enddate":"2018-02-23 12:11:44","type":"1"},
{"id":"69","machid":"1","startdate":"2018-02-23 12:11:44","enddate":"2018-02-23 12:13:44","type":"1"},
Citera
2018-05-15, 12:39
  #2
Moderator
Protons avatar
Citat:
Ursprungligen postat av [BPS]
Hej,
Jag har knackat ihop lite fulkod och lösningen fungerar idag.
I databasen har jag en massa timestamps.
Ibland så kan "startdate" vara tex. 11:59 medans "enddate" 12:01.
Kan man få ut att 11:e timmen har 1 minut samt att den 12:e timmen har 1 minut?

Idag så har jag ett PHP-script som bearbetar tabellen med jämna mellanrum.
Däremot så känns det som att det borde finnas en smidigare/bättre lösning.

Så här ser det ut i tabellen, kolumnerna är fetmarkerade.
Vilket DBMS är det? MySQL?

Låter som om det skulle kunna bli ganska joxigt dock.

Kod:
SELECT id,machid,startDate,endDate,IF(HOUR(startDate <> HOUR(endDate),60-MINUTES(startDate),NULL) AS minutesInStartDate,IF(HOUR(startDate <> HOUR(endDate),60-MINUTES(endDate),NULL AS minutesEndDate,type FROM MyTable

Känns dock som om man kan göra det bättre än detta, men nåt sånt kanske du kan börja med?
Citera
2018-05-15, 13:29
  #3
Medlem
kodsnickrarns avatar
Lite enklare blir om du först skapar en tabell med alla möjliga timmar som du sen kan "joina" mot.

Kod:
create table Hours(hour int);
insert into Hours(hour)
select 0
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9
union all select 10
union all select 11
union all select 12
union all select 13
union all select 14
union all select 15
union all select 16
union all select 17
union all select 18
union all select 19
union all select 20
union all select 21
union all select 22
union all select 23;

Sen kan du köra nåt liknande:
(antar att "din" tabell heter "FanVadDumtAttJagIntePostadeTabellSchemat" )

Kod:
    SELECT id, 
           Hours.hour AS Hour,
           CASE WHEN Hours.hour = DATE_PART('hour', startDate) 
                     AND Hours.hour = DATE_PART('hour', enddate) 
                     THEN DATE_PART('minute', enddate)-DATE_PART('minute', startDate)
                     WHEN Hours.hour = DATE_PART('hour', startDate) 
                     THEN 60 - DATE_PART('minute', startDate)
                     WHEN Hours.hour = DATE_PART('hour', enddate) 
                     THEN DATE_PART('minute', enddate)
                     WHEN Hours.hour > DATE_PART('hour', startDate) 
                     AND Hours.hour < DATE_PART('hour', enddate) 
                     THEN 60
                ELSE 0
           END AS Duration
    FROM  Hours
    LEFT JOIN "FanVadDumtAttJagIntePostadeTabellSchemat" ON 1=1

Nu kommer du dock få med alla timmar, även de som inte har någon tid registrerad, och då blir Duration = 0.

Vill du sen summera och gruppera på timmen så gör det i en "yttre" select som använder select:en ovan som en "derived table" (googla)

Edit: syntaxen ovan funkar i PostgreSQL, ingen aning om MySQL

/K
__________________
Senast redigerad av kodsnickrarn 2018-05-15 kl. 13:40.
Citera
2018-05-15, 16:46
  #4
Moderator
Protons avatar
Citat:
Ursprungligen postat av kodsnickrarn
Lite enklare blir om du först skapar en tabell med alla möjliga timmar som du sen kan "joina" mot.

Kod:
create table Hours(hour int);
insert into Hours(hour)
select 0
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9
union all select 10
union all select 11
union all select 12
union all select 13
union all select 14
union all select 15
union all select 16
union all select 17
union all select 18
union all select 19
union all select 20
union all select 21
union all select 22
union all select 23;

Sen kan du köra nåt liknande:
(antar att "din" tabell heter "FanVadDumtAttJagIntePostadeTabellSchemat" )

Kod:
    SELECT id, 
           Hours.hour AS Hour,
           CASE WHEN Hours.hour = DATE_PART('hour', startDate) 
                     AND Hours.hour = DATE_PART('hour', enddate) 
                     THEN DATE_PART('minute', enddate)-DATE_PART('minute', startDate)
                     WHEN Hours.hour = DATE_PART('hour', startDate) 
                     THEN 60 - DATE_PART('minute', startDate)
                     WHEN Hours.hour = DATE_PART('hour', enddate) 
                     THEN DATE_PART('minute', enddate)
                     WHEN Hours.hour > DATE_PART('hour', startDate) 
                     AND Hours.hour < DATE_PART('hour', enddate) 
                     THEN 60
                ELSE 0
           END AS Duration
    FROM  Hours
    LEFT JOIN "FanVadDumtAttJagIntePostadeTabellSchemat" ON 1=1

Nu kommer du dock få med alla timmar, även de som inte har någon tid registrerad, och då blir Duration = 0.

Vill du sen summera och gruppera på timmen så gör det i en "yttre" select som använder select:en ovan som en "derived table" (googla)

Edit: syntaxen ovan funkar i PostgreSQL, ingen aning om MySQL

/K
Ah, en nummertabell, att jag inte tänkte på det. Förbluffande användbara de där.

SELECT CASE funkar i sql server och MySQL med mig veterligen, så den där ska man ju kunna använda mer eller mindre oberoende av vilket DBMS det är.
Citera
2018-05-15, 16:52
  #5
Medlem
kodsnickrarns avatar
Nu förutsätts att startdate/neddate alltid kommer vara inom samma datum, kan de spänna över
längre tidsperioder (eller bara 23:59 - 00:01) får man börja summera och gruppera på datumet.

/K
Citera
2018-11-12, 22:50
  #6
Medlem
Citat:
Ursprungligen postat av [BPS]
Hej,
Jag har knackat ihop lite fulkod och lösningen fungerar idag.
I databasen har jag en massa timestamps.
Ibland så kan "startdate" vara tex. 11:59 medans "enddate" 12:01.
Kan man få ut att 11:e timmen har 1 minut samt att den 12:e timmen har 1 minut?

Idag så har jag ett PHP-script som bearbetar tabellen med jämna mellanrum.
Däremot så känns det som att det borde finnas en smidigare/bättre lösning.

Så här ser det ut i tabellen, kolumnerna är fetmarkerade.

Glöm inte att spara tidszonen också.
Spontant känns sån här gruppering som något man skulle vilja lösa i presentationslagret istället. Vad är poängen med att göra det i databasanropet?
Citera
2018-12-21, 22:28
  #7
Medlem
Kod:
DECLARE @json varchar(max) = 
'{"rows":[
   {"id":"68","machid":"1","startdate":"2018-02-23 12:09:44","enddate":"2018-02-23 12:11:44","type":"1"},
   {"id":"69","machid":"2","startdate":"2018-02-23 12:11:44","enddate":"2018-02-23 12:13:44","type":"1"},
   {"id":"70","machid":"3","startdate":"2018-02-23 23:58:13","enddate":"2018-02-24 00:12:13","type":"1"}
]}';
WITH 
input AS (
SELECT * FROM OPENJSON(@json, '$.rows')
WITH (
id int,
machid int,
startdate datetime2(0),
enddate datetime2(0),
type int
)
),
a3 (col) AS (SELECT NULL UNION ALL SELECT NULL UNION ALL SELECT NULL),
nums (num) AS (SELECT row_number() OVER(ORDER BY (SELECT NULL)) - 1 FROM a3 x CROSS JOIN a3 y CROSS JOIN a3 z),
hours (hour) AS (SELECT cast(TIMEFROMPARTS(num, 0, 0, 0, 0) as time(0)) FROM nums WHERE num < 24),
calculate AS (
SELECT 
	i.*, sh.hour AS starthour, eh.hour AS endhour,
	CAST(i.startdate as date) AS StartingDate,
	CAST(i.enddate as date) AS EndingDate,
	CASE
		WHEN sh.hour = eh.hour THEN DATEDIFF(second, i.startdate, i.enddate)
		ELSE 3600 - DATEDIFF(second, sh.hour, CAST(i.startdate as time(0)))
	END ConsumedStartHour,
	CASE
		WHEN sh.hour != eh.hour THEN DATEDIFF(second, eh.hour, CAST(i.enddate as time(0)))
		ELSE 0
	END ConsumedEndHour
FROM HOURS sh
INNER JOIN input i ON DATEPART(HOUR,i.startdate) = DATEPART(HOUR,sh.hour)
INNER JOIN hours eh ON DATEPART(HOUR,i.enddate) = DATEPART(HOUR,eh.hour)
),
consumtion (machid, date, hour, seconds) AS (
SELECT machid, StartingDate, starthour, ConsumedStartHour FROM calculate
UNION ALL
SELECT machid, EndingDate, endhour, ConsumedEndHour FROM calculate WHERE ConsumedEndHour > 0
)
SELECT 
	machid, date, hour, SUM(seconds) AS SekunderNyttjat, 
	DATEADD(second, 0 - SUM(seconds), CAST('01:00:00' as time(0))) AS OutnyttjadTid
FROM consumtion GROUP BY machid, date, hour

Kod:
Resultat:
machid	date			hour		SekunderNyttjat		OutnyttjadTid
1		2018-02-23	12:00:00	120				00:58:00
2		2018-02-23	12:00:00	120				00:58:00
3		2018-02-23	23:00:00	107				00:58:13
3		2018-02-24	00:00:00	733				00:47:47


Jäkligt rolig övning...


* SQL Server fixar numera JSON.
* Kolla in CTE a3 och nums, lite magi med NULL.
* UNION ALL är dödsviktig, enbart UNION tar bort dubletter, och det vill vi inte riskera.
* Göra detta direkt i databasen istället för applikationskod blir kanske vettigt om det rör sig om mer än två, tre rader. Koden ovan grejar lätt en hel fabrik, och säkert ett helt år utan att bli jobbig.
__________________
Senast redigerad av thesqlguru 2018-12-21 kl. 22:40.
Citera

Stöd Flashback

Flashback finansieras genom donationer från våra medlemmar och besökare. Det är med hjälp av dig vi kan fortsätta erbjuda en fri samhällsdebatt. Tack för ditt stöd!

Stöd Flashback