-- ============================================================================ -- SCHEMA: admin -- TABLE: platform_analytics -- DESCRIPTION: Metricas agregadas de la plataforma -- VERSION: 1.0.0 -- CREATED: 2026-01-16 -- SPRINT: Sprint 1 - DDL Implementation Roadmap Q1-2026 -- ============================================================================ -- Enum para tipo de metrica DO $$ BEGIN CREATE TYPE admin.metric_type AS ENUM ( 'counter', -- Contador simple 'gauge', -- Valor actual 'histogram', -- Distribucion 'summary', -- Resumen estadistico 'rate' -- Tasa por periodo ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Enum para granularidad DO $$ BEGIN CREATE TYPE admin.time_granularity AS ENUM ( 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year' ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla de Metricas de Plataforma CREATE TABLE IF NOT EXISTS admin.platform_analytics ( -- Identificadores id BIGSERIAL PRIMARY KEY, -- Scope tenant_id UUID REFERENCES tenants.tenants(id), -- NULL = plataforma global -- Metrica metric_name VARCHAR(100) NOT NULL, metric_type admin.metric_type NOT NULL DEFAULT 'gauge', category VARCHAR(50) NOT NULL, -- 'users', 'trading', 'revenue', 'performance' -- Periodo period_start TIMESTAMPTZ NOT NULL, period_end TIMESTAMPTZ NOT NULL, granularity admin.time_granularity NOT NULL DEFAULT 'day', -- Valores value DECIMAL(20, 6) NOT NULL, previous_value DECIMAL(20, 6), -- Valor periodo anterior (para calcular delta) delta_value DECIMAL(20, 6), -- Cambio vs periodo anterior delta_percent DECIMAL(10, 4), -- Cambio porcentual -- Estadisticas (para histogramas/summaries) min_value DECIMAL(20, 6), max_value DECIMAL(20, 6), avg_value DECIMAL(20, 6), median_value DECIMAL(20, 6), p95_value DECIMAL(20, 6), p99_value DECIMAL(20, 6), stddev_value DECIMAL(20, 6), sample_count BIGINT, -- Dimensiones adicionales dimensions JSONB DEFAULT '{}'::JSONB, -- Dimensiones para desglose -- Metadata source VARCHAR(50) NOT NULL DEFAULT 'system', -- Origen del dato is_estimated BOOLEAN NOT NULL DEFAULT FALSE, -- Dato estimado vs real confidence_level DECIMAL(5, 4), -- Nivel de confianza (0-1) -- Timestamps collected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraints CONSTRAINT analytics_period_check CHECK (period_end > period_start), CONSTRAINT analytics_unique_metric UNIQUE (tenant_id, metric_name, granularity, period_start) ); COMMENT ON TABLE admin.platform_analytics IS 'Metricas agregadas de la plataforma para dashboards y reportes'; COMMENT ON COLUMN admin.platform_analytics.tenant_id IS 'NULL para metricas globales de plataforma, UUID para metricas por tenant'; COMMENT ON COLUMN admin.platform_analytics.dimensions IS 'Dimensiones adicionales como {"country": "MX", "plan": "premium"}'; -- Particionamiento por fecha (recomendado para produccion) -- CREATE TABLE admin.platform_analytics_partitioned ( -- LIKE admin.platform_analytics INCLUDING ALL -- ) PARTITION BY RANGE (period_start); -- Indices CREATE INDEX IF NOT EXISTS idx_analytics_metric_period ON admin.platform_analytics(metric_name, period_start DESC); CREATE INDEX IF NOT EXISTS idx_analytics_tenant_metric ON admin.platform_analytics(tenant_id, metric_name, period_start DESC) WHERE tenant_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_analytics_category ON admin.platform_analytics(category, period_start DESC); CREATE INDEX IF NOT EXISTS idx_analytics_granularity ON admin.platform_analytics(granularity, period_start DESC); CREATE INDEX IF NOT EXISTS idx_analytics_global ON admin.platform_analytics(metric_name, granularity, period_start DESC) WHERE tenant_id IS NULL; -- GIN index para dimensiones CREATE INDEX IF NOT EXISTS idx_analytics_dimensions_gin ON admin.platform_analytics USING GIN (dimensions); -- Trigger para calcular deltas automaticamente CREATE OR REPLACE FUNCTION admin.calculate_analytics_delta() RETURNS TRIGGER AS $$ DECLARE v_prev RECORD; v_period_interval INTERVAL; BEGIN -- Calcular intervalo basado en granularidad CASE NEW.granularity WHEN 'minute' THEN v_period_interval := INTERVAL '1 minute'; WHEN 'hour' THEN v_period_interval := INTERVAL '1 hour'; WHEN 'day' THEN v_period_interval := INTERVAL '1 day'; WHEN 'week' THEN v_period_interval := INTERVAL '1 week'; WHEN 'month' THEN v_period_interval := INTERVAL '1 month'; WHEN 'quarter' THEN v_period_interval := INTERVAL '3 months'; WHEN 'year' THEN v_period_interval := INTERVAL '1 year'; END CASE; -- Buscar valor anterior SELECT value INTO v_prev FROM admin.platform_analytics WHERE metric_name = NEW.metric_name AND granularity = NEW.granularity AND (tenant_id = NEW.tenant_id OR (tenant_id IS NULL AND NEW.tenant_id IS NULL)) AND period_start = NEW.period_start - v_period_interval LIMIT 1; IF v_prev IS NOT NULL THEN NEW.previous_value := v_prev.value; NEW.delta_value := NEW.value - v_prev.value; IF v_prev.value != 0 THEN NEW.delta_percent := ((NEW.value - v_prev.value) / v_prev.value) * 100; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS analytics_calc_delta ON admin.platform_analytics; CREATE TRIGGER analytics_calc_delta BEFORE INSERT ON admin.platform_analytics FOR EACH ROW EXECUTE FUNCTION admin.calculate_analytics_delta(); -- Funcion para insertar o actualizar metrica CREATE OR REPLACE FUNCTION admin.upsert_metric( p_metric_name VARCHAR(100), p_category VARCHAR(50), p_value DECIMAL(20, 6), p_granularity admin.time_granularity DEFAULT 'day', p_tenant_id UUID DEFAULT NULL, p_metric_type admin.metric_type DEFAULT 'gauge', p_dimensions JSONB DEFAULT '{}'::JSONB ) RETURNS BIGINT AS $$ DECLARE v_period_start TIMESTAMPTZ; v_period_end TIMESTAMPTZ; v_id BIGINT; BEGIN -- Calcular periodo basado en granularidad CASE p_granularity WHEN 'minute' THEN v_period_start := DATE_TRUNC('minute', NOW()); v_period_end := v_period_start + INTERVAL '1 minute'; WHEN 'hour' THEN v_period_start := DATE_TRUNC('hour', NOW()); v_period_end := v_period_start + INTERVAL '1 hour'; WHEN 'day' THEN v_period_start := DATE_TRUNC('day', NOW()); v_period_end := v_period_start + INTERVAL '1 day'; WHEN 'week' THEN v_period_start := DATE_TRUNC('week', NOW()); v_period_end := v_period_start + INTERVAL '1 week'; WHEN 'month' THEN v_period_start := DATE_TRUNC('month', NOW()); v_period_end := v_period_start + INTERVAL '1 month'; WHEN 'quarter' THEN v_period_start := DATE_TRUNC('quarter', NOW()); v_period_end := v_period_start + INTERVAL '3 months'; WHEN 'year' THEN v_period_start := DATE_TRUNC('year', NOW()); v_period_end := v_period_start + INTERVAL '1 year'; END CASE; INSERT INTO admin.platform_analytics ( tenant_id, metric_name, metric_type, category, period_start, period_end, granularity, value, dimensions ) VALUES ( p_tenant_id, p_metric_name, p_metric_type, p_category, v_period_start, v_period_end, p_granularity, p_value, p_dimensions ) ON CONFLICT (tenant_id, metric_name, granularity, period_start) DO UPDATE SET value = EXCLUDED.value, dimensions = EXCLUDED.dimensions, collected_at = NOW() RETURNING id INTO v_id; RETURN v_id; END; $$ LANGUAGE plpgsql; -- Vista de metricas recientes CREATE OR REPLACE VIEW admin.v_recent_metrics AS SELECT metric_name, category, granularity, tenant_id, value, delta_percent, period_start, collected_at FROM admin.platform_analytics WHERE collected_at > NOW() - INTERVAL '24 hours' ORDER BY metric_name, period_start DESC; -- Vista de KPIs principales CREATE OR REPLACE VIEW admin.v_platform_kpis AS SELECT metric_name, value AS current_value, previous_value, delta_percent AS change_percent, period_start, granularity FROM admin.platform_analytics WHERE tenant_id IS NULL -- Solo metricas globales AND granularity = 'day' AND metric_name IN ( 'total_users', 'active_users_daily', 'total_trades', 'total_volume_usd', 'revenue_usd', 'new_signups', 'churn_rate' ) AND period_start = DATE_TRUNC('day', NOW()) ORDER BY metric_name; -- Grants GRANT SELECT, INSERT, UPDATE ON admin.platform_analytics TO trading_app; GRANT SELECT ON admin.platform_analytics TO trading_readonly; GRANT USAGE, SELECT ON SEQUENCE admin.platform_analytics_id_seq TO trading_app; GRANT SELECT ON admin.v_recent_metrics TO trading_app; GRANT SELECT ON admin.v_platform_kpis TO trading_app; GRANT EXECUTE ON FUNCTION admin.upsert_metric TO trading_app;