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
    )

РЕДАКТИРАНЕ: Опция 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 и той също не използва правилни дялове и индекси за извличане на данните. Другата опция също няма да вземе предвид правилните дялове и индекси - 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
Това ще използва индекс на тази заявка, но няма да промени използването на дялове. Той ще използва всички дялове вместо конкретни дялове и искам да използвам конкретни дялове, които трябва да се използват от моята заявка. Така че индексът няма да работи в моя случай. - 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 не доказва решение за първоначалния проблем, защото не покрива период от време, който обхваща този дял # нулиране. MySQL може да се справи с това нулиране, когато използва TO_DAYS много по-добре, отколкото използва DAYOFYEAR, тъй като TO_DAYS fn е линеен, но вашият пример не показва това. - person BateTech; 12.01.2015
comment
Изоставяне на BY HASH. @BateTech обяснява защо. BY RANGE би било по-добре, но все пак не толкова добро, колкото изоставянето на PARTITIONing и просто PRIMARY KEY(ReportTime). Целта на разделянето е да се намали I/O, необходим за изпълнение на задача. Нито едно решение за разделяне, обсъждано досега, не се справя по-добре от това решение без PARTITION. - person Rick James; 06.03.2015

Въз основа на вашия SELECT, това, от което наистина се нуждаете, е техника за съхранение на данни, наречена "Обобщени таблици". С такъв, вие обобщавате данните всеки ден (или час или каквото и да е) и съхранявате междинните суми в много по-малка таблица. След това "отчетът" разглежда тази таблица и сумира междинните суми. Това често е 10 пъти по-бързо от сканирането с груба сила на необработените данни. Повече подробности: http://mysql.rjweb.org/doc.php/datawarehouse .

Правейки това, елиминира необходимостта от PARTITIONing или в необработените данни ("Таблица с факти"), или в обобщената таблица.

Въпреки това, ако трябва да изчистите старите данни, тогава PARTITIONing може да бъде полезно поради DROP PARTITION. За това бихте използвали BY RANGE(TO_DAYS(...)), а не BY HASH.

person Rick James    schedule 05.03.2015