【问题标题】:Import variable time series data in SQL Server database在 SQL Server 数据库中导入可变时间序列数据
【发布时间】:2016-06-05 21:01:41
【问题描述】:

我已经在堆栈溢出和其他地方寻找解决方案,但找不到处理我必须处理的卷的示例。如果我错过了已在其他地方发布的解决方案,如果有人能指出我正确的方向,我将不胜感激。

我正在尝试从 45 个不同的 Excel 工作表(每个 Excel 工作簿大约 5 个)导入时间序列数据。每个工作表都包含商品价格系列,涵盖每种商品几年的每日价格。

原始 Excel 数据中有一行表示可能存在价格的每一天,一列表示每个商品合约,通常是每月的未来合约。因此,每份合约的点数至少为 30(但不多),而整个表有几千行和 100 多列。 我能够构建一个读取数据并使用 unpivot 的 SSIS 包,将矩阵转换为基于行的记录,其中包含以下列:

Date, Price, Contract

然而,问题在于,在逆透视变换中,我必须为每个变换后的输入列手动指定目标列。所以 45 个工作表,每个包含 100 多列(甚至几百列)的合同我最终会在接下来的几天里手动“硬编码”这些转换......最重要的是,这不是那么灵活/重新 -正如我所希望的那样可用。

附加的原始数据示例(整个 Cocoa 工作表包含 9724 行和 195 列)

以下是另一种商品的反透视的配置方式。 'Destination Column' 必须手动逐行填写。

我希望我只是错过了 unpivot 配置中使这些列动态化的正确步骤。理想情况下,SSIS 解决方案可以在以后再次与格式相同的 Excel 工作簿一起使用。不需要在服务器上运行它,因为它不是经常发生的事情,而是每年最多一次或两次。所以我可以很容易地在 VS 中手动启动它。

我正在努力帮助一位学术研究人员,否则他们会花费大量时间在 Excel 中手动清理和分析数据。

【问题讨论】:

  • 那么,为什么不编写一个动态反透视脚本,然后将其用作数据流中的脚本组件呢?
  • 我猜当有新商品到货或下架时,您的栏目会发生变化吗? SSIS 不会喜欢这样的。您能否指出这些列更改的可能性有多大,它们在工作表/工作簿之间是否不同?这可能是 BIML 的工作。虽然我从来没有做过,但理论上这就是 BIML 的优点 - 根据您的源数据动态创建包。
  • @Ralph:脚本可以处理大量 Excel 工作表中的可变列号和名称吗?你知道我可以看的任何例子吗?
  • @Nick.McDermaid:文件是静态的,它们不会改变,但结构可变。每个都有一组不同的列名和编号。 BIML 听起来像是一种全新的学习语言 - 不幸的是我没有多少时间来完成这个...... :(
  • 只是为了澄清一下:我有目标数据库结构,并且可以毫无问题地使用标准的 unpivot 事务从单个工作表导入数据。我还介绍了循环浏览工作簿和工作表的循环。我只需要能够动态地反透视数据。感谢您迄今为止的所有投入!

标签: sql-server excel ssis time-series


【解决方案1】:

首先,您需要设计和创建要接收数据的表,并尝试一些手动数据输入来检查数据模型。

确保每个电子表格都有足够的标题信息来了解如何处理行。

完成后,我会将工作表保存到带有制表符分隔符的文本文件中。

接下来我会用 Perl 编写一个加载程序。它首先读取标题行并确定将行插入数据库的规则。然后将每一行转换为插入到数据库中。

这是我拥有的发票加载程序的示例(所有权利):

if ($first) {
    $obj->_hdr2keys(0); # convert spreadhseet header into a lookup
    my $hdr = $obj->_copystruct($obj->{ar}[0]);
    my @Hhdr = ('invoice header id');
    my @Hcols = ('invhid');
    my @Htypes = ('serial');
    my @Dhdr = ('invoice detail id');
    my @Dcols = ('invdid','invhid');
    my @Dtypes = ('serial','integer');
    for (my $col=0; $col <= $#{$hdr}; $col++) {
    my $colname = lc($obj->_pomp($hdr->[$col]));
    if ($colname eq 'invoicenumber') {
        push @Hhdr, $hdr->[$col];
        push @Hcols, $colname;
        push @Htypes, 'char(32)';
    }
    elsif ($colname eq 'buysell') {
        push @Hhdr, $hdr->[$col];
        push @Hcols, $colname;
        push @Htypes, 'boolean';
        }
    elsif ($colname eq 'suppliercustomer') {
        push @Hhdr, $hdr->[$col];
        push @Hcols, $colname;
        push @Htypes, 'char(64)';
        }
    elsif ($colname eq 'date') {
        push @Hhdr, 'Transaction Date';
        push @Hcols, 'transactiondate';
        push @Htypes, 'date';
        }
    elsif ($colname eq 'article') {
        push @Dhdr, 'Article id';
        push @Dcols, 'artid';
        push @Dtypes, 'integer';
        push @Dhdr, 'Article Description';
        push @Dcols, 'description';
        push @Dtypes, 'char(64)';
        }
    elsif ($colname eq 'qty') {
        push @Dhdr, $hdr->[$col];
        push @Dcols, $colname;
        push @Dtypes, 'integer';
    }
    elsif ($colname eq 'priceexclbtw') {
        push @Dhdr, $hdr->[$col];
        push @Dcols, $colname;
        push @Dtypes, 'double precision';
        }
    elsif ($colname eq 'btw') {
        push @Dhdr, $hdr->[$col];
        push @Dcols, $colname;
        push @Dtypes, 'real';
        }
    }
    $obj->_getset('INVHar',
    ['invoiceheader',
     ['PK','invhid'],
     ['__COLUMNS__'],
     \@Hcols,
     \@Htypes,
     \@Hhdr
    ]
    );

    $obj->_getset('INVDar',
    ['invoicedetail',
     ['PK','invdid'],
     ['FK','invhid','invoiceheader','invhid'],
     ['FK','artid','article','artid'],
     ['__COLUMNS__'],
     \@Dcols,
     \@Dtypes,
     \@Dhdr
    ]
    );
}
$first = 0;

SALESROW: for (my $i=1; $i <= $#{$obj->{ar}}; $i++) {
    my @Hrow = ('');
    my @Drow = ('');
    my $date = $obj->_selectar('', $i, 'Date');
    $date =~ s/\-/\//g;
    if ($date) {
    $obj->_validCSV('date', $date)
        or die "CSV format error date |$date| in file $file";
    }
    my $invtotal = ($obj->_selectar('', $i, 'Invoice Total incl. BTW'));
    my $article = $obj->_selectar('', $i, 'Article');
    $date or $article or next SALESROW;
    if ($date) {
    push @Hrow, $obj->_selectar('', $i, 'Invoice Number');
    my $buysell = $obj->_selectar('', $i, 'Buy/Sell');
    push @Hrow, ($buysell eq 'S') ? 1 : 0;
    push @Hrow, $obj->_selectar('', $i, 'Supplier/Customer');
    push @Hrow, $date;
    push @{$obj->_getset('INVHar')}, \@Hrow;
    $invhid++;
    }
    push @Drow, $invhid;
    if ($article eq 'E0154') {
    push @Drow, 1;
    }
    elsif ($article eq 'C0154') {
    push @Drow, 2;
    }
    elsif ($article eq 'C0500') {
    push @Drow, 3;
    }
    elsif ($article eq 'C2000') {
    push @Drow, 4;
    }
    elsif ($article eq 'C5000') {
    push @Drow, 5;
    }
    else {
        die "unrecognised sales article $article\n"
      . Dumper($obj->{ar}[$i]);
    }
    push @Drow, undef; # description is in article table
    push @Drow, $obj->_selectar('', $i, 'Qty.');
    push @Drow, $obj->_selectar('', $i, 'Price excl. BTW');
    push @Drow, $obj->_selectar('', $i, 'BTW %');
    push @{$obj->_getset('INVDar')}, \@Drow;
}

这会在产品表已从另一个电子表格加载后创建发票的抬头和详细记录。

在上面的示例中,创建了两个数组,INVHar 和 INVDar。当它们准备好时,调用例程将它们加载到数据库中,如下所示。在下一个代码示例中,创建了表以及行,并且更新了元数据库以加载未来的表和管理现有表的外键。在前面的 sn-p 中创建的数组包含创建表和插入行所需的所有信息。还有一个简单的例程 _DBdatacnv,用于在电子表格中的格式和数据库中所需的格式之间进行转换。例如,电子表格中的货币符号需要在插入前去除。

sub _arr2db {
my ($obj) = @_;
my $ar = $obj->_copystruct($obj->_getset('ar'));
my $dbh = $obj->_getset('CDBh');
my $mdbh = $obj->_getset('MDBh');
my $table = shift @$ar;
$mdbh->{AutoCommit} = 0;
$dbh->{AutoCommit} = 0;

my @tables = $mdbh->selectrow_array(
    "SELECT id FROM mtables
     WHERE name = \'$table\'"
);
my $id = $tables[0] || '';
if ($id) {
$mdbh->do("DELETE FROM mcolumns where tblid=$id");
$mdbh->do("DELETE FROM mtables where id=$id");
}
# process constraints
my %constraint;
while ($#{$ar} >= 0
  and $ar->[0][0] ne '__COLUMNS__') {
my $cts = shift @$ar;
my $type = shift @$cts;
if ($type eq 'PK') {
    my $pk = shift @$cts;
    $constraint{$pk} ||= '';
    $constraint{$pk} .= ' PRIMARY KEY';
    @$cts and die "unsupported compound key for $table";
}
elsif ($type eq 'FK') {
    my ($col, $ft, $fk) = @$cts;
    $ft && $fk or die "incomplete FK declaration in CSV for $table";
    $constraint{$col} ||= '';
    $constraint{$col} .=
        sprintf( ' REFERENCES %s(%s)', $ft, $fk );
}
elsif ($type eq 'UNIQUE') {
    while (my $uk = shift @$cts) {
    $constraint{$uk} ||= '';
    $constraint{$uk} .= ' UNIQUE';
    }
}
elsif ($type eq 'NOT NULL') {
    while (my $nk = shift @$cts) {
    $constraint{$nk} ||= '';
    $constraint{$nk} .= ' NOT NULL';
    }
}
else {
    die "unrecognised constraint |$type| for table $table";
}
}
shift @$ar;
unless ($mdbh->do("INSERT INTO mtables (name) values (\'$table\')")) {
warn $mdbh->errstr . ": mtables";
$mdbh->rollback;
die;
}
@tables = $mdbh->selectrow_array(
    "SELECT id FROM mtables
     WHERE name = \'$table\'"
);
$id = shift @tables;
$dbh->do("DROP TABLE IF EXISTS $table CASCADE")
    or die $dbh->errstr;
my $create = "CREATE TABLE $table\n";
my $cols = shift @$ar;
my $types = shift @$ar;
my $desc = shift @$ar;
my $first = 1;
my $last = 0;
for (my $i=0; $i<=$#{$cols}; $i++) {
$last = 1;
if ($first) {
    $first = 0;
    $create .= "( "
}
else {
    $create .= ",\n";
}
$create .= $cols->[$i]
    . ' ' . $obj->_DBcnvtype($types->[$i]);
$constraint{$cols->[$i]}
  and $create .= ' ' . $constraint{$cols->[$i]};
unless ($mdbh->do("INSERT INTO mcolumns (tblid,name,type,description)
    values ($id,\'$cols->[$i]\',\'$types->[$i]\',\'$desc->[$i]\')"))
{
    warn $mdbh->errstr;
    $mdbh->rollback;
    die;
}
}
$last and $create .= ')';
unless ($dbh->do($create)) {
    warn $dbh->errstr;
    $dbh->rollback;
    die;
}
my $count = 0;
while (my $row = shift @$ar) {
$count++;
my $insert = "INSERT INTO $table (";
my $values = 'VALUES (';
my $first = 1;
for (my $i=0; $i<=$#{$cols}; $i++) {
    my $colname = $cols->[$i];
    unless (defined($constraint{$colname})
      and $constraint{$colname} =~ /PRIMARY KEY/) {
    if ($first) {
        $first = 0;
    }
    else {
        $insert .= ', ';
        $values .= ', ';
    }
    $insert .= $colname;
    my $val = $obj->_DBdatacnv('CSV', 'DB',
        $types->[$i],$row->[$i]);
    if ($val eq '%ABORT') {
        $mdbh->rollback;
        die;
    }
    $values .= $val;
    }
}
$insert .= ')' . $values . ')';
unless ($dbh->do($insert)) {
    warn $dbh->errstr;
    warn $insert;
    $mdbh->rollback;
    die;
}
}
NOINSERT: $mdbh->commit;
$dbh->commit;
# warn "inserted $count rows into $table";
}

更新:好的,我将添加从 CSV 转换为数组的通用例程,为上面的 _arr2db 做好准备,适用于我对系统的所有其他情况:电子表格首先增加 PK FK 和其他约束,然后是列标题数据库的名称,一行数据库类型(名义上的,实际的在_DBcnvdatatype中处理)然后是进入元数据库的一行标签,最后是数据行之前的标记COLUMNS插入。

sub _csv2arr {
my ($obj, $csv ) = @_;
my $ar = [];
my $delim = $obj->_getset('csvdelim') || '\,';
my $basename = basename($csv);
$basename =~ s/\.txt$//;
$ar = [$basename];
open my $fh, $csv
  or die "$!: $csv";
while (<$fh>) {
chomp;
my $sa = [];
@$sa = split /$delim/;
push @$ar, $sa;
}
close $fh;
$obj->{ar} = $ar;
}

【讨论】:

  • 顺便说一句,我在使用 postgres 时遇到了日期格式问题,因此我调整了 _DBdatacnv 以将 DD/MM/YYYY 转换为纪元时间,并将其作为 bigint 存储在数据库中。但策略很明确:将 CSV 类型映射到选定的数据库类型。我的示例是将非规范化发票行项目转换为规范化表格,但鉴于这些例程完全通用,反之亦然。
【解决方案2】:

我会以一系列嵌套循环的形式执行此操作:

循环1,遍历文件夹中的所有文件。将文件名传递给下一个循环

循环 2 打开文件,遍历工作表

循环 3 在工作表 X 中,循环通过列 (A) > 1

循环 4 - 循环遍历行:

阅读表格 X,B 行,

从(第 B 行,第 1 列)获取值作为日期,(第 1 行,第 A 列)作为产品。 (B 行,A 列)作为价格 - 写入目的地。

结束循环 4 (可选,在列的末尾记录一些关于行数的元数据)

循环 3 结束 (可选,记录一些关于sheet中列数的元数据)

循环 2 结束 (可选,记录一些关于文件X的张数的元数据)

循环结束 1 (强烈建议 - 记录有关文件 X 的一些元数据和工作表/行/列数 - 您可以稍后测试样本以确保自己的信心)

您可能需要修改其中一个文件的副本,以便测试问题。

  • 工作表为空

  • 无效数据

  • 缺少标题

  • 文本而不是价格

当您发现新的极端情况时,这将增加信心并缩短返工时间。

最终输出表应该是 3 列中的非规范化数据:

Date, 

Product, 

Price

编辑 这是一个链接,显示了如何动态循环遍历 Excel 电子表格(和工作表)的列,以便您可以使用此过程将数据还原为规范化表单

Looping through Excel columns in SSIS

【讨论】:

    猜你喜欢
    • 2018-04-17
    • 1970-01-01
    • 2023-04-08
    • 1970-01-01
    • 2011-09-06
    • 2014-05-18
    • 2011-05-01
    • 1970-01-01
    相关资源
    最近更新 更多