sql - 存储具有可变列数的大型 SQL 数据集

标签 sql sql-server

在美洲杯帆船赛中,我们生成大型数据集,在每个时间戳(例如 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 之外,还有另一个表 InterestingChannelDataChannelData 将拥有整套数据,以防万一。 InterestingChannelData 将仅包含最感兴趣 channel 的子集。它应该小得多,并且查询它应该花费更少的时间。无论如何,这是建立在正确规范化结构之上的优化(非规范化/数据重复)。

关于sql - 存储具有可变列数的大型 SQL 数据集,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30112900/

相关文章:

sql - 从时间戳 SQL 创建队列

sql-server - 将 varchar dd/mm/yyyy 转换为 dd/mm/yyyy 日期时间

sql-server - 使用 SQL Server 2008 更改跟踪同步框架

node.js - 运行azure函数时找不到mssql Node js包

sql-server - 无法执行 SQL 查询错误 : Msg 2714, Level 16, State 6, Line 2 数据库中已经有一个名为 'Emp' 的对象

sql-server - 数据库中已经有一个名为 I_XXXRECID 的对象

mysql - 在sql中的一行中转置多个值

mysql - 显示正确的 "most visited"日期

php - 来自同一用户表的复杂 Zend 查询

sql - 唯一的第一个实例返回值 1