MySQL:无法从特定分区中选择记录?

Posted

技术标签:

【中文标题】MySQL:无法从特定分区中选择记录?【英文标题】:MySQL: Unable to select the records from specific partitions? 【发布时间】:2015-02-28 03:10:08 【问题描述】:

我正在使用 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; 

上述查询运行良好,并使用分区 p360p364 来获取数据。现在的问题是当我将 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; 

为了生成上述场景,我创建了一个函数并传递了两个日期并生成了一个逗号分隔的 ID 字符串

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 分区表示 p360p366p1

为什么我的查询不起作用?这不是实现这个的正确方法然后我想要解决方案我该如何实现这个?

我从编码中知道我可以实现它,但我必须编写一个查询来实现它。

谢谢...

【问题讨论】:

你想对你的请求做什么?你在等什么样的结果? @akmozo 我想要一个工作查询,它​​将利用该条件所需的分区。但是我当前的查询条件是使用错误的所有分区。 我们是按 DAYOFYEAR 分区吗?您是否会有很多查询将一年中的某一天与前一年的某一天进行比较?如果不是,那么如果您的查询主要是顺序数据访问,那么您最好按 Year 或 YearMonth 或按顺序进行分区。 @BateTech 分区已经创建,所以我需要使用它,因为我无法更改它。 @SaharshShah 我已经更新了我的答案并添加了一个可能对您有用的“选项 3”,因为它在 where 子句中不使用 OR 【参考方案1】:

我能想到几个选项。

    创建涵盖多年搜索条件的 case 语句。 创建一个CalendarDays 表并使用它为您的in 子句获取DayOfYear 的不同列表。 选项 1 的变体,但使用 union 分别搜索每个范围

选项 1: 使用 case 语句。它不漂亮,但似乎有效。如果查询跨越非闰年,则此选项可能会搜索一个额外的分区 366。另外我不确定优化器是否会喜欢RS.ParitionsID 过滤器中的OR,但您可以尝试一下。

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;

【讨论】:

感谢您的宝贵回复,但我已经尝试过 CASE 语句,它也没有使用正确的分区和索引来获取数据。 Sencond 选项也不会考虑正确的分区和索引 如果您在 FROM 子句中添加一个索引提示,如 FROM Report_Summary RS USE KEY (UNIQUE) ,并移动以使 PartitionsID 是 where 子句中的第一个语句,然后再次尝试选项 2,该怎么办? dev.mysql.com/doc/refman/5.7/en/index-hints.html 这将利用该查询上的索引,但不会更改分区的使用。它将使用所有分区而不是特定分区,并且我想使用特定分区应该由我的查询使用。所以索引在我的情况下不起作用。 我刚刚更新了这个答案并添加了可能对你有用的选项 3。 我很确定只有CalendarDays方案才能带来合理的执行计划和执行时间。当您可能有零天时,也需要这样的表格,因此没有错误或订阅的天数,并且还想显示那些“差距”。【参考方案2】:

要开始解决此问题,您需要一个子查询,以在给定日期范围的情况下返回一个包含该范围内所有 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+,则有 built in sequence tables 命名为 seq_0_to_624

这里有一篇关于这个主题的文章:

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

【讨论】:

我已经对此进行了测试,但查询使用所有分区而不是特定分区。而且我之前也尝试过创建一个包含 366 个数字条目的表,并尝试使用查询 JOIN 表,但仍然失败。 我建议您对实际查询进行前后性能测试,而不仅仅是EXPLAIN,省略或放入AND RS.PartitionsID IN (...)部分陈述。即使在EXPLAIN 中没有出现,尝试列出所需的分区时,您仍可能获得性能优势。当然,也有可能是你在不久的将来会有很多一日查询的UNION ALL【参考方案3】:

我得到了解决方案,我改变了在表中存储 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 DEMO

我的查询是:

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 列的逻辑后找到了正确的解决方案。

感谢所有回复,非常感谢大家的时间...

【讨论】:

小心:当你运行更长的时间时,你会得到很多分区,因为每一天都会创建一个。我肯定会建议一个持久的日历表,其中每天有一行和正确的分区号,您可以从中选择 where in 部分。 只有 366 个分区,您可能会遇到最初遇到的相同问题,只有 PartitionsID 366 和 1 之间的中断将在 12 月 31 日至 1 月 1 日之外的某个地方。 我的意思是,由于您使用的是 HASH 分区,因此您的分区 # 是使用公式 MOD(TO_DAYS(ReportTime), 366) (dev.mysql.com/doc/refman/5.7/en/partitioning-hash.html) 生成的,所以现在您的分区 # 从 365 “重置”回 0将在 2015 年 4 月 2 日左右发生,而不是 2014 年 12 月 31 日。因此,您的 SQLFiddle 不能证明可以解决原始问题,因为它不涵盖跨越此分区 #“reset”的日期范围。使用TO_DAYS 时,MySQL 可能比使用DAYOFYEAR 更好地处理此“重置”,因为TO_DAYS fn 是线性的,但您的示例没有显示这一点。 放弃BY HASH。 @BateTech 解释了原因。 BY RANGE 会更好,但仍然不如放弃 PARTITIONing 并简单地拥有 PRIMARY KEY(ReportTime)。分区的目标是减少执行任务所需的 I/O。到目前为止,没有讨论过的分区解决方案比这种非分区解决方案更好。【参考方案4】:

根据您的选择,您真正需要的是一种称为“汇总表”的数据仓库技术。这样,您每天(或每小时或其他任何时间)汇总数据并将小计存储在一个小得多的表中。然后“报告”查看该表并汇总小计。这通常比原始数据的强力扫描快 10 倍。更多详情:http://mysql.rjweb.org/doc.php/datawarehouse.

这样做消除了对原始数据(“事实表”)或汇总表进行分区的需要。

但是,如果您需要清除旧数据,那么 PARTITIONing 可以派上用场,因为 DROP PARTITION。为此,您将使用 BY RANGE(TO_DAYS(...)),而不是 BY HASH。

【讨论】:

以上是关于MySQL:无法从特定分区中选择记录?的主要内容,如果未能解决你的问题,请参考以下文章

MySQL 从特定值中选择

无法从 spark sql 插入配置单元分区表

是否可以从分区中的每个聚类键Y中选择X记录?

Mysql无法选择表中的记录

无法从 information_schema (MySQL/MariaDB) 访问列名

Linux中如何创建新分区啊?