【问题标题】:Dynamic Multi Insert with DBI placeholders for many sets of VALUES用于多组 VALUES 的带有 DBI 占位符的动态多插入
【发布时间】:2012-09-25 18:19:34
【问题描述】:

我正在构建一个动态 SQL 语句,它将通过准备好的 DBI 语句插入一组或多组 VALUES,我的问题是:

由于我有一个动态数量的VALUES 集,并且我需要添加尽可能多的( ?, ?, ?),( ?, ?, ?) 等来扩展语句INSERT INTO `tblname` ( $columnsString ) VALUES,以便使用占位符和绑定值仅提交一个查询 - 是这是首选方法(最有效等,如果可能的话,效率背后的推理将有助于您的回答)还是我应该将其构建为带有 sprintfdbh->quote() 的查询字符串?

(作为一些额外的信息:我现在实际上正在使用 AnyEvent::DBI,它只公开占位符和绑定值,而不是 quote() 方法,所以如果不创建另一个方法,这对我来说不容易完成直接 DBI $dbh 并使用另一个数据库服务器连接只是为了使用 quote() 方法,或者自己不更改 AnyEvent::DBI 模块。)

通常我只会在必要时执行语句,但在这种繁重的工作负载情况下,我会尝试将插入批处理以提高数据库效率。

另外,如果有人可以回答是否有可能(以及如何)使用占位符和绑定值插入一个 sql DEFAULT 值,那就太棒了。通常,如果我需要这样做,我会直接将 DEFAULTs 附加到字符串中,并仅将 sprintf 和 $dbh->quote() 用于非 DEFAULT 值。

更新:

在快速聊天中解决了误解。用户ikegami 建议不要自己构建查询字符串而不使用占位符,而是将 VALUES 和占位符混合在一起,例如:

$queryString .= '(DEFAULT,?,?),(DEFAULT,DEFAULT,DEFAULT)';

我第一次在 SO 上提出这个问题背后的一些原因是因为我有点反对这种混合,因为我认为它会降低代码的可读性,尽管在确信 sql 'DEFAULT' 不能在占位符绑定值,这是我开始实施的方法。

在可能的情况下使用占位符似乎是更被接受的构建查询的方法,如果您想要 SQL DEFAULT,您只需将其包含在与占位符相同的查询构建中。这不适用于NULL 值,因为可以使用占位符和绑定值undef 插入这些值。

更新 2:

我询问性能的原因、使用 quote() 构建自己的查询与使用占位符构建的“接受度”以及为什么我采用涉及使用 SQL INSERT INTO tblname (cols) 的所有列的解决方案是因为我每天大约有 2-4 百万行进入一个糟糕的数据库服务器,我的代码在同样糟糕的服务器上运行。鉴于我需要DEFAULT sql 值的要求,以及这些可怕的性能限制,我现在选择了一个解决方案。

对于偶然发现此问题的未来开发人员 - 看看@emazep 使用 SQL::Abstract 的解决方案,或者如果出于某种原因您需要构建自己的解决方案,您可以考虑使用 @Schwern 的子程序解决方案或可能合并一些@ikegami 对此的回答,因为这些都是关于 DBI 使用和构建动态查询的“当前状况”的很好回答。

【问题讨论】:

    标签: mysql sql perl dbi


    【解决方案1】:

    您对代码的可读性以及能否传入 DEFAULT 表示担忧。我会把@ikegami 的回答更进一步......

    sub insert {
        my($dbh, $table, $fields, $values) = @_;
    
        my $q_table      = $dbh->quote($table);
        my @q_fields     = map { $dbh->quote($_) } @$fields;
        my @placeholders = map { "?" } @q_fields;
    
        my $sql = qq{
            INSERT INTO $q_table
                   ( @{[ join(', ', @q_fields)    ]} )
            VALUES ( @{[ join(', ', @placeholders ]} )
        };
    
        return $dbh->do($sql, undef, @$values);
    }
    

    现在您有了一个通用的多值插入例程。

    # INSERT INTO foo ('bar', 'baz') VALUES ( 23, 42 )
    insert( $dbh, "foo", ['bar', 'baz'], [23, 43] );
    

    要表示默认值,请不要传入该列。

    # INSERT INTO foo ('bar') VALUES ( 23 )
    # 'baz' will use its default
    insert( $dbh, "foo", ['bar'], [23] );
    

    您可以对此进行优化,以使您的子例程通过一个子例程调用和一个准备好的语句执行多次插入,从而节省客户端的 CPU(如果它支持准备句柄,则可能在数据库端节省一些 CPU)。

    sub insert {
        my($dbh, $table, $fields, @rows) = @_;
    
        my $q_table      = $dbh->quote($table);
        my @q_fields     = map { $dbh->quote($_) } @$fields;
        my @placeholders = map { "?" } @q_fields;
    
        my $sql = qq{
            INSERT INTO $q_table
                   ( @{[ join(', ', @q_fields)    ]} )
            VALUES ( @{[ join(', ', @placeholders ]} )
        };
    
        my $sth = $dbh->prepare_cached($sql);
        for my $values (@rows) {
            $sth->execute(@$values);
        }
    }
    
    # INSERT INTO foo ('bar', 'baz') VALUES ( 23, 42 )
    # INSERT INTO foo ('bar', 'baz') VALUES ( 99, 12 )
    insert( $dbh, "foo", ['bar', 'baz'], [23, 43], [99, 12] );
    

    最后,您可以编写一个在单个语句中传入多个值的批量插入。这可能是进行大量插入的最有效方法。这就是拥有一组固定列并传入DEFAULT 标记的地方。我使用了作为标量引用传递的值被视为原始 SQL 值的习语。现在您可以灵活地传递您喜欢的任何内容。

    sub insert {
        my($dbh, $table, $fields, @rows) = @_;
    
        my $q_table      = $dbh->quote($table);
        my @q_fields     = map { $dbh->quote($_) } @$fields;
    
        my $sql = qq{
            INSERT INTO $q_table
                   ( @{[ join(', ', @q_fields)    ]} )
            VALUES
        };
    
        # This would be more elegant building an array and then joining it together
        # on ",\n", but that would double the memory usage and there might be
        # a lot of values.
        for my $values (@rows) {
            $sql .= "( ";
            # Scalar refs are treated as bare SQL.
            $sql .= join ", ", map { ref $value ? $$_ : $dbh->quote($_) } @$values;
            $sql .= "),\n";
        }
        $sql =~ s{,\n$}{};
    
        return $dbh->do($sql);
    }
    
    # INSERT INTO foo ('bar', 'baz') VALUES ( 23, NOW ), ( DEFAULT, 12 )
    insert( $dbh, "foo", ['bar', 'baz'], [23, \"NOW"], [\"DEFAULT", 12] );
    

    缺点是这会在内存中构建一个字符串,可能非常大。要解决这个问题,您必须让 database specific bulk insert from file syntax 参与进来。

    与其自己编写所有这些 SQL 生成内容,不如使用 @emazep 的答案并使用 SQL::Abstract 和 SQL::Abstract::Plugin::InsertMulti

    只要确保你profile

    【讨论】:

    • 我实际上实现了一个类似的解决方案,唯一的区别是因为我有许多 VALUES 集,并且有些集对所有列都有实际值,而另一些则需要对某些列使用 DEFAULT,我准备了一个单一的东西来处理所有的情况。这让我可以动态地创建像INSERT INTO `tbl` (all,columns,in,table,here) VALUES (DEFAULT,?,DEFAULT,?,?),(?,?,?,?,?) 这样的查询等等。我真的很喜欢你对单个 VALUES 集的解决方案,它可以很容易地适应我以更优雅的方式做同样的事情。谢谢你的回答!
    • 阅读您的 cmets,您似乎倾向于指定每一列并传入 DEFAULT,而不是完全忽略该列。我可以问为什么吗?两者都需要生成 SQL。
    • 当然——如果我指定所有列,我只能有一个带有多组值的插入查询,这可以减轻数据库上的持续负载,并且还允许我只准备一个 sql 语句。我今天也将再次研究 SQL::Abstract。运行这段代码的机器很糟糕,数据库服务器很糟糕,所以我试图只导致突发负载,这样用户查询仍然可以毫无阻碍地运行;因此我希望尽可能少地执行插入查询。顺便说一句,很荣幸得到您的答复 - 我使用您的模块已经有一段时间了,它们很棒!
    • @AndrewPK 我认为这里的负载将是插入行的实际行为,而不是准备语句。如果您运行相同的插入 N 次或稍微不同的插入 N 次,我不确定它对数据库有什么不同,但 N 插入仍然很昂贵。如果你想稍微减轻一些事情,你可以使用我添加到示例代码中的prepare_cached,如果可能的话,你可以进行批量插入。 This article 涵盖了一些值得尝试的好东西,包括 profiling
    【解决方案2】:

    除非有特定的原因需要重新发明轮子(可能有一些),SQL::Abstract(等等)已经为我们所有人解决了动态 SQL 生成的问题:

    my %data = (
        name    => 'Jimbo Bobson',
        phone   => '123-456-7890',
        address => '42 Sister Lane',
        city    => 'St. Louis',
        state   => 'Louisiana'
    );
    
    use SQL::Abstract;
    my ($stmt, @bind)
        = SQL::Abstract->new->insert('people', \%data);
    
    print $stmt, "\n";
    print join ', ', @bind;
    

    哪个打印:

    INSERT INTO people ( address, city, name, phone, state)
    VALUES ( ?, ?, ?, ?, ? )
    42 Sister Lane, St. Louis, Jimbo Bobson, 123-456-7890, Louisiana
    

    SQL::Abstract 然后提供a nice trick 来迭代许多行以插入,而无需每次都重新生成 SQL,但对于批量插入,还有SQL::Abstract::Plugin::InsertMulti

    use SQL::Abstract;    
    use SQL::Abstract::Plugin::InsertMulti;
    
    my ($stmt, @bind)
        = SQL::Abstract->new->insert_multi( 'people', [
            { name => 'foo', age => 23 },
            { name => 'bar', age => 40 },
        ]);
    
    # INSERT INTO people ( age, name ) VALUES ( ?, ? ), ( ?, ? )
    # 23, foo, 40, bar
    

    【讨论】:

    • 从我过去使用 SQL::Abstract 的经验来看,我不记得它是否支持保留列或使用 sql DEFAULT 值,所以我只是选择了我能做的最快/最熟悉的事情。感谢您的回答,我今天一定会再次复习 SQL::Abstract。
    【解决方案3】:

    如果您将占位符用于“静态”查询,您也应该将它们用于“动态”查询。查询就是查询。

    my $stmt = 'UPDATE Widget SET foo=?'
    my @params = $foo;
    
    if ($set_far) {
       $stmt .= ', far=?';
       push @params, $far;
    }
    
    {
       my @where;
    
       if ($check_boo) {
          push @where, 'boo=?';
          push @params, $boo;
       }
    
       if ($check_bar) {
          push @where, 'bar=?';
          push @params, $bar;
       }
    
       $stmt .= ' WHERE ' . join ' AND ', map "($_)", @where
          if @where;
    }
    
    $dbh->do($stmt, undef, @params);
    

    我使用了 UPDATE,因为它允许我展示更多内容,但一切也适用于 INSERT。

    my @fields = ('foo');
    my @params = ($foo);
    
    if ($set_far) {
       push @fields, 'bar';
       push @params, $far;
    }
    
    $stmt = 'INSERT INTO Widget ('
          . join(',', @fields)
          . ') VALUES ('
          . join(',', ('?')x@fields)
          . ')';
    
    $dbh->do($stmt, undef, @params);
    

    【讨论】:

    • 非常好,因为我已经实现了这一点,我只是想验证这是否可以接受,并且在 perl 社区中并不被认为是荒谬的。有没有办法在存在占位符的地方插入 sql DEFAULT 值?
    • 再次感谢您的快速聊天和您的回答和建议。
    【解决方案4】:

    我有时会使用如下结构:

    #!/usr/bin/env perl
    
    use strict; use warnings;
    
    # ...
    
    my @columns = ('a' .. 'z');
    
    my $sql = sprintf(q{INSERT INTO sometable (%s) VALUES (%s)},
        join(',', map $dbh->quote($_), @columns),
        join(',', ('?') x @columns),
    );
    

    至于处理DEFAULT,不会留下那一列以确保数据库将其设置为默认值吗?

    【讨论】:

    • 您不能使用多插入将列排除在插入(列)语句之外,否则其他语句将失败。如果您只是省略了该值 - 您将收到有关 SQL 语法的错误。如果将其设置为 undef,它将在 DB 中为“NULL”,如果将其设置为空字符串“”,它将是空字符串或失败,具体取决于您的 DB 和列数据类型。另外 - 你有什么理由在性能方面使用它而不是占位符 + 绑定值?
    猜你喜欢
    • 2015-06-22
    • 2017-05-20
    • 2012-12-22
    • 1970-01-01
    • 2019-09-15
    • 1970-01-01
    • 1970-01-01
    • 2015-08-07
    • 2013-02-10
    相关资源
    最近更新 更多