2011-08-09 153 views
13

Quiero escribir un procedimiento almacenado en SQL (MySQL) para calcular el promedio del segundo y tercer cuartil.Calcule el promedio de 2,3 cuartiles en SQL

En otras palabras, tengo registros de mediciones de cuánto tiempo le lleva cargar una URL. Los registros son (id, url, time) y son muchas medidas para cada URL. Lo que trato de hacer es que cada URL elimine el 25% más bajo y el más alto (es decir, cuartiles inferiores y superiores) y calcule el promedio del 25% -75% restante de los tiempos de carga. Y almacene esto en otra mesa.

Vi algunos ejemplos de esto para MS SQL y me pareció relativamente fácil. Pero tengo que usar MySQL donde:

  • cláusula LIMIT no admite porcentajes (sin analógica para seleccionar la parte superior del 25%)
  • cláusula LIMIT no admite sus argumentos para ser variables (sólo constantes)
  • funciones no son compatibles con SQL dinámico (por ejemplo, preparar y ejecutar)

y llegaron hasta aquí:

create procedure G(
    IN val VARCHAR(10) 
) 
Begin 
    select @cnt:=count(*) from test where a=val; 
    select @of:= @cnt /4; 
    SELECT @len:= @cnt/2; 
    Prepare stmt from 'select * from test where a="a" LIMIT ?,?'; 
    execute stmt using @of, @len; 
END; 

Puedo escribirlo en PHP, pero creo que en SQL tendría un rendimiento general mucho mejor. Apreciaré mucho la ayuda.

+2

Joe Celko's 'SQL For Smarties' tiene un capítulo sobre estadísticas (modo, mediana, varianza, etc.) Vale la pena el precio de compra. –

Respuesta

2

Mira respuesta y comentario de @Richard aka cyberkiwi en this question:

Select * 
from 
(
    SELECT tbl.*, @counter := @counter +1 counter 
    FROM (select @counter:=0) initvar, tbl 
    ORDER BY ordcolumn 
) X 
where counter >= (25/100 * @counter) and counter <= (75/100 * @counter); 
ORDER BY ordcolumn 
0

¿qué tal esto?

prepare stmt from select concat('select * from test where a="a" LIMIT ',@of,@len); 
execute stmt; 
+0

1. Esto sigue siendo SQL dinámico y no es diferente de lo que escribí 2. Esta línea produce un error de sintaxis a pesar de que me parece bien – munch

0

Eche un vistazo a este excelente ejemplo de cálculo de percentiles con MySQL. Lo he usado con gran éxito en algunos conjuntos de datos bastante grandes.

http://planet.mysql.com/entry/?id=13588

Tomar nota de la sección relativa a group_concat_max_len - esto es realmente importante. Al establecer este valor en el valor máximo permitido, que es la configuración para el tamaño máximo de paquete, se asegurará de que si la cadena que construye se vuelve demasiado grande, obtendrá un error adecuado en lugar de solo una advertencia de 'campo truncado'.

SET @@group_concat_max_len := @@max_allowed_packet; 

Lo que me gustaría hacer es usar esta función para calcular el percentiles 25 y 75 (que se puede hacer en una sola consulta), y luego calcular los promedios de los datos restantes mediante la ejecución de una segunda consulta en los datos .

<?php 
$lowVal = /* result of query getting the 25%ile value */; 
$highVal = /* result of query getting the 75%ile value */; 

$strSQL = "SELECT AVG(`field`) AS myAvg 
      FROM `table` 
      WHERE { your_existing_criteria_goes_here } 
       AND `filter_field` BETWEEN '{$lowVal}' AND '{$highVal}';" 
/* Run the query and extract your data */ 
?> 

la esperanza de que todo tiene sentido, y ayudar con su problema :)

+1

Sé que no es un procedimiento almacenado y hecho dentro de PHP, y estoy seguro que alguien puede volver a escribirlo como un procedimiento almacenado que puede obtener los valores ile 25% y 75% ile de una sola consulta y luego aplicarlos a la consulta principal filtrada, pero lo anterior es cómo lo haría eso. Luego, le permite usar los valores de 25% ile y 75% de ile en otras áreas de su código, p. al crear algunos encabezados o información de leyenda para la tabla de datos o el gráfico que está construyendo;) –

0

¿Por qué no sólo tiene que utilizar una consulta de esta manera:

select url, avg(time) 
from mytable A 
where time > 
     (select min(B.time) + ((max(B.time)-min(B.time))/100*25) 
      from mytable B where B.url = A.url) 
and time < 
     (select max(B.time) - ((max(B.time)-min(B.time))/100*25) 
      from mytable B where B.url = A.url) 
group by url; 
+0

Eso no necesariamente le proporciona el 2. ° y 3. ° cuartil. Si los tiempos están distribuidos de manera uniforme, estarán cerca, pero tal como están, esto no hace lo que se pide. – Dason

1

Puede crear los valores por cuartiles de usar si para ponerlos a cero si en el cuartil equivocada:

Asumamos, la tabla de datos en bruto es creado por

DROP TABLE IF EXISTS `rawdata`; 
CREATE TABLE `rawdata` (
    `id` int(11) NOT NULL AUTO_INCREMENT, 
    `url` varchar(250) NOT NULL DEFAULT '', 
    `time` int(11) NOT NULL, 
    PRIMARY KEY (`id`), 
    KEY `time` (`time`) 
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 

(y por supuesto poblado).

Supongamos también que los datos de tabla cuartil es creado por

DROP TABLE IF EXISTS `quartiles`; 
CREATE TABLE `quartiles` (
    `url` varchar(250) NOT NULL, 
    `Q1` float DEFAULT '0', 
    `Q2` float DEFAULT '0', 
    `Q3` float DEFAULT '0', 
    `Q4` float DEFAULT '0', 
    PRIMARY KEY (`url`), 
) ENGINE=MyISAM DEFAULT CHARSET=utf8; 

(y deja vacío).

continuación un procedimiento para rellenar los cuartiles de RAWDATA se vería como

DELIMITER ;; 

CREATE PROCEDURE `ComputeQuartiles`() 
    READS SQL DATA 
BEGIN 
    DECLARE numrows int DEFAULT 0; 
    DECLARE qrows int DEFAULT 0; 
    DECLARE rownum int DEFAULT 0; 
    DECLARE done int DEFAULT 0; 
    DECLARE currenturl VARCHAR(250) CHARACTER SET utf8; 
    DECLARE Q1,Q2,Q3,Q4 float DEFAULT 0.0; 
    DECLARE allurls CURSOR FOR SELECT DISTINCT url FROM rawdata; 
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET currenturl=''; 

    OPEN allurls; 
    FETCH allurls INTO currenturl; 
    WHILE currenturl<>'' DO 
     SELECT COUNT(*) INTO numrows FROM rawdata WHERE url=currenturl; 
     SET qrows=FLOOR(numrows/4); 
     if qrows>0 THEN 
      -- Only session parameters can be recalculated inside a query, 
      -- so @rownum:[email protected]+1 will work, but rownum:=rownum+1 will not. 
      SET @rownum=0; 
      SELECT 
       SUM(IFNULL(QA,0))/qrows, 
       SUM(IFNULL(QB,0))/qrows, 
       SUM(IFNULL(QC,0))/qrows, 
       SUM(IFNULL(QD,0))/qrows 
      FROM (
       SELECT 
        if(@rownum<qrows,time,0) AS QA, 
        if(@rownum>=qrows AND @rownum<2*qrows,time,0) AS QB, 
        -- the middle 0-3 rows are left out 
        if(@rownum>=(numrows-2*qrows) AND @rownum<(numrows-qrows),time,0) AS QC, 
        if(@rownum>=(numrows-qrows),time,0) AS QD, 
        @rownum:[email protected]+1 AS dummy 
       FROM rawdata 
       WHERE url=currenturl ORDER BY time 
      ) AS baseview 
      INTO Q1,Q2,Q3,Q4 
      ; 
      REPLACE INTO quartiles values (currenturl,Q1,Q2,Q3,Q4); 
     END IF; 

     FETCH allurls INTO currenturl; 
    END WHILE; 
    CLOSE allurls; 

END ;; 

DELIMITER ; 

Los puntos principales son:

  • utiliza un cursor para cambiar las direcciones URL (o adaptar la muestra para aceptar la dirección URL como un parámetro)
  • Para cada URL, encuentre el número total de filas
  • Realice algunas matemáticas triviales para omitir las filas del medio, si (rowcount % 4) != 0
  • seleccionar todas las filas primas para la dirección URL, asignar el valor de time a uno de QA-QD, dependiendo del número de fila, la asignación de la otra Qx el valor 0
  • Utilice esta consulta como una subconsulta a otro, lo cual resume y normaliza los valores
  • utilizar los resultados de este SuperQuery para actualizar la tabla cuartiles

probé esto con 18432 filas primas, url=concat('http://.../',floor(rand()*10)), time=round(rand()*10000) en una máquina 8x1.9GHz y terminó consistentemente en 0.50-0.54sec

Cuestiones relacionadas