Квартили в запросе SQL

У меня очень простая таблица:

CREATE TABLE IF NOT EXISTS LuxLog (
  Sensor TINYINT,
  Lux INT,
  PRIMARY KEY(Sensor)
)

Он содержит тысячи журналов с разных датчиков.

Хотелось бы иметь Q1 и Q3 для всех датчиков.

Я могу выполнить один запрос для всех данных, но для меня было бы лучше иметь один запрос для всех датчиков (получение Q1 и Q3 обратно из одного запроса)

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

Кто-нибудь может мне намекнуть?

Изменить: это фрагмент кода, который я нашел в Интернете, но он не работает для меня:

SELECT  SUBSTRING_INDEX(
        SUBSTRING_INDEX(
            GROUP_CONCAT(                 -- 1) make a sorted list of values
                Lux
                ORDER BY Lux
                SEPARATOR ','
            )
        ,   ','                           -- 2) cut at the comma
        ,   75/100 * COUNT(*)        --    at the position beyond the 90% portion
        )
    ,   ','                               -- 3) cut at the comma
    ,   -1                                --    right after the desired list entry
    )                 AS `75th Percentile`
    FROM    LuxLog
    WHERE   Sensor=12
    AND     Lux<>0

Я получаю 1 как возвращаемое значение, хотя это должно быть число, которое можно разделить на 10 (10,20,30 ..... 1000)


person Hamma    schedule 03.07.2015    source источник
comment
Разве вы не задавали этот вопрос всего несколько часов назад? Нет хороших ответов?   -  person jarlh    schedule 03.07.2015
comment
Я подумал, что это будет довольно простая операция, поскольку квартили широко используются и являются одной из основных статистических переменных при расчете частоты. Это не очень хорошая основа для предсказания простоты задачи. Вычисление квартилей (и даже просто медианы) с операционной точки зрения сложнее, чем, скажем, вычисление средних значений.   -  person John Bollinger    schedule 03.07.2015
comment
Вы правы, но сейчас я подхожу к SQL, и, поскольку я привык к другим языкам программирования высокого уровня, отсутствие статистического пакета заставляет меня страдать.   -  person Hamma    schedule 03.07.2015
comment
SQL нельзя охарактеризовать как язык программирования. Это язык определения данных и манипулирования данными, основанный на реляционной модели данных. Хотя в SQL в целом и в некоторых его реализациях есть некоторые функции, направленные на упорядочение, основная модель основана на (неупорядоченных) наборах. Это плохо сочетается с некоторыми видами задач, такими как вычисление квартилей. Это не означает, что вы не можете выполнять такие вычисления в SQL, но для некоторых задач лучше сочетать SQL с другим языком.   -  person John Bollinger    schedule 03.07.2015
comment
В частности, MySQL особенно ограничен для таких задач, поскольку ему не хватает нескольких общих функций, каждая из которых облегчила бы работу (оконные функции, общие табличные выражения, различные другие специальные функции - даже без учета NTILE()).   -  person John Bollinger    schedule 03.07.2015
comment
Хорошо, но поскольку мой выбор - между поиском способа запроса к базе данных и получением 1 уже рассчитанного значения или получением около 100 тыс. Значений с последующим их вычислением самим, я предположил, что было бы более эффективно избежать передачи 100 тыс. Через Интернет. Итак, язык программирования или нет, может ли SQL дать мне квартили или мне нужно сделать это вручную?   -  person Hamma    schedule 03.07.2015


Ответы (6)


См. SqlFiddle: http://sqlfiddle.com/#!9/accca6/2/6 Примечание: для sqlfiddle я сгенерировал 100 строк, каждое целое число от 1 до 100 имеет строку, но это случайный порядок (выполняется в excel).

Вот код:

SET @number_of_rows := (SELECT COUNT(*) FROM LuxLog);
SET @quartile := (ROUND(@number_of_rows*0.25));
SET @sql_q1 := (CONCAT('(SELECT "Q1" AS quartile_name , Lux, Sensor FROM LuxLog ORDER BY Lux DESC LIMIT 1 OFFSET ', @quartile,')'));
SET @sql_q3 := (CONCAT('( SELECT "Q3" AS quartile_name , Lux, Sensor FROM LuxLog ORDER BY Lux ASC LIMIT 1 OFFSET ', @quartile,');'));
SET @sql := (CONCAT(@sql_q1,' UNION ',@sql_q3));
PREPARE stmt1 FROM @sql;
EXECUTE stmt1;

РЕДАКТИРОВАТЬ:

SET @current_sensor := 101;
SET @quartile := (ROUND((SELECT COUNT(*) FROM LuxLog WHERE Sensor = @current_sensor)*0.25));
SET @sql_q1 := (CONCAT('(SELECT "Q1" AS quartile_name , Lux, Sensor FROM LuxLog WHERE Sensor=', @current_sensor,' ORDER BY Lux DESC LIMIT 1 OFFSET ', @quartile,')'));
SET @sql_q3 := (CONCAT('( SELECT "Q3" AS quartile_name , Lux, Sensor FROM LuxLog WHERE Sensor=', @current_sensor,' ORDER BY Lux ASC LIMIT 1 OFFSET ', @quartile,');'));
SET @sql := (CONCAT(@sql_q1,' UNION ',@sql_q3));
PREPARE stmt1 FROM @sql;
EXECUTE stmt1;

Основная причина заключается в следующем: для квартиля 1 мы хотим получить 25% сверху, поэтому мы хотим знать, сколько там строк, это:

SET @number_of_rows := (SELECT COUNT(*) FROM LuxLog);

Теперь, когда мы знаем количество строк, мы хотим знать, что составляет 25% от него, это эта строка:

SET @quartile := (ROUND(@number_of_rows*0.25));

Затем, чтобы найти квартиль, мы хотим упорядочить таблицу LuxLog по Lux, а затем, чтобы получить номер строки «@quartile», для этого мы устанавливаем OFFSET на @quartile, чтобы сказать, что мы хотим начать наш выбор из строки number @quartile и мы говорим limit 1, чтобы сказать, что мы хотим получить только одну строку. Это :

SET @sql_q1 := (CONCAT('(SELECT "Q1" AS quartile_name , Lux, Sensor FROM LuxLog ORDER BY Lux DESC LIMIT 1 OFFSET ', @quartile,')'));

Мы делаем (почти) то же самое для другого квартиля, но вместо того, чтобы начинать сверху (от более высоких значений к более низким), мы начинаем снизу (это объясняет ASC).

Но пока у нас есть только строки, хранящиеся в переменных @ sql_q1 и @ sql_q3, поэтому, объединяя их, мы объединяем результаты запросов, подготавливаем запрос и выполняем его.

person Pholochtairze    schedule 03.07.2015
comment
Я создал Fiddle (sqlfiddle.com/#!9/a14a4/3) с небольшая часть моих данных (у меня около 50 тыс. строк в день). Я также добавил WHERE Sensor = x, поскольку мне нужны данные для каждого датчика, но я не могу понять, как использовать Fiddle. Хотите посмотреть? - person Hamma; 03.07.2015
comment
@Hamma: извините, я думал, что не было двух строк с одним и тем же датчиком ^^ Вот код, который работает для данного датчика (см. Отредактированный код), я работаю над тем, который выводит для всех датчиков, я скажу вам, когда это будет сделано . - person Pholochtairze; 03.07.2015
comment
Я попробую, как только смогу. Не беспокойтесь о выходе для всех датчиков, так как я могу запускать разные запросы для каждого датчика. Для меня важно получить квартили из запроса вместо того, чтобы получать тысячи исходных данных из запроса, а затем вычислять их самостоятельно. - person Hamma; 03.07.2015
comment
Возможно, это и моя ошибка. Если у меня несколько строк с одним и тем же датчиком, мне не следует использовать датчик в качестве первичного ключа, верно? - person Hamma; 03.07.2015
comment
Если мы возьмем определение из w3schools, например (сначала в Google): ограничение PRIMARY KEY однозначно идентифицирует каждую запись в таблице базы данных. Это в основном означает, что каждая строка имеет различное значение в поле первичного ключа. Это позволяет однозначно идентифицировать строку. Вот почему странно иметь PRIMARY KEY, который является избыточным. Скорее всего, это внешний ключ, это означает, что поле внешнего ключа в вашей таблице связано с первичным ключом в другой таблице. См .: stackoverflow.com/questions/1692538/ - person Pholochtairze; 03.07.2015
comment
Моя таблица - это просто таблица журнала, используемая для статистики, мне действительно не нужен первичный ключ, но теперь, когда я думаю об этом, я не знаю, может ли использование Sensor в качестве первичного ключа создать проблемы с таблицей - person Hamma; 03.07.2015
comment
Возможно, вам не нужен первичный ключ, НО вам, вероятно, понадобится индекс (чтобы увидеть полезность индекса: stackoverflow.com/questions/1108/). Это ускорит результаты вашего запроса. Кроме того, вы просто не можете использовать первичный индекс в своей базе данных, поскольку у вас есть несколько строк с одним и тем же ключом. Когда я пробовал, он возвращает ошибку. Повторяющаяся запись «101» для ключа «PRIMARY». - person Pholochtairze; 04.07.2015
comment
У меня просто была возможность проверить, и я действительно сделал это хорошо, но я сделал ошибку, когда поместил это здесь в упрощенном виде, чтобы упростить задачу. В реальной таблице у меня есть столбец даты, и я установил первичный ключ следующим образом: «PRIMARY KEY (Time, Sensor))»; У меня новые показания датчика каждые 30 секунд, так что со мной все будет в порядке. Код работает отлично, теперь мне просто нужно понять, как реализовать его через C #. - person Hamma; 06.07.2015

Что ж, использовать NTILE очень просто, но это функция Postgres. Вы просто делаете что-то вроде этого:

SELECT value_you_are_NTILING,
    NTILE(4) OVER (ORDER BY value_you_are_NTILING DESC) AS tiles
FROM
(SELECT math_that_gives_you_the_value_you_are_NTILING_here AS value_you_are_NTILING FROM tablename);

Вот простой пример, который я сделал для вас на SQLFiddle: http://sqlfiddle.com/#!15/7f05a/1

В MySQL вы бы использовали RANK ... Вот SQLFiddle для этого: http://www.sqlfiddle.com/#!2/d5587/1 (это взято из вопроса, ссылка на который приведена ниже)

Это использование MySQL RANK () происходит из Stackoverflow, на который здесь дан ответ: Функция ранжирования в MySQL

Ищите ответ Салмана А.

person Community    schedule 03.07.2015
comment
Что ж, самая большая проблема с NTILE() для этого задания заключается в том, что его нет в MySQL (и вопрос помечен как mysql). - person John Bollinger; 03.07.2015
comment
Да, ты прав. Я использую MySQL и только что заметил, что NTILE () не является функцией MySQL. Прошу прощения за потраченное время. - person Hamma; 03.07.2015
comment
Это не пустая трата времени. Я добавил ссылку на обсуждение функции RANK в Mysql. Это дает вам то, что вы ищете. - person ; 03.07.2015
comment
Я попробовал пример на своей таблице и получил результаты, сгруппированные по рангу. Я предполагаю, что для получения квартилей я должен запросить общий рейтинг * 0,25 и общий рейтинг * 0,75? - person Hamma; 03.07.2015
comment
Думаю, NTILE был добавлен кому: MySQL 8 - person y2k-shubham; 28.03.2018

Что-то вроде этого должно сработать:

select
    ll.*,
    if (a.position is not null, 1,
        if (b.position is not null, 2, 
        if (c.position is not null, 3, 
        if (d.position is not null, 4, 0)))
    ) as quartile
from
    luxlog ll
    left outer join luxlog a on ll.position = a.position and a.lux > (select count(*)*0.00 from luxlog) and a.lux <= (select count(*)*0.25 from luxlog)
    left outer join luxlog b on ll.position = b.position and b.lux > (select count(*)*0.25 from luxlog) and b.lux <= (select count(*)*0.50 from luxlog)
    left outer join luxlog c on ll.position = c.position and c.lux > (select count(*)*0.50 from luxlog) and c.lux <= (select count(*)*0.75 from luxlog)
    left outer join luxlog d on ll.position = d.position and d.lux > (select count(*)*0.75 from luxlog)
;    

Вот полный пример:

use example;

drop table if exists luxlog;

CREATE TABLE LuxLog (
  Sensor TINYINT,
  Lux INT,
  position int,
  PRIMARY KEY(Position)
);

insert into luxlog values (0, 1, 10);
insert into luxlog values (0, 2, 20);
insert into luxlog values (0, 3, 30);
insert into luxlog values (0, 4, 40);
insert into luxlog values (0, 5, 50);
insert into luxlog values (0, 6, 60);
insert into luxlog values (0, 7, 70);
insert into luxlog values (0, 8, 80);

select count(*)*.25 from luxlog;
select count(*)*.50 from luxlog;

select
    ll.*,
    a.position,
    b.position,
    if(
        a.position is not null, 1,
        if (b.position is not null, 2, 0)
    ) as quartile
from
    luxlog ll
    left outer join luxlog a on ll.position = a.position and a.lux >= (select count(*)*0.00 from luxlog) and a.lux < (select count(*)*0.25 from luxlog)
    left outer join luxlog b on ll.position = b.position and b.lux >= (select count(*)*0.25 from luxlog) and b.lux < (select count(*)*0.50 from luxlog)
    left outer join luxlog c on ll.position = c.position and c.lux >= (select count(*)*0.50 from luxlog) and c.lux < (select count(*)*0.75 from luxlog)
    left outer join luxlog d on ll.position = d.position and d.lux >= (select count(*)*0.75 from luxlog) and d.lux < (select count(*)*1.00 from luxlog)
;    


select
    ll.*,
    if (a.position is not null, 1,
        if (b.position is not null, 2, 
        if (c.position is not null, 3, 
        if (d.position is not null, 4, 0)))
    ) as quartile
from
    luxlog ll
    left outer join luxlog a on ll.position = a.position and a.lux > (select count(*)*0.00 from luxlog) and a.lux <= (select count(*)*0.25 from luxlog)
    left outer join luxlog b on ll.position = b.position and b.lux > (select count(*)*0.25 from luxlog) and b.lux <= (select count(*)*0.50 from luxlog)
    left outer join luxlog c on ll.position = c.position and c.lux > (select count(*)*0.50 from luxlog) and c.lux <= (select count(*)*0.75 from luxlog)
    left outer join luxlog d on ll.position = d.position and d.lux > (select count(*)*0.75 from luxlog)
;    
person John    schedule 03.07.2015
comment
Для выполнения этого запроса потребовалось 80 секунд, и в результате он дал мне все 10 тыс. Строк. Основная причина, по которой я пытаюсь это сделать, - избежать передачи всех этих данных. Я жду только результат операции обратно от select. - person Hamma; 03.07.2015
comment
Отредактировал ответ о ранжировании, который я дал, чтобы вернуть только одну строку для каждого элемента. - person John; 03.07.2015
comment
Я все еще получаю таблицу с тысячами строк с вашим запросом, с 25 секундами для запроса с долей данных, которые у меня будут. Я не понимаю, как эти два select count (*) в начале должны помочь. - person Hamma; 03.07.2015

Или вы можете использовать такой ранг:

select
    ll.*,
    @curRank := @curRank + 1 as rank,
    if (@curRank <= (select count(*)*0.25 from luxlog), 1,
        if (@curRank <= (select count(*)*0.50 from luxlog), 2, 
        if (@curRank <= (select count(*)*0.75 from luxlog), 3, 4))
    ) as quartile
from
    luxlog ll,
    (SELECT @curRank := 0) r
;    

И это даст только одну запись для каждого квартиля:

select
    x.quartile, group_concat(position)
from (
    select
        ll.*,
        @curRank := @curRank + 1 as rank,
        if (@curRank > 0 and @curRank <= (select count(*)*0.25 from luxlog), 1,
            if (@curRank > 0 and @curRank <= (select count(*)*0.50 from luxlog), 2, 
            if (@curRank > 0 and @curRank <= (select count(*)*0.75 from luxlog), 3, 4))
        ) as quartile
    from
        luxlog ll,
        (SELECT @curRank := 0) r
) x
group by quartile

+ ------------- + --------------------------- +
| quartile      | group_concat(position)      |
+ ------------- + --------------------------- +
| 1             | 10,20                       |
| 2             | 30,40                       |
| 3             | 50,60                       |
| 4             | 70,80                       |
+ ------------- + --------------------------- +
4 rows

РЕДАКТИРОВАТЬ: пример sqlFiddle (http://sqlfiddle.com/#!9/a14a4/17) выглядит так после его удаления

/*SET @number_of_rows := (SELECT COUNT(*) FROM LuxLog);
SET @quartile := (ROUND(@number_of_rows*0.25));
SET @sql_q1 := (CONCAT('(SELECT "Q1" AS quartile_name , Lux, Sensor FROM LuxLog WHERE Sensor=101 ORDER BY Lux DESC LIMIT 1 OFFSET ', @quartile,')'));
SET @sql_q3 := (CONCAT('( SELECT "Q3" AS quartile_name , Lux, Sensor FROM LuxLog WHERE Sensor=101 ORDER BY Lux ASC LIMIT 1 OFFSET ', @quartile,');'));
SET @sql := (CONCAT(@sql_q1,' UNION ',@sql_q3));
PREPARE stmt1 FROM @sql;
EXECUTE stmt1;*/

введите описание изображения здесь

person John    schedule 03.07.2015
comment
Я пытаюсь протестировать его, но он не запускается. Вы можете увидеть таблицу с небольшой выборкой моих данных здесь: sqlfiddle.com/#!9/ a14a4 / 6 - person Hamma; 03.07.2015
comment
Если я удалю закомментированный код (/ * * /) в верхней части примера sqlfiddle, который вы дадите, он у меня сработает. - person John; 04.07.2015

Вот запрос, который я придумал для вычисления квартилей; он выполняется в ~ 0,04 секунды с ~ 5000 строками таблицы. Я включил минимальные / максимальные значения, поскольку в конечном итоге использую эти данные для построения четырех квартильных диапазонов:

   SELECT percentile_table.percentile, avg(ColumnName) AS percentile_values
    FROM   
        (SELECT @rownum := @rownum + 1 AS `row_number`, 
                   d.ColumnName 
            FROM   PercentileTestTable d, 
                   (SELECT @rownum := 0) r 
            WHERE  ColumnName IS NOT NULL 
            ORDER  BY d.ColumnName
        ) AS t1, 
        (SELECT count(*) AS total_rows 
            FROM   PercentileTestTable d 
            WHERE  ColumnName IS NOT NULL 
        ) AS t2, 
        (SELECT 0 AS percentile 
            UNION ALL 
            SELECT 0.25
            UNION ALL 
            SELECT 0.5
            UNION ALL 
            SELECT 0.75
            UNION ALL 
            SELECT 1
        ) AS percentile_table  
    WHERE  
        (percentile_table.percentile != 0 
            AND percentile_table.percentile != 1 
            AND t1.row_number IN 
            ( 
                floor(( total_rows + 1 ) * percentile_table.percentile), 
                floor(( total_rows + 2 ) * percentile_table.percentile)
            ) 
        ) OR (
            percentile_table.percentile = 0 
            AND t1.row_number = 1
        ) OR (
            percentile_table.percentile = 1 
            AND t1.row_number = total_rows
        )
    GROUP BY percentile_table.percentile; 

Скрипка здесь: http://sqlfiddle.com/#!9/58c0e2/1

Конечно, есть проблемы с производительностью; Я был бы рад, если бы у кого-нибудь были отзывы о том, как это улучшить.

Список примеров данных:

 3, 4, 4, 4, 7, 10, 11, 12, 14, 16, 17, 18

Пример вывода запроса:

| percentile | percentile_values |
|------------|-------------------|
|          0 |                 3 |
|       0.25 |                 4 |
|        0.5 |              10.5 |
|       0.75 |                15 |
|          1 |                18 |
person icats    schedule 04.05.2016

Я использую это решение с функцией MYSQL:

x - это желаемый центиль

array_values ​​ в вашей группе значений group_concat, разделенных символом,

DROP FUNCTION IF EXISTS centile;

delimiter $$
CREATE FUNCTION `centile`(x Text, array_values TEXT) RETURNS text
BEGIN

Declare DIFF_RANK TEXT;
Declare RANG_FLOOR INT;
Declare COUNT INT;
Declare VALEUR_SUP TEXT;
Declare VALEUR_INF TEXT;

SET COUNT = LENGTH(array_values) - LENGTH(REPLACE(array_values, ',', '')) + 1;
SET RANG_FLOOR = FLOOR(ROUND((x) * (COUNT-1),2));
SET DIFF_RANK = ((x) * (COUNT-1)) - FLOOR(ROUND((x) * (COUNT-1),2));

SET VALEUR_SUP = CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(array_values,',', RANG_FLOOR+2),',',-1) AS DECIMAL);
SET VALEUR_INF = CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(array_values,',', RANG_FLOOR+1),',',-1) AS DECIMAL);

/****
    https://fr.wikipedia.org/wiki/Quantile
    x_j+1 + g (x_j+2 - x_j+1)       
***/
RETURN  Round((VALEUR_INF + (DIFF_RANK* (VALEUR_SUP-VALEUR_INF) ) ),2);

END$$

Пример :

Select centile(3/4,GROUP_CONCAT(lux ORDER BY lux SEPARATOR ',')) as quartile_3
FROM LuxLog
WHERE Sensor=12 AND Lux<>0
person dartaloufe    schedule 26.10.2016