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;
上述查询运行良好,并使用分区 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;
为了生成上述场景,我创建了一个函数并传递了两个日期并生成了一个逗号分隔的 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 分区表示 p360 到 p366 和 p1。
为什么我的查询不起作用?这不是实现这个的正确方法然后我想要解决方案我该如何实现这个?
我从编码中知道我可以实现它,但我必须编写一个查询来实现它。
谢谢...
【问题讨论】:
你想对你的请求做什么?你在等什么样的结果? @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:无法从特定分区中选择记录?的主要内容,如果未能解决你的问题,请参考以下文章