Analizando las estadísticas del sitio, obtenemos una idea de lo que está sucediendo con él. Comparamos los resultados con otros conocimientos sobre el producto o servicio y así mejoramos nuestra experiencia.
Cuando se ha completado el análisis de los primeros resultados, se reflexiona sobre la información y se sacan conclusiones, comienza la siguiente etapa. Surgen ideas: ¿qué pasaría si miramos los datos desde otro ángulo?
En esta etapa hay limitaciones en las herramientas de análisis. Esta es una de las razones por las que la herramienta Google Analytics no me resultó suficiente, debido a la capacidad limitada para ver y manipular mis datos.
Siempre he querido cargar rápidamente los datos básicos (metadatos), agregar otro nivel de agregación o interpretar de otra manera los valores existentes.
Esto es fácil de hacer en basado en el archivo access.log y para ello es suficiente con el lenguaje SQL.
Entonces, ¿qué preguntas quería responder?
Qué y cuándo se cambió en el sitio
La historia de los cambios en los datos básicos (metadatos) siempre es interesante.

Consulta SQL del informe
SELECT
1 as 'SideStackedBar: Actualizaciones de Contenido por Meses',
strftime('%m/%Y', datetime(UPDATE_DT, 'unixepoch')) AS 'Día',
COUNT(CASE WHEN PAGE_TITLE != 'n.a.' THEN DIM_REQUEST_ID END) AS 'Actualizaciones de páginas web',
COUNT(CASE WHEN PAGE_DESCR = 'IMAGES' THEN DIM_REQUEST_ID END) AS 'Cargas de imágenes',
COUNT(CASE WHEN PAGE_DESCR = 'VIDEO' THEN DIM_REQUEST_ID END) AS 'Cargas de videos',
COUNT(CASE WHEN PAGE_DESCR = 'AUDIO' THEN DIM_REQUEST_ID END) AS 'Cargas de audio'
FROM DIM_REQUEST
WHERE PAGE_TITLE != 'n.a.' OR PAGE_DESCR != 'n.a.'
GROUP BY strftime('%m/%Y', datetime(UPDATE_DT, 'unixepoch'))
ORDER BY UPDATE_DTPor ejemplo, en algún momento se llevó a cabo una optimización de búsqueda o se añadió nuevo contenido al sitio, por lo tanto, se espera un aumento en el tráfico.
Grupos de usuarios
El ejemplo más sencillo de un grupo puede ser un agente de usuario o el nombre del sistema operativo.
La medición de los agentes de usuario ha acumulado alrededor de mil registros y me interesaba ver la dinámica de distribución de los agentes dentro del grupo.

Consulta SQL del informe
SELECT
1 AS 'SideStackedBar: Agentes de Usuarios',
AGENT_OS AS 'OS',
SUM(CASE WHEN AGENT_BOT = 'n.a.' THEN 1 ELSE 0 END ) AS 'Agente de Usuario de Usuarios',
SUM(CASE WHEN AGENT_BOT != 'n.a.' THEN 1 ELSE 0 END ) AS 'Agente de Usuario de Bots'
FROM DIM_USER_AGENT
WHERE DIM_USER_AGENT_ID != -1
GROUP BY AGENT_OS
ORDER BY 3 DESCLa mayor cantidad de combinaciones de agentes provienen del mundo de Windows. Entre los indefinidos se encuentran opciones como WhatsApp, PocketImageCache, PlayStation, SmartTV, etc.
Actividad de grupos de usuarios por semanas
Al combinar algunos grupos, se puede observar la distribución de su actividad.
Por ejemplo, los usuarios del clúster de Linux consumen más tráfico en el sitio que los demás.

Consulta SQL del informe
SELECT
1 as 'StackedBar: Volumen de Tráfico por SO de Usuario y por Semana',
strftime('%W semana', datetime(FCT.EVENT_DT, 'unixepoch')) AS 'Semana',
SUM(CASE WHEN USG.AGENT_OS IN ('Android', 'Linux') THEN FCT.BYTES ELSE 0 END) / 1000 AS 'Usuarios Android/Linux',
SUM(CASE WHEN USG.AGENT_OS IN ('Windows') THEN FCT.BYTES ELSE 0 END) / 1000 AS 'Usuarios Windows',
SUM(CASE WHEN USG.AGENT_OS IN ('Macintosh', 'iOS') THEN FCT.BYTES ELSE 0 END) / 1000 AS 'Usuarios Mac/iOS',
SUM(CASE WHEN USG.AGENT_OS IN ('n.a.', 'BlackBerry') THEN FCT.BYTES ELSE 0 END) / 1000 AS 'Otros'
FROM
FCT_ACCESS_USER_AGENT_DD FCT,
DIM_USER_AGENT USG,
DIM_HTTP_STATUS HST
WHERE FCT.DIM_USER_AGENT_ID = USG.DIM_USER_AGENT_ID
AND FCT.DIM_HTTP_STATUS_ID = HST.DIM_HTTP_STATUS_ID
AND USG.AGENT_BOT = 'n.a.' /* solo usuarios */
AND HST.STATUS_GROUP IN ('Exitoso') /* buenas páginas */
AND datetime(FCT.EVENT_DT, 'unixepoch') > date('now', '-3 month')
GROUP BY strftime('%W semana', datetime(FCT.EVENT_DT, 'unixepoch'))
ORDER BY FCT.EVENT_DTConsumo intensivo de tráfico
La tabla muestra los grupos de usuarios más activos y los días de su actividad.
Los más activos pertenecen al clúster de Linux.

Consulta SQL del informe
SELECT
1 AS 'Tabla: Agente de Usuario con Uso Alto',
strftime('%d.%m.%Y', datetime(FCT.EVENT_DT, 'unixepoch')) AS 'Día',
ROUND(1.0 * SUM(FCT.BYTES) / 1000000, 1) AS 'Tráfico MB',
ROUND(1.0 * SUM(FCT.IP_CNT) / SUM(1), 1) AS 'IPs',
ROUND(1.0 * SUM(FCT.REQUEST_CNT) / SUM(1), 1) AS 'Solicitudes',
USA.DIM_USER_AGENT_ID AS 'ID',
MAX(USA.USER_AGENT_NK) AS 'Agente de Usuario',
MAX(USA.AGENT_BOT) AS 'Bot'
FROM
FCT_ACCESS_USER_AGENT_DD FCT,
DIM_USER_AGENT USA
WHERE FCT.DIM_USER_AGENT_ID = USA.DIM_USER_AGENT_ID
AND datetime(FCT.EVENT_DT, 'unixepoch') >= date('now', '-30 day')
GROUP BY USA.DIM_USER_AGENT_ID, strftime('%d.%m.%Y', datetime(FCT.EVENT_DT, 'unixepoch'))
ORDER BY SUM(FCT.BYTES) DESC, FCT.EVENT_DT
LIMIT 10Utilizando los atributos día e ID de agente, se puede encontrar y rastrear rápidamente las estadísticas por días de grupos de usuarios específicos. Si es necesario, se puede encontrar rápidamente información detallada en la tabla de etapa.
¿Cómo obtener información?
se puede hacer aún más eficiente si se integran fuentes de datos adicionales, se introducen nuevos niveles de agregación y agrupación.
Datos y entidades básicas
Los datos básicos incluyen información sobre las entidades: páginas web, imágenes, contenido de vídeo y audio, en el caso de una tienda, productos.
Las entidades en sí actúan como dimensiones, y el proceso de guardar los cambios de atributos se llama historización. En la base de datos, este proceso a menudo se realiza en forma de dimensiones que cambian lentamente (SCD).
Las fuentes de datos básicas pueden ser sistemas muy diversos, por lo tanto, casi siempre es necesario integrarlos.
Dimensión de cambio lento
La dimensión DIM_REQUEST contendrá información sobre las solicitudes en el sitio en forma histórica.
Tabla SCD2
CREAR TABLA DIM_REQUEST (
DIM_REQUEST_ID ENTERO NO NULO CLAVE PRINCIPAL AUTOINCREMENT,
DIM_REQUEST_ID_HIST ENTERO NO NULO POR DEFECTO -1,
REQUEST_NK TEXTO NO NULO POR DEFECTO 'n.a.',
PAGE_TITLE TEXTO NO NULO POR DEFECTO 'n.a.',
PAGE_DESCR TEXTO NO NULO POR DEFECTO 'n.a.',
PAGE_KEYWORDS TEXTO NO NULO POR DEFECTO 'n.a.',
DELETE_FLAG ENTERO NO NULO POR DEFECTO 0,
UPDATE_DT ENTERO NO NULO POR DEFECTO 0,
ÚNICO (REQUEST_NK, DIM_REQUEST_ID_HIST)
);
INSERTAR EN DIM_REQUEST (DIM_REQUEST_ID) VALORES (-1);Además, crearemos una vista que siempre muestre todos los registros en su último estado. Esto es necesario para cargar la propia dimensión.

Vista actual SCD2
/* Content: actual view on scd table */
SELECT HI.DIM_REQUEST_ID,
HI.DIM_REQUEST_ID_HIST,
HI.REQUEST_NK,
HI.PAGE_TITLE,
HI.PAGE_DESCR,
HI.PAGE_KEYWORDS,
NK.CNT AS HIST_CNT,
HI.DELETE_FLAG,
strftime('%d.%m.%Y %H:%M', datetime(HI.UPDATE_DT, 'unixepoch')) AS UPDATE_DT
FROM
( SELECT REQUEST_NK, MAX(DIM_REQUEST_ID) AS DIM_REQUEST_ID, SUM(1) AS CNT
FROM DIM_REQUEST
GROUP BY REQUEST_NK
) NK,
DIM_REQUEST HI
WHERE 1 = 1
AND NK.REQUEST_NK = HI.REQUEST_NK
AND NK.DIM_REQUEST_ID = HI.DIM_REQUEST_ID;Y una vista donde se recopila información histórica para cada registro. Esto es necesario para construir una relación históricamente precisa con los hechos.

Vista histórica SCD2
/* Content: actual view on scd table */
SELECT SCD.DIM_REQUEST_ID,
SCD.DIM_REQUEST_ID_HIST,
SCD.REQUEST_NK,
SCD.PAGE_TITLE,
SCD.PAGE_DESCR,
SCD.PAGE_KEYWORDS,
SCD.DELETE_FLAG,
CASE
WHEN HIS.UPDATE_DT IS NULL
THEN 1
ELSE 0 END ACTIVE_FLAG,
SCD.DIM_REQUEST_ID_HIST AS ID_FROM,
SCD.DIM_REQUEST_ID AS ID_TO,
CASE
WHEN SCD.DIM_REQUEST_ID_HIST=-1
THEN 3600
ELSE IFNULL(SCD.UPDATE_DT,3600)
END AS TIME_FROM,
CASE
WHEN HIS.UPDATE_DT IS NULL
THEN 253370764800
ELSE HIS.UPDATE_DT
END AS TIME_TO,
CASE
WHEN SCD.DIM_REQUEST_ID_HIST=-1
THEN STRFTIME('%d.%m.%Y %H:%M', DATETIME(3600, 'unixepoch'))
ELSE STRFTIME('%d.%m.%Y %H:%M', DATETIME(IFNULL(SCD.UPDATE_DT,3600), 'unixepoch'))
END AS ACTIVE_FROM,
CASE
WHEN HIS.UPDATE_DT IS NULL
THEN STRFTIME('%d.%m.%Y %H:%M', DATETIME(253370764800, 'unixepoch'))
ELSE STRFTIME('%d.%m.%Y %H:%M', DATETIME(HIS.UPDATE_DT, 'unixepoch'))
END AS ACTIVE_TO
FROM
DIM_REQUEST SCD
LEFT OUTER JOIN DIM_REQUEST HIS
ON SCD.REQUEST_NK = HIS.REQUEST_NK AND SCD.DIM_REQUEST_ID = HIS.DIM_REQUEST_ID_HIST;Agregación de datos
La agregación permite evaluar los datos a un nivel más alto y detectar anomalías y tendencias que no son visibles en los informes detallados.
Por ejemplo, en la dimensión con códigos de estado de solicitudes DIM_HTTP_STATUS agregaremos el grupo:
ESTADO / GRUPO
0xx / n.a.
1xx / Informativo
2xx / Exitoso
3xx / Redirección
4xx / Error del Cliente
5xx / Error del Servidor
La dimensión de agentes de usuario DIM_USER_AGENT contendrá los atributos AGENT_OS y AGENT_BOT, que corresponden a los grupos. Estos se pueden rellenar durante el proceso ETL:
Carga de DIM_USER_AGENT
/* Propagate the user agent from access log */
INSERT INTO DIM_USER_AGENT (USER_AGENT_NK, AGENT_OS, AGENT_ENGINE, AGENT_DEVICE, AGENT_BOT, UPDATE_DT)
WITH CLS AS (
SELECT BROWSER
FROM STG_ACCESS_LOG WHERE LENGTH(BROWSER)>1
GROUP BY BROWSER
)
SELECT
CLS.BROWSER AS USER_AGENT_NK,
CASE
WHEN INSTR(CLS.BROWSER,'Macintosh')>0
THEN 'Macintosh'
WHEN INSTR(CLS.BROWSER,'iPhone')>0
OR INSTR(CLS.BROWSER,'iPad')>0
OR INSTR(CLS.BROWSER,'iPod')>0
OR INSTR(CLS.BROWSER,'Apple TV')>0
OR INSTR(CLS.BROWSER,'Darwin')>0
THEN 'iOS'
WHEN INSTR(CLS.BROWSER,'Android')>0
THEN 'Android'
WHEN INSTR(CLS.BROWSER,'X11;')>0 OR INSTR(CLS.BROWSER,'Wayland;')>0 OR INSTR(CLS.BROWSER,'linux-gnu')>0
THEN 'Linux'
WHEN INSTR(CLS.BROWSER,'BB10;')>0 OR INSTR(CLS.BROWSER,'BlackBerry')>0
THEN 'BlackBerry'
WHEN INSTR(CLS.BROWSER,'Windows')>0
THEN 'Windows'
ELSE 'n.a.' END AS AGENT_OS, -- OS
CASE
WHEN INSTR(CLS.BROWSER,'AppleCoreMedia')>0
THEN 'AppleWebKit'
WHEN INSTR(CLS.BROWSER,') ')>1 AND LENGTH(CLS.BROWSER)>INSTR(CLS.BROWSER,') ')
THEN COALESCE(SUBSTR(CLS.BROWSER, INSTR(CLS.BROWSER,') ')+2, LENGTH(CLS.BROWSER) - INSTR(CLS.BROWSER,') ')-1), 'N/A')
ELSE 'n.a.' END AS AGENT_ENGINE, -- Engine
CASE
WHEN INSTR(CLS.BROWSER,'iPhone')>0
THEN 'iPhone'
WHEN INSTR(CLS.BROWSER,'iPad')>0
THEN 'iPad'
WHEN INSTR(CLS.BROWSER,'iPod')>0
THEN 'iPod'
WHEN INSTR(CLS.BROWSER,'Apple TV')>0
THEN 'Apple TV'
WHEN INSTR(CLS.BROWSER,'Android ')>0 AND INSTR(CLS.BROWSER,'Build')>0
THEN COALESCE(SUBSTR(CLS.BROWSER, INSTR(CLS.BROWSER,'Android '), INSTR(CLS.BROWSER,'Build')-INSTR(CLS.BROWSER,'Android ')), 'n.a.')
WHEN INSTR(CLS.BROWSER,'Android ')>0 AND INSTR(CLS.BROWSER,'MIUI')>0
THEN COALESCE(SUBSTR(CLS.BROWSER, INSTR(CLS.BROWSER,'Android '), INSTR(CLS.BROWSER,'MIUI')-INSTR(CLS.BROWSER,'Android ')), 'n.a.')
ELSE 'n.a.' END AS AGENT_DEVICE, -- Device
CASE
WHEN INSTR(LOWER(CLS.BROWSER),'yandex.com')>0
THEN 'yandex'
WHEN INSTR(LOWER(CLS.BROWSER),'googlebot')>0
THEN 'google'
WHEN INSTR(LOWER(CLS.BROWSER),'bingbot')>0
THEN 'microsoft'
WHEN INSTR(LOWER(CLS.BROWSER),'ahrefsbot')>0
THEN 'ahrefs'
WHEN INSTR(LOWER(CLS.BROWSER),'jobboersebot')>0 OR INSTR(LOWER(CLS.BROWSER),'jobkicks')>0
THEN 'job.de'
WHEN INSTR(LOWER(CLS.BROWSER),'mail.ru')>0
THEN 'mail.ru'
WHEN INSTR(LOWER(CLS.BROWSER),'baiduspider')>0
THEN 'baidu'
WHEN INSTR(LOWER(CLS.BROWSER),'mj12bot')>0
THEN 'majestic-12'
WHEN INSTR(LOWER(CLS.BROWSER),'duckduckgo')>0
THEN 'duckduckgo'
WHEN INSTR(LOWER(CLS.BROWSER),'bytespider')>0
THEN 'bytespider'
WHEN INSTR(LOWER(CLS.BROWSER),'360spider')>0
THEN 'so.360.cn'
WHEN INSTR(LOWER(CLS.BROWSER),'compatible')>0 OR INSTR(LOWER(CLS.BROWSER),'http')>0
OR INSTR(LOWER(CLS.BROWSER),'libwww')>0 OR INSTR(LOWER(CLS.BROWSER),'spider')>0
OR INSTR(LOWER(CLS.BROWSER),'java')>0 OR INSTR(LOWER(CLS.BROWSER),'python')>0
OR INSTR(LOWER(CLS.BROWSER),'robot')>0 OR INSTR(LOWER(CLS.BROWSER),'curl')>0 OR INSTR(LOWER(CLS.BROWSER),'wget')>0
THEN 'other'
ELSE 'n.a.' END AS AGENT_BOT, -- Bot
STRFTIME('%s','now') AS UPDATE_DT
FROM CLS
LEFT OUTER JOIN DIM_USER_AGENT TRG
ON CLS.BROWSER = TRG.USER_AGENT_NK
WHERE TRG.DIM_USER_AGENT_ID IS NULLIntegración de datos
Incluye la organización de la transferencia de datos desde el sistema operativo al informe. Para ello, es necesario crear una tabla de etapa con una estructura similar a la del origen.
En la etapa, la información sobre las páginas web proviene de una copia de seguridad del CMS en forma de consultas de inserción.
La carga de la tabla histórica DIM_REQUEST con datos básicos se realiza en tres etapas: carga de nuevas claves y atributos, actualización de existentes y registro de registros eliminados.
Carga de nuevos registros SCD2
/* Load request table SCD from master data */
INSERT INTO DIM_REQUEST (DIM_REQUEST_ID_HIST, REQUEST_NK, PAGE_TITLE, PAGE_DESCR, PAGE_KEYWORDS, DELETE_FLAG, UPDATE_DT)
WITH CLS AS ( -- prepare keys
SELECT
'/' || NAME AS REQUEST_NK,
TITLE AS PAGE_TITLE,
CASE WHEN DESCRIPTION = '' OR DESCRIPTION IS NULL
THEN 'n.a.' ELSE DESCRIPTION
END AS PAGE_DESCR,
CASE WHEN KEYWORDS = '' OR KEYWORDS IS NULL
THEN 'n.a.' ELSE KEYWORDS
END AS PAGE_KEYWORDS
FROM STG_CMS_MENU
WHERE CONTENT_TYPE != 'folder' -- only web pages
AND PAGE_TITLE != 'n.a.' -- master data which make sense
)
/* new records from stage: CLS */
SELECT
-1 AS DIM_REQUEST_ID_HIST,
CLS.REQUEST_NK,
CLS.PAGE_TITLE,
CLS.PAGE_DESCR,
CLS.PAGE_KEYWORDS,
0 AS DELETE_FLAG,
STRFTIME('%s','now') AS UPDATE_DT
FROM CLS
LEFT OUTER JOIN
(
SELECT
DIM_REQUEST_ID,
REQUEST_NK,
PAGE_TITLE,
PAGE_DESCR,
PAGE_KEYWORDS
FROM DIM_REQUEST_V_ACT
) TRG ON CLS.REQUEST_NK = TRG.REQUEST_NK
WHERE TRG.REQUEST_NK IS NULL -- no such record in data martActualización de atributos SCD2
/* Load request table SCD from master data */
INSERT INTO DIM_REQUEST (DIM_REQUEST_ID_HIST, REQUEST_NK, PAGE_TITLE, PAGE_DESCR, PAGE_KEYWORDS, DELETE_FLAG, UPDATE_DT)
WITH CLS AS ( -- prepare keys
SELECT
'/' || NAME AS REQUEST_NK,
TITLE AS PAGE_TITLE,
CASE WHEN DESCRIPTION = '' OR DESCRIPTION IS NULL
THEN 'n.a.' ELSE DESCRIPTION
END AS PAGE_DESCR,
CASE WHEN KEYWORDS = '' OR KEYWORDS IS NULL
THEN 'n.a.' ELSE KEYWORDS
END AS PAGE_KEYWORDS
FROM STG_CMS_MENU
WHERE CONTENT_TYPE != 'folder' -- only web pages
AND PAGE_TITLE != 'n.a.' -- master data which make sense
)
/* updated records from stage: CLS and build reference to history: HIST */
SELECT
HIST.DIM_REQUEST_ID AS DIM_REQUEST_ID_HIST,
HIST.REQUEST_NK,
CLS.PAGE_TITLE,
CLS.PAGE_DESCR,
CLS.PAGE_KEYWORDS,
0 AS DELETE_FLAG,
STRFTIME('%s','now') AS UPDATE_DT
FROM CLS,
DIM_REQUEST_V_ACT TRG,
DIM_REQUEST HIST
WHERE CLS.REQUEST_NK = TRG.REQUEST_NK
AND TRG.DIM_REQUEST_ID = HIST.DIM_REQUEST_ID
AND ( CLS.PAGE_TITLE != HIST.PAGE_TITLE /* changes only */
OR CLS.PAGE_DESCR != HIST.PAGE_DESCR
OR CLS.PAGE_KEYWORDS != HIST.PAGE_KEYWORDS )Registros eliminados SCD2
/* Load request table SCD from master data */
INSERT INTO DIM_REQUEST (DIM_REQUEST_ID_HIST, REQUEST_NK, PAGE_TITLE, PAGE_DESCR, PAGE_KEYWORDS, DELETE_FLAG, UPDATE_DT)
WITH CLS AS ( -- prepare keys
SELECT
'/' || NAME AS REQUEST_NK,
TITLE AS PAGE_TITLE
FROM STG_CMS_MENU
WHERE CONTENT_TYPE != 'folder' -- only web pages
AND PAGE_TITLE != 'n.a.' -- master data which make sense
)
/* deleted records in data mart: TRG */
SELECT
TRG.DIM_REQUEST_ID AS DIM_REQUEST_ID_HIST,
TRG.REQUEST_NK,
TRG.PAGE_TITLE,
TRG.PAGE_DESCR,
TRG.PAGE_KEYWORDS,
1 AS DELETE_FLAG,
STRFTIME('%s','now') AS UPDATE_DT
FROM (
SELECT
DIM_REQUEST_ID,
REQUEST_NK,
PAGE_TITLE,
PAGE_DESCR,
PAGE_KEYWORDS
FROM DIM_REQUEST_V_ACT
WHERE PAGE_TITLE != 'n.a.' -- track master data only
AND DELETE_FLAG = 0 -- not already deleted
) TRG
LEFT OUTER JOIN CLS ON TRG.REQUEST_NK = CLS.REQUEST_NK
WHERE CLS.REQUEST_NK IS NULL -- no such record in stageCada fuente de datos debe ser acompañada de una descripción formal, por ejemplo, en un archivo readme.txt:
Receptor de datos formal/técnico: nombre, dirección de correo electrónico
Proveedor de datos formal/técnico: nombre, dirección de correo electrónico
Fuente de datos: ruta al archivo, nombres de servicios
Información sobre el acceso a los datos: usuarios y contraseñas
Un esquema de movimiento de datos ayudará en el proceso de mantenimiento y actualización, por ejemplo, en forma de texto:
Movimiento de archivo. Fuente: ftp.domain.net: /logs/access.log Objetivo: /var/www/access.log
Lectura en la etapa. Objetivo: STG_ACCESS_LOG
Carga y transformación. Objetivo: FCT_ACCESS_REQUEST_REF_HH
Carga y transformación. Objetivo: FCT_ACCESS_USER_AGENT_DD
Informe. Objetivo: /var/www/report.html
Salida
Así, el artículo describe mecanismos como la integración de bases de datos y la introducción de nuevos niveles de agregación. Estos son necesarios al construir almacenes de datos con el fin de obtener conocimientos adicionales y mejorar la calidad de la información.
Fuente: habr.com
