在美洲杯帆船赛中,我们生成大型数据集,在每个时间戳(例如 100Hz),我们可能需要存储 100-1000 个传感器数据通道(例如速度、负载、压力)。我们将其存储在 MS SQL Server 中,并且需要能够检索数据通道的子集进行分析,并执行查询,例如测试中或整个季节中特定传感器的最大压力。
要存储的 channel 集在数千个时间戳中保持不变,但随着新传感器的添加、重命名等,每天都会发生变化......并且根据测试、赛车或模拟, channel 数量可能相差很大。
构造 SQL 表的教科书方法可能是:
选项 1
ChannelNames
+-----------+-------------+
| ChannelID | ChannelName |
+-----------+-------------+
| 50 | Pressure |
| 51 | Speed |
| ... | ... |
+-----------+-------------+
Sessions
+-----------+---------------+-------+----------+
| SessionID | Location | Boat | Helmsman |
+-----------+---------------+-------+----------+
| 789 | San Francisco | BoatA | SailorA |
| 790 | San Francisco | BoatB | SailorB |
| ... | ... | ... | |
+-----------+---------------+-------+----------+
SessionTimestamps
+-------------+-------------+------------------------+
| SessionID | TimestampID | DateTime |
+-------------+-------------+------------------------+
| 789 | 12345 | 2013/08/17 10:30:00:00 |
| 789 | 12346 | 2013/08/17 10:30:00:01 |
| ... | ... | ... |
+-------------+-------------+------------------------+
ChannelData
+-------------+-----------+-----------+
| TimestampID | ChannelID | DataValue |
+-------------+-----------+-----------+
| 12345 | 50 | 1015.23 |
| 12345 | 51 | 12.23 |
| ... | ... | ... |
+-------------+-----------+-----------+
这种结构简洁但效率低下。每个DataValue需要三个存储字段,并且在每个时间戳我们需要INSERT 100-1000行。
如果我们始终拥有相同的 channel ,那么每个时间戳使用一行和如下结构会更明智:
选项 2
+-----------+------------------------+----------+-------+----------+--------+-----+
| SessionID | DateTime | Pressure | Speed | LoadPt | LoadSb | ... |
+-----------+------------------------+----------+-------+----------+--------+-----+
| 789 | 2013/08/17 10:30:00:00 | 1015.23 | 12.23 | 101.12 | 98.23 | ... |
| 789 | 2013/08/17 10:30:00:01 | 1012.51 | 12.44 | 100.33 | 96.82 | ... |
| ... | ... | ... | | | | |
+-----------+------------------------+----------+-------+----------+--------+-----+
然而, channel 每天都在变化,几个月后,列的数量会不断增加,大多数单元格最终都是空的。我们可以为每个新 session 创建一个新表,但是使用表名作为变量感觉不太合适,并且最终会导致数以万计的表 - 而且,在一个季度内查询变得非常困难,数据存储在多个表中。
另一种选择是:
选项3
+-----------+------------------------+----------+----------+----------+----------+-----+
| SessionID | DateTime | Channel1 | Channel2 | Channel3 | Channel4 | ... |
+-----------+------------------------+----------+----------+----------+----------+-----+
| 789 | 2013/08/17 10:30:00:00 | 1015.23 | 12.23 | 101.12 | 98.23 | ... |
| 789 | 2013/08/17 10:30:00:01 | 1012.51 | 12.44 | 100.33 | 96.82 | ... |
| ... | ... | ... | | | | |
+-----------+------------------------+----------+----------+----------+----------+-----+
从 channel 列 ID 到 channel 名称进行查找 - 但这需要 EXEC 或 eval 来执行预先构造的查询以获得我们想要的 channel - 因为 SQL 并未设计为将列名称作为变量。从好的方面来说,当 channel 发生变化时,我们可以重复使用列,但仍然会有许多空单元格,因为表格的宽度必须与我们遇到的最大 channel 数一样宽。使用稀疏表可能会有所帮助,但我对上面的 EXEC/eval 问题感到不舒服。
解决这个问题的正确解决方案是什么,可以提高存储、插入和查询的效率?
最佳答案
我会选择选项 1。
数据完整性第一,优化(如果需要)第二。
其他选项最终会出现大量 NULL
值以及由于未标准化而产生的其他问题。管理数据和进行有效的查询将很困难。
此外,还有一个limit on the number of columns一个表可以有 - 1024,所以如果你有 1000 个传感器/ channel ,你已经接近极限了。即使您将 table 设置为 wide table ,它允许 30,000 列,但表中行的大小仍然存在限制 - 每行 8,060 字节。并且有一定的performance considerations .
在这种情况下,我不会使用宽表,即使我确定每行的数据永远不会超过 8060 字节,并且不断增长的 channel 数永远不会超过 30,000。
我没有发现在选项 1 中插入 100 - 1000 行与在其他选项中插入 1 行有什么问题。要有效地执行此类 INSERT
,不要创建 1000 个单独的 INSERT
语句,而是批量执行。在我的系统的不同地方,我使用以下两种方法:
1) 构建一个长 INSERT
语句
INSERT INTO ChannelData (TimestampID, ChannelID, DataValue) VALUES
(12345, 50, 1015.23),
(12345, 51, 12.23),
...
(), (), (), (), ........... ();
包含 1000 行,并在一个事务中像正常的 INSERT
一样执行它,而不是 1000 个事务(检查 syntax details )。
2) 有一个接受 table-valued parameter 的存储过程。调用这样的过程,将 1000 行作为表传递。
CREATE TYPE [dbo].[ChannelDataTableType] AS TABLE(
[TimestampID] [int] NOT NULL,
[ChannelID] [int] NOT NULL,
[DataValue] [float] NOT NULL
)
GO
CREATE PROCEDURE [dbo].[InsertChannelData]
-- Add the parameters for the stored procedure here
@ParamRows dbo.ChannelDataTableType READONLY
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
BEGIN TRANSACTION;
BEGIN TRY
INSERT INTO [dbo].[ChannelData]
([TimestampID],
[ChannelID],
[DataValue])
SELECT
TT.[TimestampID]
,TT.[ChannelID]
,TT.[DataValue]
FROM
@ParamRows AS TT
;
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
END
GO
如果可能,在插入之前累积多个时间戳的数据,以使批处理更大。您应该尝试使用您的系统并找到批处理的最佳大小。我使用存储过程批量处理了大约 10K 行。
如果您的数据每秒来自传感器 100 次,那么我首先会将传入的原始数据转储到一些非常简单的 CSV 文件中,并使用一个并行后台进程将其分块插入数据库中。换句话说,为传入数据提供一些缓冲区,这样,如果服务器无法处理传入数据量,您就不会丢失数据。
根据您的评论,当您说某些 channel 可能更有趣并被查询多次,而其他 channel 则不太有趣时,这是我会考虑的一项优化。除了所有 channel 都有一个表 ChannelData
之外,还有另一个表 InterestingChannelData
。 ChannelData
将拥有整套数据,以防万一。 InterestingChannelData
将仅包含最感兴趣 channel 的子集。它应该小得多,并且查询它应该花费更少的时间。无论如何,这是建立在正确规范化结构之上的优化(非规范化/数据重复)。
关于sql - 存储具有可变列数的大型 SQL 数据集,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30112900/