SQL-Abfrage Teil der Besuchsdauer von Log-Tabelle zu berechnen
-
06-07-2019 - |
Frage
Ich habe eine Tabelle, die den Benutzer-ID, Kurs anmeldet, sessionid und requestdate jedes Mal eine Webseite geladen wird. Ich möchte die Dauer pro Benutzer-ID für eine gegebene courseid calcuate. Problematisch ist dies durch überlappende Zeitspanne zu tun.
Die hier zur Verfügung gestellten Daten sollten für den Kurs 1 pro Benutzer innerhalb von 10 Minuten Dauer zur Folge hat. Ich kann nicht scheinen, dieses Recht zu erhalten.
CREATE TABLE PageLogSample (
id INT NOT NULL PRIMARY KEY IDENTITY
, userid INT
, courseid INT
, sessionid INT
, requestdate DATETIME
);
TRUNCATE TABLE PageLogSample;
INSERT INTO PageLogSample (userid, courseid, sessionid, requestdate)
-- [0, 10] = 10 minutes
SELECT 1, 1, 1, '00:00:00'
UNION ALL SELECT 1, 1, 1, '00:10:00'
-- [0, 12] - [3, 5] = 10 minutes
-- or ... [0, 3] + [5, 12] = 10 minutes
UNION ALL SELECT 2, 1, 2, '00:00:00'
UNION ALL SELECT 2, 2, 2, '00:03:00'
UNION ALL SELECT 2, 2, 2, '00:05:00'
UNION ALL SELECT 2, 1, 2, '00:12:00'
-- [0, 12] - [3, 5] = 10 minutes
-- or ... [0, 3] + [5, 12] = 10 minutes
UNION ALL SELECT 3, 1, 3, '00:00:00'
UNION ALL SELECT 3, 2, 3, '00:03:00'
UNION ALL SELECT 3, 2, 3, '00:05:00'
UNION ALL SELECT 3, 1, 3, '00:12:00'
UNION ALL SELECT 3, 2, 3, '00:15:00'
-- [1, 13] - [3, 5] = 10 minutes
-- or ... [1, 3] + [5, 13] = 10 minutes
UNION ALL SELECT 4, 2, 4, '00:00:00'
UNION ALL SELECT 4, 1, 4, '00:01:00'
UNION ALL SELECT 4, 2, 4, '00:03:00'
UNION ALL SELECT 4, 2, 4, '00:05:00'
UNION ALL SELECT 4, 1, 4, '00:13:00'
UNION ALL SELECT 4, 2, 4, '00:15:00'
-- [0, 5] + [10, 15] = 10 minutes
UNION ALL SELECT 5, 1, 5, '00:00:00'
UNION ALL SELECT 5, 1, 5, '00:05:00'
UNION ALL SELECT 5, 1, 6, '00:10:00'
UNION ALL SELECT 5, 1, 6, '00:15:00'
-- [0, 10] = 10 minutes (ignoring everything inbetween)
UNION ALL SELECT 6, 1, 7, '00:00:00'
UNION ALL SELECT 6, 1, 7, '00:03:00'
UNION ALL SELECT 6, 1, 7, '00:05:00'
UNION ALL SELECT 6, 1, 7, '00:07:00'
UNION ALL SELECT 6, 1, 7, '00:10:00'
-- [0, 11] - [5, 6] = 10 minutes
-- or ... [0, 3] + [7, 11] = 6 minutes (good)
-- or ... [0, 5] + [7, 11] = 9 minutes (better)
UNION ALL SELECT 7, 1, 8, '00:00:00'
UNION ALL SELECT 7, 1, 8, '00:03:00'
UNION ALL SELECT 7, 2, 8, '00:05:00'
UNION ALL SELECT 7, 2, 8, '00:06:00'
UNION ALL SELECT 7, 1, 8, '00:07:00'
UNION ALL SELECT 7, 1, 8, '00:11:00'
-- [0, 1] + [2, 4] + [5, 7] + [8, 13] = 10
UNION ALL SELECT 8, 1, 9, '00:00:00'
UNION ALL SELECT 8, 2, 9, '00:01:00'
UNION ALL SELECT 8, 1, 9, '00:02:00'
UNION ALL SELECT 8, 1, 9, '00:03:00'
UNION ALL SELECT 8, 2, 9, '00:04:00'
UNION ALL SELECT 8, 1, 9, '00:05:00'
UNION ALL SELECT 8, 1, 9, '00:06:00'
UNION ALL SELECT 8, 2, 9, '00:07:00'
UNION ALL SELECT 8, 1, 9, '00:08:00'
UNION ALL SELECT 8, 1, 9, '00:13:00'
;
erster Versuch, den naiven Ansatzes. Dies gibt Fehler mit überlappenden Teilen der Sitzung.
DECLARE @courseid INT;
SET @courseid = 1;
SELECT subquery.userid
, COUNT(DISTINCT subquery.sessionid) AS sessioncount
, SUM(subquery.duration) AS duration
, CASE SUM(subquery.duration)
WHEN 10 THEN 'ok'
ELSE 'ERROR'
END
FROM (
SELECT userid
, sessionid
, DATEDIFF(MINUTE, MIN(requestdate), MAX(requestdate)) AS duration
FROM PageLogSample
WHERE courseid = @courseid
GROUP BY userid
, sessionid
) subquery
GROUP BY subquery.userid
ORDER BY subquery.userid;
-- userid sessioncount duration
-- 1 1 10 ok
-- 2 1 12 ERROR
-- 3 1 12 ERROR
-- 4 1 12 ERROR
-- 5 2 10 ok
Zweiter Versuch. Vermeiden Sie Überlappungen. Dies funktioniert nur teilweise.
DECLARE @courseid INT;
SET @courseid = 1;
WITH cte (userid, courseid, sessionid, start, finish, duration)
AS (
SELECT userid
, courseid
, sessionid
, MIN(requestdate)
, MAX(requestdate)
, DATEDIFF(MINUTE, MIN(requestdate), MAX(requestdate))
FROM PageLogSample
GROUP BY userid
, courseid
, sessionid
)
SELECT naive.userid
, naive.sessioncount
, naive.duration AS naiveduration
, correction.duration AS correctionduration
, naive.duration - ISNULL(correction.duration, 0) AS duration
, CASE naive.duration - ISNULL(correction.duration, 0)
WHEN 10 THEN 'ok'
ELSE 'ERROR'
END
FROM (
SELECT cte.userid
, COUNT(DISTINCT cte.sessionid) AS sessioncount
, SUM(cte.duration) AS duration
FROM cte
WHERE cte.courseid = @courseid
GROUP BY cte.userid
) naive
LEFT JOIN (
SELECT errors.userid
, SUM(errors.duration) AS duration
FROM cte errors
WHERE errors.courseid <> @courseid
AND EXISTS (
SELECT *
FROM cte
WHERE cte.start <= errors.start
AND cte.finish >= errors.finish
AND cte.courseid = @courseid
)
GROUP BY errors.userid
) correction
ON naive.userid = correction.userid
;
-- userid sessioncount naiveduration correctionduration duration
-- 1 1 10 NULL 10 ok
-- 2 1 12 2 10 ok
-- 3 1 12 NULL 12 ERROR
-- 4 1 12 NULL 12 ERROR
-- 5 2 10 NULL 10 ok
Update: Ed Harpers Kommentar habe mich wirklich meinen Ansatz zu überdenken.
kommt also hier den dritten Versuch. Hier suche ich zuerst für die Reihen einen Einstieg in den Kurs darstellen und welche darstellen jemand zu verlassen. Dann nehme ich die Summe aller endtimes und subtrahieren die Summe aller begintimes. Ich denke, es ist richtiger, wenn auch nicht perfekt.
DECLARE @courseid INT;
SET @courseid = 1;
WITH numberedcte (rn, id, userid, courseid, sessionid, requestdate)
AS (
SELECT ROW_NUMBER() OVER (PARTITION BY sessionid, userid ORDER BY id)
, id
, userid
, courseid
, sessionid
, requestdate
FROM PageLogSample
)
, typedcte (rowtype, id, userid, courseid, sessionid, requestdate, nextrequestdate)
AS (
SELECT CASE
WHEN previousrequest.courseid = nextrequest.courseid
THEN 'between'
WHEN previousrequest.courseid IS NULL
OR nextrequest.courseid = numberedcte.courseid
THEN 'begin'
WHEN nextrequest.courseid IS NULL
OR previousrequest.courseid = numberedcte.courseid
THEN 'end'
ELSE 'error?'
END AS rowtype
, numberedcte.id
, numberedcte.userid
, numberedcte.courseid
, numberedcte.sessionid
, numberedcte.requestdate
, nextrequest.requestdate
FROM numberedcte
LEFT JOIN numberedcte previousrequest
ON previousrequest.userid = numberedcte.userid
AND previousrequest.sessionid = numberedcte.sessionid
AND previousrequest.rn = numberedcte.rn - 1
LEFT JOIN numberedcte nextrequest
ON nextrequest.userid = numberedcte.userid
AND nextrequest.sessionid = numberedcte.sessionid
AND nextrequest.rn = numberedcte.rn + 1
WHERE numberedcte.courseid = @courseid
AND (
nextrequest.courseid = @courseid
OR previousrequest.courseid = @courseid
)
)
, beginsum (userid, value)
AS (
SELECT userid, SUM(DATEPART(MINUTE, requestdate))
FROM typedcte
WHERE rowtype = 'begin'
GROUP BY userid
)
, endsum (userid, value)
AS (
SELECT userid, SUM(DATEPART(MINUTE, ISNULL(nextrequestdate, requestdate)))
FROM typedcte
WHERE rowtype = 'end'
GROUP BY userid
)
SELECT beginsum.userid
, endsum.value - beginsum.value AS duration
FROM beginsum
INNER JOIN endsum
ON beginsum.userid = endsum.userid
;
Das einzige Problem dabei ist, dass ich nur dann ausgegeben, für Benutzer 1 und 5 von meinen ursprünglichen Probendaten. Der hinzugefügte Benutzer 6 gibt auch richtige Ausgabe. Der hinzugefügte Benutzer 7 gibt mir eine zufriedenstellende Ausgabe jetzt. User-8 ist fast perfekt, ich vermisse eine Minute von der ersten Reihe zum zweiten.
-- userid duration
-- 1 10
-- 5 10
-- 6 10
-- 7 9
-- 8 9
Ich fühle mich wie ich Zoll weg von immer dies völlig Recht habe. Die einzige Dauer fehlt, ist von den pagerequests, die nicht in Gruppen geschah. Kann mir jemand helfen, einen Weg zu finden, die einsamen Seitenzugriffe zu bekommen?
Update: Hier kommt ein vierte Versuch. Hier habe ich einen Wert auf jede Anforderung zuordnen und summieren sie. Es gibt nicht ich exactlu den Ausgang ich gehofft, aber sieht aus wie es gut genug sein könnte.
DECLARE @courseid INT;
SET @courseid = 1;
WITH numberedcte (rn, userid, courseid, sessionid, requestdate)
AS (
SELECT ROW_NUMBER() OVER (PARTITION BY sessionid, userid ORDER BY id)
, userid
, courseid
, sessionid
, requestdate
FROM PageLogSample
)
, valuecte (value, userid, courseid, sessionid)
AS (
SELECT CASE
--alone
WHEN ( previousrequest.courseid IS NULL
OR previousrequest.courseid <> numberedcte.courseid
)
AND nextrequest.courseid <> numberedcte.courseid
THEN DATEDIFF(MINUTE, numberedcte.requestdate, nextrequest.requestdate)
--between
WHEN previousrequest.courseid = nextrequest.courseid
THEN 0
--begin
WHEN previousrequest.courseid IS NULL
OR nextrequest.courseid = numberedcte.courseid
THEN -1 * DATEPART(MINUTE, numberedcte.requestdate)
--ignored (end with no next request)
WHEN nextrequest.courseid IS NULL
AND previousrequest.courseid <> numberedcte.courseid
THEN 0
--end
WHEN nextrequest.courseid IS NULL
OR previousrequest.courseid = numberedcte.courseid
THEN DATEPART(MINUTE, ISNULL(nextrequest.requestdate, numberedcte.requestdate))
--impossible?
ELSE 0
END
, numberedcte.userid
, numberedcte.courseid
, numberedcte.sessionid
FROM numberedcte
LEFT JOIN numberedcte previousrequest
ON previousrequest.userid = numberedcte.userid
AND previousrequest.sessionid = numberedcte.sessionid
AND previousrequest.rn = numberedcte.rn - 1
LEFT JOIN numberedcte nextrequest
ON nextrequest.userid = numberedcte.userid
AND nextrequest.sessionid = numberedcte.sessionid
AND nextrequest.rn = numberedcte.rn + 1
WHERE numberedcte.courseid = @courseid
)
SELECT userid
, courseid
, COUNT(DISTINCT sessionid) AS sessioncount
, SUM(value) AS duration
FROM valuecte
GROUP BY userid
, courseid
ORDER BY userid
;
Wie Sie die Ergebnisse sehen können, sind nicht ganz, was ich erwartet hatte.
-- userid courseid sessioncount duration
-- 1 1 1 10
-- 2 1 1 3
-- 3 1 1 6
-- 4 1 1 4
-- 5 1 2 10
-- 6 1 1 10
-- 7 1 1 9
-- 8 1 1 10
Die Leistung ist schrecklich auf meiner lokalen Kopie der echten Datenbank. Also, wenn jemand Ideen, wie dies in einer leistungsfähigere Weise zu schreiben ... schießen.
Update: Die Leistung ist auf. Ich habe einen Index und es funktioniert jetzt einen Reiz.
Lösung 4
Einige weitere Beispieldaten und eine hoffentlich logische Annahme, wie viel Zeit jeder Benutzer in jedem Kurs ausgegeben.
INSERT INTO PageLogSample (userid, courseid, sessionid, requestdate)
-- [0, 10] = 10 minutes
SELECT 1, 1, 1, '00:00:00'
UNION ALL SELECT 1, 1, 1, '00:10:00'
-- [0, 3] = 3 minutes
-- there is no way to know how long the user was on that last page
UNION ALL SELECT 2, 1, 2, '00:00:00'
UNION ALL SELECT 2, 2, 2, '00:03:00'
UNION ALL SELECT 2, 2, 2, '00:05:00'
UNION ALL SELECT 2, 1, 2, '00:12:00'
-- [0, 3] + [12, 15] = 6 minutes
-- the [5, 12] part was spent on a page of course 2
UNION ALL SELECT 3, 1, 3, '00:00:00'
UNION ALL SELECT 3, 2, 3, '00:03:00'
UNION ALL SELECT 3, 2, 3, '00:05:00'
UNION ALL SELECT 3, 1, 3, '00:12:00'
UNION ALL SELECT 3, 2, 3, '00:15:00'
-- [1, 3] + [13, 15] = 4 minutes
UNION ALL SELECT 4, 2, 4, '00:00:00'
UNION ALL SELECT 4, 1, 4, '00:01:00'
UNION ALL SELECT 4, 2, 4, '00:03:00'
UNION ALL SELECT 4, 2, 4, '00:05:00'
UNION ALL SELECT 4, 1, 4, '00:13:00'
UNION ALL SELECT 4, 2, 4, '00:15:00'
-- [0, 5] + [10, 15] = 10 minutes
UNION ALL SELECT 5, 1, 5, '00:00:00'
UNION ALL SELECT 5, 1, 5, '00:05:00'
UNION ALL SELECT 5, 1, 6, '00:10:00'
UNION ALL SELECT 5, 1, 6, '00:15:00'
-- [0, 10] = 10 minutes (ignoring everything inbetween)
UNION ALL SELECT 6, 1, 7, '00:00:00'
UNION ALL SELECT 6, 1, 7, '00:03:00'
UNION ALL SELECT 6, 1, 7, '00:05:00'
UNION ALL SELECT 6, 1, 7, '00:07:00'
UNION ALL SELECT 6, 1, 7, '00:10:00'
-- [0, 5] + [7, 11] = 9 minutes
UNION ALL SELECT 7, 1, 8, '00:00:00'
UNION ALL SELECT 7, 1, 8, '00:03:00'
UNION ALL SELECT 7, 2, 8, '00:05:00'
UNION ALL SELECT 7, 2, 8, '00:06:00'
UNION ALL SELECT 7, 1, 8, '00:07:00'
UNION ALL SELECT 7, 1, 8, '00:11:00'
-- [0, 1] + [2, 4] + [5, 7] + [8, 13] = 10
UNION ALL SELECT 8, 1, 9, '00:00:00'
UNION ALL SELECT 8, 2, 9, '00:01:00'
UNION ALL SELECT 8, 1, 9, '00:02:00'
UNION ALL SELECT 8, 1, 9, '00:03:00'
UNION ALL SELECT 8, 2, 9, '00:04:00'
UNION ALL SELECT 8, 1, 9, '00:05:00'
UNION ALL SELECT 8, 1, 9, '00:06:00'
UNION ALL SELECT 8, 2, 9, '00:07:00'
UNION ALL SELECT 8, 1, 9, '00:08:00'
UNION ALL SELECT 8, 1, 9, '00:13:00'
-- there is nothing we can say about either of there requests
-- 0 minutes
UNION ALL SELECT 9, 1, 10, '00:10:00'
UNION ALL SELECT 9, 1, 11, '00:20:00'
;
Jetzt bekommen wir unsere Daten wie folgt aus:
WITH numberedcte (rn, userid, courseid, sessionid, requestdate)
AS (
SELECT ROW_NUMBER() OVER (PARTITION BY sessionid, userid ORDER BY id)
, userid
, courseid
, sessionid
, requestdate
FROM PageLogSample
)
, valuecte (value, userid, courseid, sessionid)
AS (
SELECT CASE
--alone in session
WHEN previousrequest.courseid IS NULL
AND nextrequest.courseid IS NULL
THEN 0
--alone
WHEN ( previousrequest.courseid IS NULL
OR previousrequest.courseid <> numberedcte.courseid
)
AND nextrequest.courseid <> numberedcte.courseid
THEN DATEDIFF(MINUTE, numberedcte.requestdate, nextrequest.requestdate)
--between
WHEN previousrequest.courseid = nextrequest.courseid
THEN 0
--begin
WHEN previousrequest.courseid IS NULL
OR nextrequest.courseid = numberedcte.courseid
THEN -1 * DATEPART(MINUTE, numberedcte.requestdate)
--ignored (end with no next request)
WHEN nextrequest.courseid IS NULL
AND previousrequest.courseid <> numberedcte.courseid
THEN 0
--end
WHEN nextrequest.courseid IS NULL
OR previousrequest.courseid = numberedcte.courseid
THEN DATEPART(MINUTE, ISNULL(nextrequest.requestdate, numberedcte.requestdate))
--impossible?
ELSE 0
END
, numberedcte.userid
, numberedcte.courseid
, numberedcte.sessionid
FROM numberedcte
LEFT JOIN numberedcte previousrequest
ON previousrequest.userid = numberedcte.userid
AND previousrequest.sessionid = numberedcte.sessionid
AND previousrequest.rn = numberedcte.rn - 1
LEFT JOIN numberedcte nextrequest
ON nextrequest.userid = numberedcte.userid
AND nextrequest.sessionid = numberedcte.sessionid
AND nextrequest.rn = numberedcte.rn + 1
WHERE numberedcte.courseid = @courseid
)
SELECT userid
, courseid
, COUNT(DISTINCT sessionid) AS sessioncount
, SUM(value) AS duration
FROM valuecte
GROUP BY userid
, courseid
ORDER BY userid
;
Dies ist das Ergebnis, das ich bekommen. Ich bin sehr zufrieden mit ihm. Beachten Sie, wie die Sitzungsanzahl bleibt richtig für Benutzer 9.
userid courseid sessioncount duration
1 1 1 10
2 1 1 3
3 1 1 6
4 1 1 4
5 1 2 10
6 1 1 10
7 1 1 9
8 1 1 10
9 1 2 0
Andere Tipps
Sorry, aber ich denke, ein Datenproblem. Mit Blick auf die Abtastdaten Benutzer bereitgestellt 2 ist in courseid 1 für 12 Minuten und courseid 2 für 2 Minuten.
Sind Sie sicher, dass Sie die richtigen Daten geliefert haben?
Das ist so nah wie ich bekommen kann. Es schlägt fehl, für Benutzer-ID 4.
Wie ich in meinem Kommentar sagte requestdate
ist manchmal ein Anfang und manchmal ein Ende eines Kurses, und ich kann nicht eine einfache Regel für die Ableitung, welche Rolle spielt sie auf einer bestimmten Zeile sehen.
DECLARE @courseid INT;
SET @courseid = 1;
WITH orderCTE
AS
(
SELECT *
,ROW_NUMBER() OVER (PARTITION BY sessionid
ORDER BY id
) AS rn
FROM PageLogSample
--order by rn
)
,startendCTE
AS
(
SELECT CASE WHEN start1.rn = 1
THEN start1.courseid
ELSE end1.courseid
END courseid
,start1.sessionid
,start1.userid
,DATEDIFF(mi,start1.requestdate,end1.requestdate) duration
FROM orderCTE AS start1
JOIN orderCTE AS end1
ON end1.rn = start1.rn + 1
AND end1.sessionid = start1.sessionid
)
SELECT courseid
,COUNT(1) sessionCount
,userid
,SUM(duration) totalDuration
FROM startendCTE
WHERE courseid = @courseid
GROUP BY courseid
,userid;
Das ist ziemlich chaotisch, aber es scheint für courseid zu funktionieren 1. ich es nicht mit anderen Kursen versucht haben, so können Sie das testen möchten! : D
Die Grundvoraussetzung ist, dass ich die Dauer der Zeit zwischen der ersten und letzten Sitzung des Ziel courseid bin immer und dann die Dauer von Sitzungen wir subtrahieren, die nicht von dem angegebenen courseid waren, aber wo die Sitzungsanforderung Zeit fiel innerhalb der Min- und max-Anfrage Zeiten des anvisierten courseid. Ich hoffe, das macht Sinn.
Die Abfrage könnte auf jeden Fall gereinigt werden, möglicherweise mit einem CTE oder so etwas. Interessante Frage BTW! :)
DECLARE @courseid INT;
SET @courseid = 1;
SELECT
TargetCourse.UserID,
COUNT(Distinct(TargetCourse.SessionID)) as SessionCount,
SUM(TargetCourse.Duration - Coalesce(OtherCourses.Duration,0)) as Duration
FROM
(
SELECT
TargetCourse.UserID, TargetCourse.SessionID,
MIN(TargetCourse.RequestDate) FirstRequest, MAX(TargetCourse.RequestDate) LastRequest,
DATEDIFF(MINUTE, MIN(TargetCourse.RequestDate), MAX(TargetCourse.RequestDate)) AS duration
FROM
PageLogSample TargetCourse
WHERE
TargetCourse.CourseID = @courseid
GROUP BY
TargetCourse.UserID, TargetCourse.SessionID
) as TargetCourse
LEFT OUTER JOIN
(
SELECT
OtherCourses.UserID, OtherCourses.SessionID,
MIN(OtherCourses.RequestDate) AS FirstRequest, MAX(OtherCourses.RequestDate) AS LastRequest,
DATEDIFF(MINUTE, MIN(OtherCourses.RequestDate), MAX(OtherCourses.RequestDate)) AS duration
FROM
PageLogSample OtherCourses
WHERE
OtherCourses.CourseID <> @courseid AND
OtherCourses.RequestDate between
(Select MIN(RequestDate) From PageLogSample T Where T.UserID = OtherCourses.UserID and T.CourseID = @courseid) AND
(Select MAX(RequestDate) From PageLogSample T Where T.UserID = OtherCourses.UserID and T.CourseID = @courseid)
GROUP BY
OtherCourses.UserID, OtherCourses.SessionID
) as OtherCourses ON
OtherCourses.UserID = TargetCourse.UserID AND
OtherCourses.FirstRequest BETWEEN TargetCourse.FirstRequest and TargetCourse.LastRequest
Group By TargetCourse.UserID
„Die Daten sind korrekt, aber ist nur schwer relevante Bedeutung aus ihm heraus zu bekommen.“
Ich bin gedrückt, dass dies ein Widerspruch von Begriffen zu reagieren. Daten, von denen Sie nicht wissen, was es bedeutet, ist nicht Daten.
Wie für Ihre ursprüngliche Frage:
Was Sie brauchen, ist ein DBMS, die für INTERVAL-Typen anständige Unterstützung bietet. Kein SQL-System spielt in dieser Liga. Abgesehen von ein paar Tutorial-Systeme, mein eigenes DBMS (nicht weiter, dass in diesem Zusammenhang drängen, so dass kein Link) ist die einzige, die ich kenne, die Art der Unterstützung bietet, die wirklich für solche Probleme benötigt wird.
Wenn Sie interessiert sind, dann googeln um für „Intervalltypen“, „verpackt Normalform“, „Zeitdaten“, und Sie werden in sie laufen schließlich.