MySQL: невозможно выбрать записи из определенных разделов?

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

Таблица Report_Summary:

CREATE TABLE `Report_Summary` (
  `PartitionsID` int(4) unsigned NOT NULL,
  `ReportTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `Amount` int(10) NOT NULL,
  UNIQUE KEY `UNIQUE` (`PartitionsID`,`ReportTime`),
  KEY `PartitionsID` (`PartitionsID`),
  KEY `ReportTime` (`ReportTime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=COMPRESSED
/*!50100 PARTITION BY HASH (PartitionsID)
PARTITIONS 366 */

Мой текущий запрос:

SELECT DATE(RS.ReportTime) AS ReportDate, SUM(RS.Amount) AS Total
FROM Report_Summary RS
WHERE RS.ReportTime >= '2014-12-26 00:00:00' AND RS.ReportTime <= '2014-12-30 23:59:59' AND 
      RS.PartitionsID BETWEEN DAYOFYEAR('2014-12-26 00:00:00') AND DAYOFYEAR('2014-12-30 23:59:59')
GROUP BY ReportDate; 

Приведенный выше запрос отлично работает и использует разделы с p360 по p364 для получения данных. Теперь проблема заключается в том, что когда я передаю fromDate в «2014-12-26» и toDate в «2015-01-01», тогда приведенный выше запрос не будет работать. Поскольку день года для '2015-01-01' равен 1, мои условия не сработали.

Теперь я попытался передать значение в операторе IN, тогда он отлично работает при проверке базы данных ниже запроса:

SELECT DATE(RS.ReportTime) AS ReportDate, SUM(RS.Amount) AS Total
FROM Report_Summary RS
WHERE RS.ReportTime >= '2014-12-26 00:00:00' AND RS.ReportTime <= '2015-01-01 23:59:59' AND 
      RS.PartitionsID IN (360,361,362,363,364,365,1)
GROUP BY ReportDate; 

Чтобы сгенерировать вышеприведенный сценарий, я создал функцию и передал две даты и сгенерировал строку идентификаторов, разделенных запятыми.

SELECT GenerateRange('2014-12-26 00:00:00', '2015-01-01 23:59:59');

Который возвращает мне данные как:

'360,361,362,363,364,365,366,1'

И я попытался использовать эту функцию в своем запросе, поэтому я изменил свой запрос, как показано ниже:

SELECT DATE(RS.ReportTime) AS ReportDate, SUM(RS.Amount) AS Total
FROM Report_Summary RS
WHERE RS.ReportTime >= '2014-12-26 00:00:00' AND RS.ReportTime <= '2015-01-01 23:59:59' AND 
      FIND_IN_SET(RS.PartitionsID, GenerateRange('2014-12-26 00:00:00', '2015-01-01 00:00:00'))
GROUP BY ReportDate; 

Затем я проверил план выполнения вышеуказанного запроса, используя EXPLAIN PARTITION SELECT.... И я обнаружил, что мое условие не сработает. Он использует все разделы для получения данных. Я хочу использовать только определенные разделы этих дат. Необходимо проверить только эти 360,361,362,363,364,365,366,1 разделы, означающие p360 до p366 и p1.

Почему мой запрос не работает? И это неправильный способ реализовать это, тогда мне нужно решение. Как я могу этого добиться?

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

Спасибо...


person Saharsh Shah    schedule 01.01.2015    source источник
comment
Что вы пытаетесь сделать со своим запросом? Какого результата вы ждете?   -  person akmozo    schedule 04.01.2015
comment
@akmozo Мне нужен рабочий запрос, который будет использовать разделы, необходимые для этого условия. Но мой текущий запрос с условием использования всех разделов неверен.   -  person Saharsh Shah    schedule 05.01.2015
comment
Мы разделяем вас по DAYOFYEAR? У вас будет много запросов, сравнивающих один день в году с одним днем ​​в предыдущем году (годах)? Если нет, то вам, вероятно, лучше разбить по годам или по годам или по чему-то последовательному, если ваши запросы будут в основном последовательным доступом к данным.   -  person BateTech    schedule 07.01.2015
comment
@BateTech Разделы уже созданы, поэтому мне нужно их использовать, потому что я не могу это изменить.   -  person Saharsh Shah    schedule 07.01.2015
comment
@SaharshShah Я обновил свой ответ и добавил вариант 3, который может подойти вам, поскольку он не использует OR в предложении where.   -  person BateTech    schedule 07.01.2015
comment
@SaharshShah, когда вы делаете EXPLAIN PARTITION в запросе с оператором PartitionsID IN(hard coded list), вторым запросом в вашем вопросе, вы обнаружите, что он правильно использует ваши разделы? Просьба уточнить.   -  person O. Jones    schedule 08.01.2015


Ответы (4)


Есть несколько вариантов, которые я могу придумать.

  1. Создайте case заявления, которые охватывают многолетние критерии поиска.
  2. Создайте таблицу CalendarDays и используйте ее, чтобы получить отдельный список DayOfYear для вашего предложения in.
  3. Вариант варианта 1, но с использованием union для поиска в каждом диапазоне отдельно

Вариант 1. Использование операторов case. Это некрасиво, но, кажется, работает. Существует сценарий, в котором эта опция может искать один дополнительный раздел, 366, если запрос охватывает годы невисокосного года. Также я не уверен, что оптимизатору понравится OR в фильтре RS.ParitionsID, но вы можете попробовать.

SELECT DATE(RS.ReportTime) AS ReportDate, SUM(RS.Amount) AS Total
FROM Report_Summary RS
WHERE RS.ReportTime >= @startDate AND RS.ReportTime <= @endDate
    AND 
    (
    RS.PartitionsID BETWEEN 
        CASE 
            WHEN
                --more than one year, search all days 
                year(@endDate) - year(@startDate) > 1
                --one full year difference 
                OR year(@endDate) - year(@startDate) = 1 
                    AND DAYOFYEAR(@startDate) <= DAYOFYEAR(@endDate)
            THEN 1
            ELSE DAYOFYEAR(@startDate)
        END
        and 
        CASE
            WHEN 
                --query spans the end of a year
                year(@endDate) - year(@startDate) >= 1
            THEN 366
            ELSE DAYOFYEAR(@endDate)
        END
    --Additional query to search less than portion of next year
    OR RS.PartitionsID <=
        CASE
            WHEN year(@endDate) - year(@startDate) > 1
                OR DAYOFYEAR(@startDate) > DAYOFYEAR(@endDate)
            THEN DAYOFYEAR(@endDate)
            ELSE NULL
        END
    )
GROUP BY ReportDate;

Вариант 2. Использование таблицы CalendarDays. Этот вариант намного чище. Недостатком является то, что вам нужно будет создать новую таблицу CalendarDays, если у вас ее нет.

SELECT DATE(RS.ReportTime) AS ReportDate, SUM(RS.Amount) AS Total
FROM Report_Summary RS
WHERE RS.ReportTime >= @startDate AND RS.ReportTime <= @endDate
    AND RS.PartitionsID IN
    (
        SELECT DISTINCT DAYOFYEAR(c.calDate) 
        FROM dbo.calendarDays c
        WHERE c.calDate >= @startDate and c.calDate <= @endDate
    )

EDIT: Вариант 3: вариант варианта 1, но с использованием Union All для поиска по каждому диапазону отдельно. Идея заключается в том, что, поскольку в операторе нет OR, оптимизатор сможет применить сокращение раздела. Примечание: обычно я не работаю в MySQL, поэтому мой синтаксис может немного отличаться, но общая идея присутствует.

DECLARE @startDate datetime, @endDate datetime;
DECLARE @rangeOneStart datetime, @rangeOneEnd datetime, @rangeTwoStart datetime, @rangeTwoEnd datetime;

SELECT @rangeOneStart := 
        CASE 
            WHEN
                --more than one year, search all days 
                year(@endDate) - year(@startDate) > 1
                --one full year difference 
                OR year(@endDate) - year(@startDate) = 1 
                    AND DAYOFYEAR(@startDate) <= DAYOFYEAR(@endDate)
            THEN 1
            ELSE DAYOFYEAR(@startDate)
        END
    , @rangeOneEnd := 
        CASE
            WHEN 
                --query spans the end of a year
                year(@endDate) - year(@startDate) >= 1
            THEN 366
            ELSE DAYOFYEAR(@endDate)
        END 
    , @rangeTwoStart := 1
    , @rangeTwoEnd := 
        CASE
            WHEN year(@endDate) - year(@startDate) > 1
                OR DAYOFYEAR(@startDate) > DAYOFYEAR(@endDate)
            THEN DAYOFYEAR(@endDate)
            ELSE NULL
        END
;

SELECT t.ReportDate, sum(t.Amount) as Total
FROM 
(
    SELECT DATE(RS.ReportTime) AS ReportDate, RS.Amount
    FROM Report_Summary RS
    WHERE RS.PartitionsID BETWEEN @rangeOneStart AND @rangeOneEnd
        AND RS.ReportTime >= @startDate AND RS.ReportTime <= @endDate

    UNION ALL

    SELECT DATE(RS.ReportTime) AS ReportDate, RS.Amount
    FROM Report_Summary RS
    WHERE RS.PartitionsID BETWEEN @rangeTwoStart AND @rangeTwoEnd
        AND @rangeTwoEnd IS NOT NULL
        AND RS.ReportTime >= @startDate AND RS.ReportTime <= @endDate
) t
GROUP BY ReportDate;
person BateTech    schedule 05.01.2015
comment
Спасибо за ваш ценный ответ, но я уже пробовал оператор CASE, и он также не использует правильные разделы и индексы для извлечения данных. Опция Sencond также не будет учитывать правильные разделы и индексы. - person Saharsh Shah; 06.01.2015
comment
Что, если вы добавите подсказку индекса в предложение FROM, например FROM Report_Summary RS USE KEY (UNIQUE) , а также переместите так, чтобы PartitionsID был первым оператором в предложении where, а затем снова попробуете вариант 2? dev.mysql.com/doc/refman/5.7/en/ index-hints.html - person BateTech; 06.01.2015
comment
Это будет использовать индекс для этого запроса, но не изменит использование разделов. Он будет использовать все разделы вместо определенных разделов, и я хочу, чтобы в моем запросе использовались определенные разделы. Так что index не будет работать в моем случае. - person Saharsh Shah; 06.01.2015
comment
Я только что обновил этот ответ и добавил вариант 3, который может вам подойти. - person BateTech; 07.01.2015
comment
Я совершенно уверен, что только решение CalendarDays принесет разумный план выполнения и время выполнения. Такая таблица также необходима, когда у вас могут быть нулевые дни, то есть дни без ошибок или подписок, и вы хотите показать также эти «пробелы». - person flaschenpost; 12.01.2015

Чтобы приступить к решению этой проблемы, вам нужен подзапрос для заданного диапазона дат, возвращающий набор результатов, состоящий из всех значений DAYOFYEAR() в этом диапазоне.

Давайте разберемся с этим. Для начала нам нужен запрос, который может вернуть последовательность всех целых чисел от 0 до хотя бы 366. Вот этот запрос. Он возвращает столбец seq значений 0-624.

SELECT A.N + 5*(B.N + 5*(C.N + 5*(D.N))) AS seq
  FROM (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 
                      UNION SELECT 3 UNION SELECT 4) AS A
  JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                      UNION SELECT 3 UNION SELECT 4) AS B
  JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                      UNION SELECT 3 UNION SELECT 4) AS C
  JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                      UNION SELECT 3 UNION SELECT 4) AS D

(Это простой трюк с перекрестным соединением для генерации всех комбинаций из 5 ** 4 чисел.)

Затем нам нужно использовать это для создания списка значений DAYOFYEAR(). Давайте использовать даты начала и окончания для примера. Этот запрос генерирует набор результатов, содержащий набор целых чисел, показывающих дни года в этом диапазоне дат.

SELECT DISTINCT DAYOFYEAR(first_day + INTERVAL seq DAY) doy
  FROM (SELECT DATE('2014-12-26 00:00:00') AS first_day,
               DATE('2015-01-01 23:59:59') AS last_day
       ) params
  JOIN (
         SELECT A.N + 5*(B.N + 5*(C.N + 5*(D.N))) AS seq
           FROM (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 
                               UNION SELECT 3 UNION SELECT 4) AS A
           JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                               UNION SELECT 3 UNION SELECT 4) AS B
           JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                               UNION SELECT 3 UNION SELECT 4) AS C
           JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                               UNION SELECT 3 UNION SELECT 4) AS D
       ) seq ON seq.seq <= TIMESTAMPDIFF(DAY,first_day,last_day)
 ORDER BY 1

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

Наконец, вы можете использовать этот запрос в предложении PartitionsID IN (). Это будет выглядеть так.

SELECT DATE(RS.ReportTime) AS ReportDate, SUM(RS.Amount) AS Total
  FROM Report_Summary RS
 WHERE RS.ReportTime >= '2014-12-26 00:00:00'
   AND RS.ReportTime <= '2015-01-01 23:59:59'
   AND RS.PartitionsID 
     IN (
         SELECT DISTINCT DAYOFYEAR(first_day + INTERVAL seq DAY) doy
           FROM (SELECT DATE('2014-12-26 00:00:00') AS first_day,
                        DATE('2015-01-01 23:59:59') AS last_day
                ) params
           JOIN (
                  SELECT A.N + 5*(B.N + 5*(C.N + 5*(D.N))) AS seq
                    FROM (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 
                                        UNION SELECT 3 UNION SELECT 4) AS A
                    JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                                        UNION SELECT 3 UNION SELECT 4) AS B
                    JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                                        UNION SELECT 3 UNION SELECT 4) AS C
                    JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2
                                        UNION SELECT 3 UNION SELECT 4) AS D
                ) seq ON seq.seq <= TIMESTAMPDIFF(DAY,first_day,last_day)
          ORDER BY 1
         ) 
GROUP BY ReportDate; 

Это должно сделать это за вас.

Если вы используете MariaDB 10+, существуют встроенные таблицы последовательности с именами вроде seq_0_to_624.

Здесь есть запись на эту тему:

http://www.plumislandmedia.net/mysql/filling-missing-data-sequences-cardinal-integers/

person O. Jones    schedule 08.01.2015
comment
Я тестировал это, но запрос использует все разделы вместо определенных разделов. А также я пытался раньше, создав таблицу с 366 записями чисел, и попытался СОЕДИНИТЬ таблицу с запросом, но все же у меня не получилось. - person Saharsh Shah; 09.01.2015
comment
Я предлагаю вам выполнить тест производительности до и после для фактического запроса, а не только EXPLAIN, опустив или вставив AND RS.PartitionsID IN (...) часть оператора. Возможно, вы все еще получаете выигрыш в производительности от попытки перечислить необходимые разделы, даже если это не отображается в EXPLAIN. Конечно, может случиться так, что в ближайшем будущем у вас будет UNION ALL множество однодневных запросов. - person O. Jones; 09.01.2015

Я получил решения для этого, я изменил свою логику хранения столбца PartitionsId в своей таблице. Сначала я сохраняю столбец DayOfYear(reportTime) в столбце PartitionsId. Теперь я изменил эту логику, сохранив TO_DAYS(reportTime) и сохранив в столбце PartitionsId.

Итак, моя структура таблицы выглядит следующим образом:

CREATE TABLE `Report_Summary` (
  `PartitionsID` int(10) unsigned NOT NULL,
  `ReportTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `Amount` int(10) NOT NULL,
  UNIQUE KEY `UNIQUE` (`PartitionsID`,`ReportTime`),
  KEY `PartitionsID` (`PartitionsID`),
  KEY `ReportTime` (`ReportTime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=COMPRESSED
/*!50100 PARTITION BY HASH (PartitionsID)
PARTITIONS 366 */

INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735928','2014-12-26 11:46:12','100');
INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735929','2014-12-27 11:46:23','50');
INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735930','2014-12-28 11:46:37','44');
INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735931','2014-12-29 11:46:49','15');
INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735932','2014-12-30 11:46:59','56');
INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735933','2014-12-31 11:47:22','68');
INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735934','2015-01-01 11:47:35','76');
INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735935','2015-01-02 11:47:43','88');
INSERT INTO `Report_Summary` (`PartitionsID`, `ReportTime`, `Amount`) VALUES('735936','2015-01-03 11:47:59','77');

Посмотрите ДЕМО-ПРОГРАММУ SQL FIDDLE:

Мой запрос:

EXPLAIN PARTITIONS 
SELECT DATE(RS.ReportTime) AS ReportDate, SUM(RS.Amount) AS Total
FROM Report_Summary RS
WHERE RS.ReportTime >= '2014-12-26 00:00:00' AND RS.ReportTime <= '2015-01-01 23:59:59' AND 
      RS.PartitionsID BETWEEN TO_DAYS('2014-12-26 00:00:00') AND TO_DAYS('2015-01-01 23:59:59')
GROUP BY ReportDate; 

Приведенный выше запрос сканирует определенные разделы, которые мне нужны, а также использует правильный индекс. Итак, я пришел к правильному решению после изменения логики столбца PartitionsId.

Спасибо за все ответы и большое спасибо каждому за потраченное время...

person Saharsh Shah    schedule 11.01.2015
comment
Будьте осторожны: когда вы работаете дольше, вы получите много разделов, так как каждый новый день создает один. Я бы определенно предложил постоянную таблицу календаря с одной строкой на каждый день и правильным номером раздела, из которого вы можете выбрать часть where in. - person flaschenpost; 12.01.2015
comment
Имея только 366 разделов, вы, вероятно, столкнетесь с той же проблемой, что и у вас изначально, только разрыв между PartitionsID 366 и 1 будет где-то другим, чем 31 декабря - 1 января. - person BateTech; 12.01.2015
comment
Я хочу сказать, что, поскольку вы используете разбиение HASH, ваш раздел # создается с использованием формулы MOD(TO_DAYS(ReportTime), 366) (dev.mysql.com/doc/refman/5.7/en/partitioning-hash.html), поэтому теперь сброс вашего раздела с 365 обратно на 0 произойдет примерно в 2015-04 гг. -02 вместо 2014-12-31. Таким образом, ваш SQLFiddle не является решением исходной проблемы, потому что он не охватывает диапазон дат, охватывающий этот раздел # reset. MySQL может справиться с этим сбросом при использовании TO_DAYS намного лучше, чем при использовании DAYOFYEAR, поскольку TO_DAYS fn является линейным, но ваш пример этого не показывает. - person BateTech; 12.01.2015
comment
Отказаться от BY HASH. @BateTech объясняет, почему. BY RANGE было бы лучше, но все же не так хорошо, как отказаться от РАЗДЕЛЕНИЯ и просто иметь PRIMARY KEY(ReportTime). Цель секционирования — сократить количество операций ввода-вывода, необходимых для выполнения задачи. Ни одно из обсуждавшихся до сих пор решений для разбиения не работает лучше, чем это решение без разбиения. - person Rick James; 06.03.2015

Основываясь на вашем SELECT, вам действительно нужна техника хранилища данных, называемая «Сводные таблицы». При этом вы суммируете данные каждый день (или час, или что-то еще) и сохраняете промежуточные итоги в гораздо меньшей таблице. Затем «отчет» просматривает эту таблицу и суммирует промежуточные итоги. Это часто в 10 раз быстрее, чем сканирование необработанных данных методом грубой силы. Дополнительные сведения: http://mysql.rjweb.org/doc.php/datawarehouse.

Это устраняет необходимость в РАЗДЕЛЕНИИ либо необработанных данных («таблицы фактов»), либо сводной таблицы.

Однако, если вам нужно очистить старые данные, то разбиение на разделы может пригодиться из-за DROP PARTITION. Для этого вы должны использовать BY RANGE(TO_DAYS(...)), а не BY HASH.

person Rick James    schedule 05.03.2015