Подтвердить что ты не робот

Вычислить среднее среднее квартили в SQL

Я хочу написать хранимую процедуру в SQL (MySQL), чтобы вычислить среднее значение второго и третьего квартилей.

Другими словами, у меня есть записи для измерений, сколько времени требуется для загрузки URL-адреса. Записи - это (id, url, time), и они содержат много измерений для каждого URL-адреса. То, что я пытаюсь сделать, это для каждого URL-адреса удалить самый низкий и верхний 25% (т.е. Нижний и верхний квартили) и вычислить среднее значение оставшихся 25% -75% времени загрузки. И сохраните это в другой таблице.

Я видел несколько примеров для этого для MS SQL и, казалось, был относительно простым. Но я должен использовать MySQL, где:

  • Предложение LIMIT не поддерживает проценты (нет аналога для выбора 25%)
  • Предложение LIMIT не поддерживает его аргументы как переменные (только константы)
  • функции не поддерживают динамический SQL (например, PREPARE и EXECUTE)

И я добрался до места:

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;

Я могу написать его на PHP, но думаю, что в SQL он будет иметь гораздо лучшую общую производительность. Я буду очень благодарен за помощь.

4b9b3361

Ответ 1

Посмотрите на ответ и комментарий @Richard aka cyberkiwi в этот вопрос:

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

Ответ 2

Вы можете создать значения квартилей, используя IF, чтобы установить их в ноль, если в неправильном квартиле:

Предположим, что таблица необработанных данных создана

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;

(и, конечно же, заселен).

Предположим также, что данные таблицы квартили создаются с помощью

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;

(и осталось пустым).

Тогда процедура заполнения квартилей из rawdata будет выглядеть как

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 ;

Основные моменты:

  • Используйте курсор для циклирования URL-адресов (или адаптируйте образец, чтобы принять URL-адрес в качестве параметра)
  • Для каждого URL найдите общее количество строк
  • Сделайте некоторые тривиальные математические данные, чтобы опустить средние строки, если (rowcount % 4) != 0
  • выберите все необработанные строки для URL-адреса, присвоив значение time одному из QA-QD, в зависимости от номера строки, присвоив другому Qx значение 0
  • Используйте этот запрос в качестве подзапроса для другого, который суммирует и нормализует значения
  • Используйте результаты этого суперзапроса для обновления таблицы квартилей.

Я протестировал это с 18432 необработанными строками, url=concat('http://.../',floor(rand()*10)), time=round(rand()*10000) на машине 8x1,9 ГГц, и он закончил последовательно в 0.50-0.54 сек

Ответ 3

как насчет этого?

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

Ответ 4

Взгляните на этот отличный пример вычисления процентилей с MySQL. Я использовал это с большим успехом на некоторых довольно больших наборах данных.

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

Обратите внимание на раздел, относящийся к group_concat_max_len - это действительно важно. Установка этого значения в максимально допустимое значение, которое является вашим параметром для максимального размера пакета, гарантирует, что если строка, которую он создает, станет слишком большой, вы получите правильную ошибку, а не только предупреждение об усеченном поле.

SET @@group_concat_max_len := @@max_allowed_packet;

Я бы использовал эту функцию для вычисления 25-го и 75-го процентилей (что можно сделать в одном запросе), а затем вычислить средние значения ваших оставшихся данных, выполнив второй запрос к данным.

<?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 */
?>

Надеюсь, что все имеет смысл и поможет с вашей проблемой:)

Ответ 5

Почему бы вам просто не использовать один запрос таким образом:

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;