在 SQL 上对粒度样本进行平均最简单的方法
Posted
技术标签:
【中文标题】在 SQL 上对粒度样本进行平均最简单的方法【英文标题】:Averaging granular samples the simplest way on SQL 【发布时间】:2018-05-14 22:59:11 【问题描述】:所以我有一个相当大的表格,其中包含精细的价格变动(使用 MariaDB)。
CREATE TABLE `table` (
`num` int(11) NOT NULL AUTO_INCREMENT,
`datetime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`pairs` varchar(40) NOT NULL,
`price` decimal(16,10) NOT NULL,
`volume` decimal(22,10) NOT NULL,
PRIMARY KEY (`num`),
KEY `datetime_pairs` (`pairs`,`datetime`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
当数据超过 x 天时,我想按输入的类型计算这些价格的每小时平均值。对于这个例子,我需要 7 天。我想出了这个查询。
SELECT `num`, `datetime`, `pairs`, `price`, `volume`,
AVG(`price`) AS `priceAVG`, AVG(`volume`) AS `volumeAVG`
FROM table
WHERE DATE_FORMAT(`datetime`, '%Y-%m-%d %H:00:00')
< DATE_FORMAT(DATE_SUB(NOW(),INTERVAL 7 DAY), '%Y-%m-%d %H:00:00')
GROUP BY DATE_FORMAT(`datetime`, '%Y-%m-%d %H:00:00'), `pairs`
查询运行大约需要 25 秒。我不认为我可以优化它。 这可能会打印出我正在寻找的结果......但是,一旦我有数据可以使用,我真的不确定回答我的问题的最佳做法是什么。
插入这个结果,并删除旧数据?它会弄乱主键号num
,使其与datetime
的排序方式不一致。
使用 SELECT 查询的结果更新旧数据,并删除旧数据减去那些更新的行?这就是我目前正在努力实现的目标......
我认为重复的表可能不是一个选项,因为我有一百个这样的表要处理,而且 cpu 资源也是需要考虑的。我正在使用 cron 和 php 来转换这些查询。我可能每 12 或 24 小时执行一次此操作。
在这种情况下合适的方法是什么?
更新查询是一种现实的处理方式吗?
【问题讨论】:
老天,删除对DATE_FORMAT()
的每一个电话。这样做没有世俗的理由,这将是查询需要这么长时间的原因。
使用 DATE_FORMAT
将阻止 mysql 使用任何索引。如果除了日期时间之外,您还存储自纪元以来经过的小时数并索引该字段,您将获得更好的性能。这样一来,您最终会在一个整数上进行分组,这会容易得多。
【参考方案1】:
有什么理由使用 11 字节 decimal(22,10)
而不是简单的 4 字节 FLOAT
?
在执行GROUP BY
时选择num
、datetime
、pairs
、price
、volume
没有意义。
构建和维护一个汇总表,按小时细分。然后从该表构建报告。 http://mysql.rjweb.org/doc.php/summarytables
两个具有相同(datetime, pairs)
组合的条目是否有任何变化?如果不摆脱 id
并将其设为 2 列 PRIMARY KEY
。
同时,您可以部分优化查询
过去一周到当前时间的每小时平均值,但不包括当前时间:
SELECT LEFT(`datetime`, 13) AS the_hour,
pairs,
AVG(`price`) AS `priceAVG`,
AVG(`volume`) AS `volumeAVG`
FROM `table`
WHERE `datetime` >= DATE_FORMAT(NOW() - INTERVAL 7 DAY), '%Y-%m-%d %H:00:00')
AND `datetime` < DATE_FORMAT(NOW(), '%Y-%m-%d %H:00:00')
GROUP BY LEFT(`datetime`, 13), pairs
还有
INDEX(datetime)
摘要表方法是最复杂的,但可以为您带来最大的收益。
【讨论】:
感谢您的回答。我使用的是小数(22,10),因为我需要小数点右边至少 10 个非常精确的数字,总共至少需要 22 个数字。如果由于某些原因我忽略了我使用的服务器上的一个小时问题(夏令时更改或其他),(日期时间,对)可能会更改并且可能不会重复。因为我问了这个问题,所以我稍微改变了我的查询,还需要分组到分钟(5 分钟)。 现在看起来像这样:SELECTpairs
, AVG(price
) as priceAVG
, AVG(volume
) as volumeAVG
FROM table
WHERE datetime
datetime div 500. div 500 分组 5 分钟并得出平均值。我不知道为什么以及如何,但它似乎在进行快速 sql 检查时工作。我目前的计划有点笨拙,但那是 1. 执行此查询并将其存储在 php 中 2. 选择此范围内的最小主键与另一个查询 3. 删除粒度数据样本(有限空间) 4. 使用适当的 id 插入每一行php 5. 每桌每天执行一次
嗯? “卷”是日期时间+对? DST 可能导致重复时间。考虑TIMESTAMP
。
A DATETIME
,当用于算术时,被视为20171231235959
;使用 div 500
可能不会给你预期的价值。首先转换为秒。
@user46987151 - 听起来你有汇总表的一般理想;只是实现中的一些小问题。【参考方案2】:
-
使用
DATE_FORMAT()
将日期转换为字符串效率低下。直接比较日期。
比较字符串效率低下。直接比较日期。
datetime
字段上没有索引。不,datetime_pairs
不算数,因为:
pairs
不参与。
datetime
不是复合键中的第一项。
所以:
CREATE TABLE `table` (
`num` int(11) NOT NULL AUTO_INCREMENT,
`datetime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`pairs` varchar(40) NOT NULL,
`price` decimal(16,10) NOT NULL,
`volume` decimal(22,10) NOT NULL,
PRIMARY KEY (`num`),
KEY `datetime` (`datetime`), -- change
KEY `pairs` (`pairs`) -- change
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
还有:
SELECT `num`, `datetime`, `pairs`, `price`, `volume`,
AVG(`price`) AS `priceAVG`, AVG(`volume`) AS `volumeAVG`
FROM table
-- DATE(datetime) is the same as 00:00:00 on that day, but is
-- a DATE type that can be efficiently compared
WHERE `datetime` < DATE(DATE_SUB(NOW(),INTERVAL 7 DAY)
GROUP BY DATE(`datetime`), `pairs`
而您仍然会受到GROUP BY DATE(datetime)
的限制,因为没有可使用的索引。您可能想要添加一个简单的 date DATE NOT NULL
列,以便您可以在其上利用索引,尽管上述内容应该已经大大减少了所需的时间。
此外,查询中的列num, datetime, price, volume
将毫无意义,因为它们没有在GROUP BY
语句中引用。
此外,历史数据不会发生变化,因此您永远不必多次汇总每日数据。存储每日聚合(如平均值和计数)足以在顶部构建更大的聚合。例如:
SELECT AVG(daily_count * daily_average) AS 'weekly_average'
FROM daily_aggregates
WHERE datestamp > DATE(NOW() - INTERVAL 7 DAY)
利用缓存。我已经看到了很多很多性能问题,因为开发人员在永远不会改变的巨大历史数据集上反复运行完全相同的计算。像缓存每日聚合这样简单的事情可以将数据集从每年数千或数百万个项目减少到仅 365 个。
【讨论】:
感谢您的意见。请让我处理这个。我需要datetime_pairs
用于使用此索引的另一个选择查询,我会看看我能做些什么来对第一个问题进行排序。至于保存 AVG 结果,您建议创建一个新的 daily_aggregate 表(该表将包含与 100 个表中的每一个相关的 avg 聚合),并避免完全接触超过 7 天的数据的“基本”表? (效仿)
INDEX(datetime, pairs)
足以满足其他选择以及这一选择。 INDEX(datetime)
将是多余的。【参考方案3】:
您似乎也需要时间,而不仅仅是日期?所以我猜只是比较日期部分对你没有帮助。
添加一个persistent generated column,它为您提供来自datetime
的带有小时但没有分钟或秒(它们设置为0)的日期。
ALTER TABLE `table`
ADD (`date_to_the_hour` date AS (date(`datetime`) + INTERVAL hour(`datetime`) HOUR) PERSISTENT);
在该列和pairs
上放置一个索引。
CREATE INDEX `date_to_the_hour_pairs`
ON `table`
(`date_to_the_hour`,
`pairs`);
更改您的选择以与新列进行比较。
SELECT `num`,
`datetime`,
`pairs`,
`price`,
`volume`,
AVG(`price`) `priceAVG`,
AVG(`volume`) `volumeAVG`
FROM `table`
WHERE `date_to_the_hour` < date(now() - INTERVAL 7 DAY) + INTERVAL hour(now()) HOUR
GROUP BY `date_to_the_hour`,
`pairs`;
这可能会加快速度。
【讨论】:
以上是关于在 SQL 上对粒度样本进行平均最简单的方法的主要内容,如果未能解决你的问题,请参考以下文章
简单调和平均数公式 加权调和平均数公式 众数,中位数的典型计算
从 WaveStream 将 2 通道样本读入数组的最简单方法