为什么在向数据库中插入数据时 `state` 不比 `my` 快?

Why `state` is not faster than `my` while inserting data to database?

在转换数据库的过程中,我尝试使用 best/fastest 插入。 AFAIK,惯用的 mass-insert 应该准备语句处理程序,然后迭代数据以插入它。类似的东西:

my $sql = q|INSERT INTO test.table ( value ) VALUES ( ? ) |;  
my $sth = $dbh->prepare( $sql );  
for my $val ( 1 .. 1000000 ) {
  $sth->execute( $val );
}

我认为在 state-声明符的帮助下,我可以将此例程重构为函数,如下所示:

sub sql_state {  
  my ( $val ) = @_;
  state $sql = q|INSERT INTO test.table ( value ) VALUES ( ? ) |;  
  state $sth = $dbh->prepare( $sql );  
  $sth->execute( $val ) 
    or die "State";  
}  

所以现在 $sql 在所有插入期间初始化一次并且 $sth 准备一次,这是增强的基础。

在迁移我的数据库期间,我觉得这种改进并没有像我希望的那样给我带来胜利。然后我找到了一篇文章 Enemy of the State,这与我问自己的问题完全相同:为什么 state 没有比 my 有任何改进?

在 rjbs 的评论中猜测,在初始化语句处理程序时使用 statemy 时差异会很大。我做了一些基准测试并得出了与文章作者完全相同的结论:即使在某些情况下我 state 快一点(0.5%),在大多数情况下 my 速度相同或甚至更快(高达 9%)。

首先,我尝试使用 innodb 表,因为我在自己的任务中需要:

Benchmark: timing 100 iterations of callFor, callMy, callState...
   callFor: 922 wallclock secs ( 7.31 usr +  3.78 sys = 11.09 CPU) @  9.02/s (n=100)
    callMy: 927 wallclock secs ( 6.09 usr +  4.46 sys = 10.55 CPU) @  9.48/s (n=100)
 callState: 922 wallclock secs ( 6.72 usr +  4.62 sys = 11.34 CPU) @  8.82/s (n=100)

那些对于更广泛的迭代来说太慢了,所以我也用 myisam 表做了一些(1000x1000 = 百万插入):

Benchmark: timing 1000 iterations of callfor, callmy, callstate...
   callfor: 96 wallclock secs (15.19 usr + 15.50 sys = 30.69 CPU) @ 32.58/s (n=1000)
    callmy: 95 wallclock secs (15.18 usr + 14.90 sys = 30.08 CPU) @ 33.24/s (n=1000)
 callstate: 104 wallclock secs (18.86 usr + 16.15 sys = 35.01 CPU) @ 28.56/s (n=1000)

另一个运行:

Benchmark: timing 1000 iterations of callfor, callmy, callstate...
   callfor: 94 wallclock secs (14.90 usr + 14.47 sys = 29.37 CPU) @ 34.05/s (n=1000)
    callmy: 92 wallclock secs (14.77 usr + 14.09 sys = 28.86 CPU) @ 34.65/s (n=1000)
 callstate: 99 wallclock secs (17.66 usr + 15.30 sys = 32.96 CPU) @ 30.34/s (n=1000)

下面是我的实际测试代码:

use strict; use warnings; use 5.010;
use ...; # something to get $dbh ...
use Benchmark qw{:all} ;  


sub prepareTable {
  my $dropTable   = q|DROP TABLE IF EXISTS test.table|;
  $dbh->do( $dropTable )
    || die "droptable";

  my $createTable = q|
    CREATE TABLE test.table (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `value` varchar(60),
      PRIMARY KEY (`id`)
    ) ENGINE=MYISAM
  |;
  $dbh->do( $createTable )
    || die "createtable";  
}


sub callFor {
  prepareTable();

  my $sql = q|INSERT INTO test.table ( value ) VALUES ( ? ) |;  
  my $sth = $dbh->prepare( $sql );  

  for my $val ( 1 .. 1000 ) {
    sql_for( $sth, $val );
  }
}

sub callMy {
  prepareTable();

  for my $val ( 1 .. 1000 ) {
    sql_my( $val );
  }
}

sub callState {
  prepareTable();

  for my $val ( 1 .. 1000 ) {
    sql_state( $val );
  }
}

sub sql_for {  
  my ( $sth, $val ) = @_;
  $sth->execute( $val ) 
    or die "For";
}  

sub sql_my {  
  my ( $val ) = @_;
  my $sql = q|INSERT INTO test.table ( value ) VALUES ( ? ) |;  
  my $sth = $dbh->prepare( $sql );  
  $sth->execute( $val ) 
    or die "My";
}  

sub sql_state {  
  my ( $val ) = @_;
  state $sql = q|INSERT INTO test.table ( value ) VALUES ( ? ) |;  
  state $sth = $dbh->prepare( $sql );  
  $sth->execute( $val ) 
    or die "State";  
}  


timethese(  
  1000 , {  
    'callFor'   => sub { callFor( ) ; } ,  
    'callMy'    => sub { callFor( ) ; } ,  
    'callState' => sub { callState( ) ; } ,  
  }
);  

那么,为什么 state 在这里没有战胜 my?它应该很容易。或者?

你在有牛吃草的田地里追逐蚱蜢以获取蛋白质。

在对数据库进行批量插入或更新时,autocommit 不是 你想要速度的朋友。完成后做一个 commit,或者每 1000 条记录中抛出一个。

最后,如果您还在等待磁盘 I/O 的更改和索引更新完成,则每次迭代节省几个 CPU 周期将毫无意义。

感谢@Borodin,我可能会宣布:state 仍然胜过 my,这是应该的和预期的。

我的代码中有错字 timethese() 应该有正确的行 'callMy' => sub { callMy( ) ; } 并且没有问题要问。

修复它并删除数据库操作后,我得到了很多丰富多彩的结果:

Benchmark: timing 1000 iterations of callFor, callMy, callState...
   callFor: 85 wallclock secs (15.60 usr + 13.38 sys = 28.98 CPU) @ 34.51/s (n=1000)
    callMy: 162 wallclock secs (64.36 usr + 21.17 sys = 85.53 CPU) @ 11.69/s (n=1000)
 callState: 86 wallclock secs (15.83 usr + 13.55 sys = 29.38 CPU) @ 34.04/s (n=1000)

然后我设置$dbh->{AutoCommit} = 0;并在迭代后使用$dbh->commit();

Benchmark: timing 1000 iterations of callFor, callMy, callState...
   callFor: 87 wallclock secs (16.57 usr + 14.50 sys = 31.07 CPU) @ 32.19/s (n=1000)
    callMy: 166 wallclock secs (66.39 usr + 21.92 sys = 88.31 CPU) @ 11.32/s (n=1000)
 callState: 87 wallclock secs (16.50 usr + 14.21 sys = 30.71 CPU) @ 32.56/s (n=1000)

在我看来,autocommit在这里影响不大

在这里留下我的答案以供将来参考。

如果你想测试 state 与 my,你真正想要的基准测试是准备的成本。我投入 prepare_cached 因为它做同样的事情。

use strict; use warnings; use 5.010;
use DBI;
use Benchmark qw{:all} ;  

my $dbh = DBI->connect('DBI:mysql:database=test', '', '',
                       {RaiseError => 1, AutoCommit => 0}
                      );

sub prepareTable {
    my $dropTable   = q|DROP TABLE IF EXISTS test.table|;
    $dbh->do( $dropTable );

    my $createTable = q{
    CREATE TABLE test.table (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `value` varchar(60),
      PRIMARY KEY (`id`)
    )
    };
    $dbh->do( $createTable );
}

prepareTable;
my $sql = q|INSERT INTO test.table ( value ) VALUES ( ? ) |;
cmpthese( -3, {
    'my'                => sub { my $sth = $dbh->prepare($sql); return; },  
    'state'             => sub { state $sth = $dbh->prepare($sql); return; },
    'prepare_cached'    => sub { my $sth = $dbh->prepare_cached($sql); return; },
});
__END__
                     Rate             my prepare_cached          state
my                67966/s             --           -73%           -99%
prepare_cached   253414/s           273%             --           -98%
state          11267589/s         16478%          4346%             --

这只是告诉您非 运行 代码比 运行 代码快。如果您想了解这对实际应用程序的影响有多大,那么您走的路是正确的,但您已经搞砸了您的基准。 Here is my improved benchmark. This makes it more like how code would be run in production (AutoCommit off, RaiseError on), eliminates the table scaffolding, and uses the generally superior InnoDB。重要的是,它删除了许多额外的代码和子程序,这些代码和子程序只会影响基准测试。

结果不出所料,执行 1000 次 INSERT 的成本超过了准备 INSERT 的成本。 INSERT 占主导地位,其运行时非常不可靠,很难从中获得一致的基准测试结果。

如果每次准备执行的次数较少怎么办?准备应该开始产生更大的影响,这正是我们所看到的。

$EXECUTES_PER_PREPARE = 1;
                  Rate             my prepare_cached          state
my             24722/s             --           -36%           -56%
prepare_cached 38610/s            56%             --           -31%
state          56180/s           127%            46%             --

$EXECUTES_PER_PREPARE = 2;
                  Rate             my prepare_cached          state
my             15949/s             --           -22%           -41%
prepare_cached 20325/s            27%             --           -25%
state          27027/s            69%            33%             --

$EXECUTES_PER_PREPARE = 10;
                 Rate             my prepare_cached          state
my             4405/s             --           -17%           -22%
prepare_cached 5305/s            20%             --            -6%
state          5618/s            28%             6%             --

$EXECUTES_PER_PREPARE = 100;
                Rate             my prepare_cached          state
my             546/s             --            -0%            -1%
prepare_cached 546/s             0%             --            -1%
state          552/s             1%             1%             --