consulta SQL para parte calcular de visita duração da tabela log
-
06-07-2019 - |
Pergunta
Eu tenho uma tabela que registra o ID de usuário, é claro, sessionid e requestdate cada vez que uma página é carregada. Quero calcuate a duração per ID de usuário para um determinado courseid. É problemático para fazer isso devido à sobreposição de intervalos de tempo.
Os dados fornecidos aqui deve resultar em 10 minutos de duração por usuário para o curso 1. Eu não consigo obter este direito.
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'
;
primeiro tentar a abordagem ingênua. Isto dá erros com sobreposição de partes da sessão.
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
Segundo tentativa. Evitar sobreposições. Isso só funciona parcialmente.
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: comentário Ed Harpers Realmente me fez repensar a minha abordagem.
Então, aqui vem o terceiro julgamento. Aqui eu primeiro procurar quais linhas representam uma entrada para o curso e que representam alguém sair. Então eu tomar a soma de todas as endtimes e subtrair a soma de todos begintimes. Eu acho que é mais correto, embora não seja perfeito.
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
;
O único problema aqui é que eu só obter uma saída para o usuário 1 e 5 de meus dados amostra original. O utilizador adicionado 6 também dá de saída correcto. O usuário adicionado 7 me dá uma saída satisfatória agora. Usuário 8 é quase perfeito, eu perca um minuto da primeira linha para a segunda.
-- userid duration
-- 1 10
-- 5 10
-- 6 10
-- 7 9
-- 8 9
Eu sinto que estou polegadas longe de conseguir isso completamente certo. Os únicos períodos que faltam são dos pagerequests que não aconteceram em grupos. Alguém pode me ajudar a encontrar uma maneira de obter os pageviews solitários?
Update: Aí vem um quarto ensaio. Aqui eu atribuir um valor a cada pedido e resumi-los. Ele não dá me exactlu a saída que eu esperava, mas parece que poderia ser bom o suficiente.
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
;
Como você pode ver os resultados não são inteiramente o que eu esperava.
-- 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
O desempenho é horrível na minha cópia local do banco de dados real. Então, se alguém tem idéias como escrever isso de uma maneira mais performance ... shoot.
Update: Desempenho é para cima. Eu adicionei um índice e trabalha um encanto agora.
Solução 4
Alguns dados de mais amostras e uma suposição espero lógica de quanto tempo cada usuário gastou em cada curso.
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'
;
Agora temos nossos dados como este:
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
;
Este é o resultado que eu recebo. Estou muito satisfeito com ele. Observe como os restos de contagem de sessão de corrigir para usuário 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
Outras dicas
Desculpe, mas eu acho que você tem um problema de dados. Olhando para os dados de amostra fornecidos utilizador 2 é em courseid 1 durante 12 minutos e CourseID 2 durante 2 minutos.
Você tem certeza de ter fornecido os dados corretos?
Este é o mais perto que eu posso começar. Ele falha para UserID 4.
Como eu disse no meu comentário, requestdate
às vezes é um começo e, por vezes, um fim de um curso, e eu não posso ver uma regra geral simples para derivar o papel que desempenha em uma determinada linha.
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;
Este é bastante confuso, mas parece estar a trabalhar para CourseID 1. Eu não experimentá-lo com outros cursos, então você pode querer testar isso! : D
A premissa básica é que eu estou recebendo o período de tempo entre a primeira ea última sessão do CourseID alvo e, em seguida, eu estou subtraindo a duração de todas as sessões que não eram do CourseID especificado, mas onde a solicitação de sessão tempo entraram dentro das mínimo e máximo de solicitação vezes do CourseID alvo. Espero que isso faz sentido.
A consulta pode definitivamente ser limpo, possivelmente com um CTE ou algo assim. Pergunta interessante 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
"Os dados está correto, mas está é apenas difícil conseguir significado relevante fora dele."
Eu estou pressionado a responder que esta uma contradição de termos. Os dados dos quais você não sabe o que significa não é de dados.
Quanto à sua pergunta original:
O que você precisa é um DBMS que oferece suporte decente para tipos de intervalo. Nenhum sistema SQL desempenha nessa liga. Para além de alguns sistemas tutoriais, meus próprios DBMS (não mais empurrando que, neste contexto, de modo nenhum link) é o único que eu conheço que oferece o tipo de apoio que é realmente necessário para tais problemas.
Se você estiver interessado, google em torno de "tipos de intervalo", "embalado forma normal", "dados temporais" e você vai correr para ele eventualmente.