美女Linux on iPhoneを作る(1)

電子書籍アプリとは別に、ごく簡単な「美女Linux on iPhone」を作ってみます。
まあ、電子書籍アプリのほうは、UIViewContoller 絡みがややこしそうなので、そのあたりベタベタに書こうと思って(所詮、スクリプトで出力させるわけだし)。

目的としては
・独立した iPhone アプリができればよい。
・Apple Store に無料版として up できればよい。
ってところですね。

「無料版」を up させるところが練習なわけで、最終的には「有料版」(美女Linuxじゃないけど)ができることが最終目標。
「独立した」というのは、ネットワークに接続せずに画面切り替え、ってところです。最終的にはしかるべき web サイトから画像をダウンロードするわけですが、3G 契約だったりすると通信料の課金の関係から、ネットにつなぐ/つながないはシビアに考えたほうがよいですよね。日本の携帯電話の場合、そのあたり通信回線に接続するのにシビアなのに、iPhone アプリがルーズなのが気になるところです(まぁ、広告を表示するのに、いちいち「回線に接続するか?」と問い合わせるのも変な話なんですしょうが、一応、apple の指針では回線接続の場合には問い合わせが推奨のようです)。

iPhone で動いている画像があればよいのですが、iPhone では iPhone を写せないというジレンマが(笑)、あるので、開発中の画面なぞ(実機でも動いています。画像は非常にきれいです)。

<001>

本来は、練習として interface builder を使いたいところなのですが、まだ使い方がわからないので、ベタに記述します。

// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    _page = 1;
    
    // 背景画像を UIImage, UIImageView で作成
    NSString *backName = [NSString stringWithFormat:@"iphone_back%02d.png", _page  ];
    NSLog( @"%@", backName );
    UIImage *back = [[UIImage imageNamed:backName] autorelease];
    UIImageView *backview = [[[UIImageView alloc] initWithImage:back] autorelease];
    backview.frame = self.view.bounds;
    [self.view addSubview:backview];
    // タイマーイベントで使うので保存しておく
    _backview = backview;
    
    // 「次へ」ボタンの作成(デバッグ用)
    // UIButton *btnNext = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    UIButton *btnNext = [UIButton buttonWithType:UIButtonTypeCustom];
    [btnNext setTitle:@"NEXT" forState:UIControlStateNormal];
    [btnNext setFrame:CGRectMake(0,0, 50, 50  )];
    [btnNext addTarget:self action:@selector(btnNextDidPush) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btnNext];
    
    // 日付を表示する
    UILabel *lblClock = [[UILabel alloc] init];
    lblClock.frame = CGRectMake(0, 50, 300, 50);
    NSDate *now = [NSDate date];
    NSDateFormatter *fmtr = [[NSDateFormatter alloc] init] ;
    [fmtr setDateFormat:@"yyyy/MM/dd HH:mm:ss"];	// 日付のフォーマット
    lblClock.textColor = [UIColor whiteColor];
    lblClock.backgroundColor = [UIColor clearColor]; // 背景を透明にする
    lblClock.font = [UIFont fontWithName:@"AppleGothic" size:20];
    lblClock.text = [fmtr stringFromDate:now];
    [self.view addSubview:lblClock];
    [fmtr release];
    _lblClock = lblClock;
    
    // 日時切り替え用のタイマー(1秒間隔)
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(onTimerClock:) userInfo:nil repeats:YES];
    // 画像切り替え用のタイマー(15秒間隔)
    _timer = [NSTimer scheduledTimerWithTimeInterval:15.0f target:self selector:@selector(onTimer:) userInfo:nil repeats:YES];
    
}
// 次へボタン
- (void)btnNextDidPush {
    
    _page++;
    if ( _page > 3 ) _page = 1;
    
    // 新しい UIImage を作成
    NSString *backName = [NSString stringWithFormat:@"iphone_back%02d.png", _page  ];
    UIImage *back = [[UIImage imageNamed:backName] autorelease];
    // UIImageView に設定する
    [_backview setImage:back];
    
}
// タイマーで切り替え
- (void)onTimer:(NSTimer*)timer {
    NSLog(@"on timer");
    _page++;
    if ( _page > 3 ) _page = 1;
    // btnNextDidPush と同じことをやっているが、まぁ、いいか(コピー&ペーストで)
    NSString *backName = [NSString stringWithFormat:@"iphone_back%02d.png", _page  ];
    UIImage *back = [[UIImage imageNamed:backName] autorelease];
    [_backview setImage:back];
}

// 日時の表示
- (void)onTimerClock:(NSTimer*)timer {

	// このあたりも、viewDidLoad とダブっているけど、まぁ良しとする。
    NSDate *now = [NSDate date];
    NSDateFormatter *fmtr = [[NSDateFormatter alloc] init] ;
    [fmtr setDateFormat:@"yyyy/MM/dd HH:mm:ss"];
    _lblClock.text = [fmtr stringFromDate:now];
    [fmtr release];
}

美女Linuxの画像は iPhone4 用に 960×640 で用意しておきます。縦横比が写真と異なるので加工が必須ですね(拡大縮小をしてもいいんでしょうけど、綺麗さが損なわれるのでやめます)。

シミュレーターを動かしながら、try & error を繰り返すわけですが、このぐらいのコードであれば 2 時間で書けるようになりました(2時間も掛かるという話もあるッ!!!)。NSDateFormatter の使い方がわからんとか、NSTimer は autorelease しちゃいけないとか、落ちるところには落ちまくったので、もう一度作るときはまぁ大丈夫です。

大枠はできたので、他に加工するところは、

・日時表示をもう少し綺麗に
・美女Linux のロゴを表示(透過PNGを使う)
・画像の切り替えをアニメーションで(お手軽にできれば)
・美女Linux のロゴをクリックしたら、通信回線を調べて、ブラウザで表示。

ぐらいですかね。ある程度できたら、さっさと apple store に公開してみよう。

カテゴリー: Objective-C, iPad | 美女Linux on iPhoneを作る(1) はコメントを受け付けていません

PMBOKの開発工程が手薄なので、100プロセス追加してみるテスト

PMBOK ってのは、プロジェクト マネージメントなので、管理者側(management)の視点から書かれているのがご存知の通り。な訳ですが、実際、ソフトウェア開発をする上では、開発(development or impliment)の作業が必須なわけで、SWEBOK が良いというかというとそうではなくて、managment から見た pmbok 配下の開発工程プロセスの手薄さは、プロジェクト自体の破綻を招くのではないかッ!!! とかいうことをあえて【妄想】してみて、100 項目ほど挙げてみます。

ま、7月頭だし、たまにはコーディング以外の頭も廻すということで。

一応、アジャイル開発も意識していますが、PMBOK ベースなのでウォーターフォール開発中心のプロセス/tipsになります。

・いわゆる「software factory」状態にすること(最近の microsoft ではやらないが)
 文章中、人=機械となっているのは、このためです。
・ムリ・ムラ・ムダを気に掛けること。
・完了しないリスクに対して、最大限対処すること。
 → 開発工程については設計に沿っての【完遂】が優先事項であるため。

■進捗関連
001 開発工程の初期に「巡航速度」を把握する(平均的なコーディング量、生産性を把握する)
002 巡航速度に従ったときに、開発工程の納期が間に合うかを定期的にチェックする。
003 間に合わない場合は、増員を準備する(この時点で「生産性」を上げることは難しい)
004 間に合わない場合は、生産量を減らす(設計工程の手戻りとなる)
005 間に合わない場合は、ムダな作業を減らす(自動化できるところを自動化する。ただし、設計工程の手戻りとなる)
006 巡航速度よりも早い場合は、燃え尽き症候群に注意する。
007 巡航速度よりも早い場合は、不用意なコピー&ペーストに注意する(全体の生産量が上がってしまう可能性がある)
008 巡航速度よりも早い場合は、不具合の発生率が高くなる(不良品の混入率が高くなる)
009 巡航速度よりも早い場合は、勤務時間に注意する(単位時間あたりの巡航速度を守る)
010 巡航速度よりも早い場合は、嘘の報告に注意する(意図的に生産性を挙げている可能性がある)

■不具合関連
011 開発工程の初期で不具合発生数を最初に決める(不具合を直す時間をとる)
012 不具合発生率の高い人員を見つける(人=生産機械と考えると、不具合の多い機械ということになる)
013 不具合発生率が高い場合は、巡航速度を下げる(不注意なバグが減る)
014 不具合発生率が高い場合は、コード量を減らす(不具合の発生しやすい箇所をひとつにまとめる)
015 軽度な不具合は、発生率としてカウントしない(xUnit 内での不具合をカウントすると時間コストがかかる)
016 軽度な不具合は、コーディングの生産量を抑えて調節する(xUnit を用いて、コード&テストを一体化する)
017 重度な不具合は、周知する場所を作る(軽度/重度を区別する)
018 重度な不具合は、修正サイクルを遅く廻す(すぐに修正しようとすると、コーディング自体が止まってしまう)
019 修正サイクルを遅く廻すには、不具合票を取り回す(あえて、重たい紙の不具合票や、手順が多い不具合票にする)
020 重度な不具合は、コーディング&テストのサイクルに含めない(コーディングの速度を不意に上げ下げしてしまう)

■規約関連
021 コーディング規約は、周知できるものにとどめる(開発者のスキルにあわせる)
022 コーディングスタイルを合わせることも規約のうちに含める(初心者が多い場合は、ベテランが修正する可能性が高いので)
023 平易に書けるコーディング規約にする(IDE などの規約にあわせる)
024 規約に合わせるリファクタリングを許す(巡航速度の範囲で)
025 規約には例外があることを周知する(規約は「ルール」や「モラル」でしかない)
026 規約のための問い合わせを減らす(コミュニケーションコストを減らす)
027 テンプレートを随時用意する
028 一度、テンプレートに従ってコーディングをしてみる(コーディング時の規約による負担がわかる)
029 多すぎる規約は、減らす(規約に頭を取られるのは問題)
030 コーディング状態に合わせて規約を追加する(コーディングの現状にあわせる。最大公約数を使う)

■品質システム関連
031 開発工程の途中で、開発物をチェックする時間をとる。
032 チェック自体は形式的でもよい(「チェックする」こと自体が重要)
033 重度の不具合は常にカウントし、監視する(多くならないようにする)
034 重度の不具合を発生箇所(人)を監視する(多くならないようにする)
035 重度の不具合について、「理由」を明確にする(理由があればOKとする)
036 監視作業はできるだけ管理者自身が行う(虚偽の報告を減らすため)
037 監視作業はできるだけ自動化する(人手を減らす、あるいは人的理由で進捗の進退が変わらないように)
038 監視作業は、監視されているこを意識させないようにする(計測されることに注力されてしまう)
039 リスクが減ったら、監視自体をやめてもよい(単純なモニタリング作業に戻す)
040 「監視されている」という意識だけを植え付けて、実際監視作業はしなくてもよい(モラルの問題、性善説風な性悪説手法)

■稼動率関連
041 定時の作業を監視する(巡航速度を守るためムリな徹夜、残業をしない)
042 定時の作業量を監視する(一定の生産量を計測する)
043 休日に関しては、あらかじめ確保しておく(週単位の進捗量ではなく、日単位を監視する)
044 休出に関しては、時間をカウントする(巡航速度を守るため)
045 残業に関しては、時間をカウントする(巡航速度を守るため)
046 生産性は、人単位で計測する(人=機械の生産性のばらつきを考慮する)
047 難易度が異なる場合は、大枠で生産量をカウントする(早くなったり遅くなったりをカウントするのを正確にカウントするとコストがかかる)
048 別作業が割り込みであった場合は、時間を減算する(巡航速度を守るため)
049 月20日、8時間/日の 160h/月でカウントすると大枠で正しい。
050 開発工程を完遂することが目的なので、そのほかの稼動率は考慮しない(細かいところにコストを掛けない)

■コミュニケーション関連
051 コミュニケーション自体には「コスト」があることを意識する(会議にはコストがかかっている)
052 会議コストは、開発工程の初期に計上しておき、生産性から外す(巡航速度を計測するため)
053 会議コストは、主に資料集めにある(管理者は例外)
054 会議コストは、物事が決まらない場合は「最大値」となる(いわゆるムダな会議となる)
055 進捗報告は、監視側(管理者側)が抽出した資料をもとにする(虚偽を防ぐため)
056 進捗報告は、常に開発工程の完了を意識する(目的を明確にする)
057 軽度の遅れの見込みは、無視してよい(会議コストが上がるため)
058 進捗のムラに関しては、長期的な監視の対象とする(巡航速度が下がっていなければよい)
059 開発自体にムリがないことを確認する(ムダな作業が、貴重な時間を押しつぶしていないか)
060 開発自体のムダを省くように管理者は注力する(ムダな作業で、貴重な時間を使っていないか)

■人員関連
061 マイナス生産者を外す(他開発者への邪魔となるため)
062 マイナス生産者を隔離する(他開発者への影響をさげるため)
063 マイナス生産者に規定の作業を与える(作業見積がしやすい、超過がわかりやすい)
064 マイナス生産者に定期作業を与えるとプラスに転じる(定期作業は不具合のリスクが低いため)
065 初心者には定型作業を与える(巡航速度が把握しやすい)
066 初心者にはシンプルな作業を割り振る(不具合が発生しにくい)
067 ベテランには複雑な作業を割り振る(開発工程全体の生産性をあげるため)
068 ベテランには不具合発生率の高い作業を割り振る(重度不具合の対処など)
068 ベテランは、初心者のフォローに廻るだけでもよい(不具合の対処、ライブラリ作成など)
069 ベテランは、時には生産性を考慮しない(巡航速度だけを守るため)
070 開発スピードは標準的な開発者を集めた場合を考慮する(巡航速度を守るため)

■制約条件関連
071 外してよい制約があれば、即外してしまう(ムダな規約、細かすぎる進捗報告など)
072 制約は時間依存である(時間経過により状況が変わると、制約が変わる)
073 制約は前提依存である(前提条件が変わると、制約が変わる)
074 制約を見つけたあとは、その制約を利用すると(見かけ上)生産性があがる(ムダが減るなど)
075 取り除けない制約には、ベテランを配置する(生産性が一番高い人を配置する)
076 ベテランを、長期に縛る作業に割り当てない(変化する制約に対処させるため)
077 ベテランを、休ませない(常に働かせる状態がよい)
078 他の作業が止まりそうな不具合は即対処する(他作業がとまってしまうので)
079 他作業が止まらない作業にはベテランを配置しない(キーとなる合流プロセスで利用する)
080 インプットとアウトプットを固定して、内部を変化可能にする(契約プログラミングなど)

■モチベーション関連
081 人=機械ではないので、プロジェクトが終わった後の人を意識する。
082 不定期なガス抜きを考慮する(定期的なガス抜きは時には負担になる)
083 一定量のコーディングをしたら、その日は帰る(ムリな作業はしない)
084 一定量の不具合をつぶしたら、その日は帰る(ムリな作業はしない)
085 効率化を求める場合は、3 倍以上のスピードになることを確認する(2倍以下では全体に大きく関わらない)
086 手作業ではなく頭を使う作業をする(疲れるが、そのほうが作業効率がよい)
087 手作業が必要な場合は、時間を決めて作業をする(開発者自身の作業効率を知る、巡航速度を守る)
088 使い捨ての道具「治具」を作る時間を確保する(一時的に作業量が減る)
089 「治具」は過度に高度化させない(所詮、使い捨てなので過度に自動化させない)
090 プロジェクト内に過度に「遊んでいる」人を作らない(ばらつきはあってもよいが、他の人のモチベーションに関わる)

■コスト関連
091 人的コストよりも、既存ソフトウェアコストのほうが安い(購入できれば、それを使う)
092 セキュリティによる制約コストは高い(ウィルススキャン、フリーソフトの導入不可など)
093 調査コストは非常に高い(インターネットで探せればそれに越したことはない)
094 書籍コストは非常に安い(人的コストに比べると安い)
095 複雑化されたコードよりも、単純化されたコードのほうが保守コストが低い。
096 人依存でないコードのほうが、保守コストは低い。
097 製品寿命を考慮したコーディングをするほうが、開発コストが低い。
098 管理されたリスクに対しての対処コストは低い(管理されてないリスクに対しては非常に高い)
099 コード量が少ないほうが、保守コストは低い
100 保守コストが高くなる場合のみ、開発期間を延ばしてよい。

と、ざっと書き下しましたが(2時間程度)、プロセスでなくて、チェックシートみたいになってしまいましたが、ひとまずこれで up することにします。機会があれば、もうちょっと手直しをするということで。
リスク発生 → 対処方法 の流れがないといけないし、PDCA の Check → Action → Plan の繋がりがないと駄目ですからね。

カテゴリー: プロジェクト管理, Plan Language | PMBOKの開発工程が手薄なので、100プロセス追加してみるテスト はコメントを受け付けていません

UIScrollView 上では UIViewController 上のボタンイベントを拾えない

電子書籍のおおまかな構造設計をしている途中なのですが、どうも腑に落ちない現象に出会ったのでメモ的に。
結論から言うと、タイトル通り、UIScrollView 上にある UIViewController のイベントは、手軽には拾えません…という話です。

■設計要件

まずは、次のように設計を考えました。

・ページ捲りの部分は、UIScrollView を使う。
 → スライドさせたいので、ページを横に並べてスクロールさせるとよい。
 → ページ捲りのアニメーションよりも、手軽に作れる、と思う。
 
・ページ単位は、View で作りたい。
 → 手軽にページを構成したいので、ページ単位で作りたい。
 → これは UIViewController にボタンなどを乗せるのがよいかなと。
 
・ページ間で共通のボタンがある。
 → ナビゲートボタンのように、ページ間で共通の場所にボタンがある。
 → 自動生成をするか、スクロールさせても移動させない、のどちらか。
 
最終的には、スクリプトを書いてページを構成するための objective-c のソースコードを出力させたいわけです。

■クラス構造

これを踏まえてクラス構造を考えて、素直に以下の構造にしました。

UIApplication
+ MainViewController : 最初の View
 + UIView
  + UIScrollView
   + UIViewController
    + UIView
     + Button : ボタン
     + Movie : 動画
     + Image : 背景画像 

なんか、ややこしそうに見えますが、

・MainView の上に ScrollView が乗っかる。
・ScrollView の上にページ単位となる ViewController が乗っかる。
・ページ単位の ViewController の上に Button などが乗っかる。

という感じです。

■Button のイベントは、ViewController で取れる筈

そんなわけで、Button を貼り付けて、UIViewScroller で取ろうとしたのですが、なぜか飛ばない。

UIApplication
+ MainViewController 
 + UIView
  + UIScrollView
   + UIViewController ←ここに飛んで欲しい
    + UIView
     + Button : ボタンのイベント
     + Movie 
     + Image  

試しに、Main の ViewController の上に Button を置くとうまくイベントを拾えます。

UIApplication
+ MainViewController : ←ここで拾える
 + UIView
  + Button : このイベントは
  + UIScrollView
   + UIViewController
    + UIView
     + Button 
     + Movie 
     + Image  

同じコードを打っているはずなのに何故に???ってことで調べていくと、どうやら、ScrollView のほうが先にタッチイベントを取ってしまうので、その上に乗っているコントロールにはイベントがいきわたらないのですね。なるほど。

# ボタンクリックのイベントは UIControlEventTouchUpInside で取得すればよいのですが、ScrollView のスクロールのために取られてしまうという現象。

■解決にはどうすればいいのか?

UIScrollView をサブクラス化して、なんらかの Touch イベントを拾えば解決できそうなんですが、ScrollView 内に複数の ViewController が乗ることになるので、これにはひと工夫必要そう。

なので、まだ未解決。まあ、ボタンなんかは MainViewController に貼り付けておいて、Main のほうでイベントを拾うってのが逃れ方なのでしょうけど、あまりスマートではないし。なにしろ Page 単位の ViewController が複数あるので、ちょっと考えないと、というところです。

カテゴリー: 開発, iPad | 6件のコメント

iPad の電子書籍アプリを作るよ(1)

とある六本木でとある電子書籍アプリを作っている途中でして…全くの iPad/iPhone の素人プログラマ(.NET プログラミングに関してはプロねッ!!!プロッ!!!) が、玄人になるまでの航跡だったらいいなぁと。

iPad電子書籍アプリ開発ガイドブック
http://www.amazon.co.jp/dp/4844329065

賛否両論ありましょうが、最初の一冊が↑です。
電子書籍アプリに特化しているので、その他のアプリ(ゲームアプリとかネットにアクセスするアプリとか)には全く役に立ちませんが、逆に言えば、取っ掛かりとしては十分です。

これを読んで、電子書籍アプリの作成をする際に、

・文章も含めて背景を貼り付ける。
・動画やアニメーションを重ね合わせる。
・透明ボタンを重ね合わせて、ボタン機能を作る。

という方針を決めました。まぁ、お手軽に作るのと、電子書籍自体のページ数がそう多くない場合(50頁以下ぐらい?)ならば、こっちのほうがやりやすいかなと。

以下は、上記を実現するためのコードメモです。

■背景画像の貼り付け

UIView を継承している PageView クラスの drawRect に記述する。
背景で使う画像ファイル(title.jpg)は、Resource フォルダに突っ込んでおく。

in PageView.h

#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>

@interface PageView : UIView {
}
@end

in PageView.h

- (void)drawRect:(CGRect)rect {
	UIImage *img = [UIImage imageNamed:@"title.jpg"];
	[img drawInRect:CGRectMake(0,0,768,1024)];
}

■動画の表示

よくわからんですが、UIView::initWithFrame のところ(初期化かな?)で、MPMoviePlayerController クラスを使って動画を表示させます。カレントビューに [self addSubview:movie.view]; な形で追加して指定した矩形に表示させます。
本当は、動画の終了を検知しないといけないんですけどね、これは後ほど。

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        // Initialization code
		self.backgroundColor = [UIColor whiteColor];
		
		MPMoviePlayerController *movie = [[MPMoviePlayerController alloc] 
			initWithContentURL:[NSURL fileURLWithPath:[
			[NSBundle mainBundle] pathForResource:@"m01" ofType:@"mov"]]];
		movie.controlStyle = MPMovieControlStyleNone;
		movie.scalingMode  = MPMovieScalingModeFill;
		movie.view.frame = CGRectMake(50,50,400,300);
		[movie play];
		[self addSubview:movie.view];
    }
    return self;
}

■横スクロールさせる

ページ送りは、いくつか方法があるのですが、指でスライドさせることでページを送れるスクロール方式にします。
ちょっと、方法は後で変えるかもしれませんが、実験コードということで、ざっと。

UIView を継承した PageView クラスの表示部分で、3 枚の背景画像を貼り付けます。
なので、横幅が、768 x 3 ドットのビューができますね。

in PageView.m

- (void)drawRect:(CGRect)rect {
    // Drawing code
	UIImage *img1 = [UIImage imageNamed:@"title.jpg"];
	[img1 drawInRect:CGRectMake(768*0,0,768,1024)];
	UIImage *img2 = [UIImage imageNamed:@"winemovie01.jpg"];
	[img2 drawInRect:CGRectMake(768*1,0,768,1024)];
	UIImage *img3 = [UIImage imageNamed:@"last.jpg"];
	[img3 drawInRect:CGRectMake(768*2,0,768,1024)];
}

これを横スクロールさせるために UIViewController クラスを継承した SamplePad04ViewController のようなクラスの loadView メソッドを書き換えます。

in SamplePad04ViewController.m

- (void)loadView {
	[super loadView];
	PageView *pageView = [[PageView alloc]
						  initWithFrame:CGRectMake(0,0,768*3,1024)];
	UIScrollView *scrollView = [[UIScrollView alloc]
								initWithFrame:self.view.bounds];

	[scrollView setContentSize:pageView.frame.size];
	[self.view addSubview:scrollView];
	[scrollView addSubview:pageView];
	[scrollView setDelegate:self];
	[scrollView setPagingEnabled: YES];
}

表示するビューの親子関係は UIViewController -> UIScrollView -> PageView になります。
親子関係は addSubview メソッドを使います(そうらしい)。

■参考にしたサイト

iPhoneアプリ開発、その(118) UIScrollViewはどうやって使うのか?|テン*シー*シー
http://ameblo.jp/xcc/entry-10322378932.html
UIScrollView
http://iphone-tora.sakura.ne.jp/uiscrollview.html

カテゴリー: 開発, iPad | iPad の電子書籍アプリを作るよ(1) はコメントを受け付けていません

SqlBulkCopy のスピードは 20 倍ぐらい早い

SQL Server に insert を繰り返してデータを入れる場合は、

・bcp を使う。
・bulk insert を使う。
・SqlBulkCopy を使う。

を使います。bcp や bulk insert の場合は、ファイルからインポートするのでちょっと扱いづらい。SQL Server が別のマシン(サーバー機)にある場合は、一度ファイル転送をするか、ファイル共有をしないといけないので、ちょっと面倒です。

なので、SqlBulkCopy を使う…ってところまでは知っていたのですが、果たしてどのぐらいのスピードかどうかは定かだではないので、測定してみました。

結論から言えば、20 倍ほど早くなります。SQL Server 2008 の場合は 30 倍ほど、SQL Server 2000 の場合は 10-20 倍ほどなので業務コードに入れる場合は実測が必須ですね。

以下は、

create table bulk0 (
  id int,
  val varchar(100)
)

のテーブルに 10 万件のデータを挿入したときの結果です(CPU 1.7GHz程度)

Normal 3.37 sec
Insert 46.34 sec
Normal 4.00 sec
Insert 43.29 sec
Normal 1.85 sec
Insert 47.50 sec
avg. Normal     3.07 sec
avg. Insert     45.71 sec

以下は、実験用のコード。
SqlBulkCopy には、DataTable あるいは DataRow の配列を渡せるので、大量データの挿入が非常に楽になります。まあ、大量すぎる場合は、DataTable のメモリ溢れに注意する必要がありますが on memory に乗る量であれば、この程度で ok ということで。

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace SampleBluk
{
	class TestBulk
	{

		public void Go()
		{
			double t1 = 0.0;
			double t2 = 0.0;
			_dt = MakeDataTable();

			int max = 3;
			for (int i = 0; i < max; i++)
			{
				DateTime start;
				double span;

				setup();
				start = DateTime.Now;
				TestNormal();
				span = ((TimeSpan)(DateTime.Now - start)).TotalSeconds;
				Console.WriteLine(&quot;Normal {0:0.00} sec&quot;, span);
				t1 += span;

				setup();
				start = DateTime.Now;
				TestInsert();
				span = ((TimeSpan)(DateTime.Now - start)).TotalSeconds;
				Console.WriteLine(&quot;Insert {0:0.00} sec&quot;, span);
				t2 += span;

			}
			Console.WriteLine(&quot;avg. Normal	{0:0.00} sec&quot;, t1 / max);
			Console.WriteLine(&quot;avg. Insert	{0:0.00} sec&quot;, t2 / max);
            // avg. Normal     3.07 sec
            // avg. Insert     45.71 sec
		}

		int _max = 100000;
		DataTable _dt;
		// 接続文字列
		const string CNSTR =
			@&quot;Data Source=.\sqlexpress;Initial Catalog=stress;Integrated Security=True;Pooling=False&quot;;

		private string toMD5(string s)
		{
			//文字列をbyte型配列に変換する
			byte[] data = System.Text.Encoding.UTF8.GetBytes(s);

			//MD5CryptoServiceProviderオブジェクトを作成
			System.Security.Cryptography.MD5CryptoServiceProvider md5 =
				new System.Security.Cryptography.MD5CryptoServiceProvider();
			//または、次のようにもできる
			//System.Security.Cryptography.MD5 md5 =
			//  u  System.Security.Cryptography.MD5.Create();

			//ハッシュ値を計算する
			byte[] bs = md5.ComputeHash(data);

			//byte型配列を16進数の文字列に変換
			System.Text.StringBuilder result = new System.Text.StringBuilder();
			return BitConverter.ToString(bs).ToLower().Replace(&quot;-&quot;, &quot;&quot;);
		}

		public DataTable MakeDataTable()
		{
			DataTable dt = new DataTable();
			dt.Columns.Add(new DataColumn(&quot;id&quot;, typeof(int)));
			dt.Columns.Add(new DataColumn(&quot;val&quot;, typeof(string)));
			string s = toMD5(DateTime.Now.ToString());
			for (int i = 0; i < _max; i++)
			{
				DataRow row = dt.NewRow();
				dt.Rows.Add(row);
				row[&quot;id&quot;] = i;
				row[&quot;val&quot;] = s;
				s = toMD5(s);
			}
			return dt;
		}
		public void setup()
		{
			SqlConnection cn = new SqlConnection(CNSTR);
			SqlCommand cmd = new  SqlCommand(&quot;truncate table bulk0&quot;, cn);
			cn.Open();
			cmd.ExecuteNonQuery();
			cn.Close();
		}


		public void TestNormal()
		{
			DataTable dt = _dt;

			SqlConnection cn = new SqlConnection(CNSTR);
			SqlBulkCopy bc = new SqlBulkCopy(cn);
			bc.DestinationTableName = &quot;bulk0&quot;;
			cn.Open();
			bc.WriteToServer(dt);
			cn.Close();
		}
		public void TestInsert()
		{
			DataTable dt = _dt;

			SqlConnection cn = new SqlConnection(CNSTR);
			SqlCommand cmd = new SqlCommand(&quot;insert into bulk0 ( id, val ) values ( @id, @val ) &quot;, cn);
			cmd.Parameters.Add(new SqlParameter(&quot;@id&quot;, SqlDbType.Int));
			cmd.Parameters.Add(new SqlParameter(&quot;@val&quot;, SqlDbType.VarChar, 100));

			cn.Open();
			foreach (DataRow row in dt.Rows)
			{
				cmd.Parameters[&quot;@id&quot;].Value = row[&quot;id&quot;];
				cmd.Parameters[&quot;@val&quot;].Value = row[&quot;val&quot;];
				cmd.ExecuteNonQuery();
			}
			cn.Close();
		}
	}
}
カテゴリー: 開発, C# | SqlBulkCopy のスピードは 20 倍ぐらい早い はコメントを受け付けていません

StringBuilder はどれだけ早いのだろうか?、実は大してかわりません

SqlCommand や DataTable を使うときに文字列をたくさん使うのですが、果たして世間一般(?)で言われているほど、String は遅く、StringBuilder は早いのでしょうか?というベンチマークです。

非常に長い場合は StringBuilder を使うほうが良いのですが、SqlCommand などに渡す SQL 文程度(長くても 5000 文字ぐらい)は、どうなのでしょうか?ということです。

■結論

結論から言えば、大して変わりません。以下は 20,000 件のデータを廻したときの結果です。

avg. Normal 7.54 sec
avg. StirngBuilder 7.22 sec
avg. SqlCommand 7.28 sec

違いは 2,3 % ぐらいしかないので誤差の範囲ですね。

以下は、ベンチマーク用の実験コード

■通常の String パターン

''' <summary>
''' 1.通常の string の連結で作成
''' </summary>
''' <remarks></remarks>
Public Sub TestNormal()
    Dim s As String = toMD5(DateTime.Now.ToString())

    Dim cn As New SqlConnection(&quot;&quot;)
    For j As Integer = 0 To MAX - 1
        Dim sql As String = &quot;&quot;
        ' よくある string.Format を使った作り方
        sql += &quot;SELECT * FROM DUAL &quot;
        sql += &quot; WHERE 1 = 1 &quot;
        sql += String.Format(&quot; AND col0 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col1 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col2 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col3 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col4 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col5 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col6 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col7 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col8 = '{0}' &quot;, s) : s = toMD5(s)
        sql += String.Format(&quot; AND col9 = '{0}' &quot;, s) : s = toMD5(s)

        Dim dt As New DataTable
        Dim da As New SqlDataAdapter(sql, cn) 'dummy
    Next
End Sub

string.Format を使って、ぽちぽちと文字列をフォーマットしていきます。

■StringBuilder を使ったパターン

よくあるように、文字列の連結は StringBuilder を使えッ!!! ってことなので、使ってみたのですが、大して変わりません。もっと長い SQL の場合、効果があるんでしょうが…そんなに長い SQL を書くのはどうなの???ってことなのです。

''' <summary>
''' 2.StringBuilder を使う
''' </summary>
''' <remarks></remarks>
Public Sub TestStringBuilder()
    Dim s As String = toMD5(DateTime.Now.ToString())

    Dim cn As New SqlConnection(&quot;&quot;)
    For j As Integer = 0 To MAX - 1
        Dim sql As String = &quot;&quot;
        ' よくある StringBuilder を使った作り方
        Dim sb As New System.Text.StringBuilder(&quot;&quot;)
        sb.Append(&quot;SELECT * FROM DUAL &quot;)
        sb.Append(&quot; WHERE 1 = 1 &quot;)
        sb.AppendFormat(&quot; AND col0 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col1 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col2 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col3 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col4 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col5 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col6 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col7 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col8 = '{0}' &quot;, s) : s = toMD5(s)
        sb.AppendFormat(&quot; AND col9 = '{0}' &quot;, s) : s = toMD5(s)

        Dim dt As New DataTable
        Dim da As New SqlDataAdapter(sql, cn) 'dummy
    Next
End Sub

■SqlCommand を使う

SqlCommand を使うと、最初に SQL 文を使うので文字列編集部分が極端に減ります。
これの場合は、20000 回の編集が 1 回になるわけですが、劇的に早くなる…はずなんですけど、結果は変わりません。

''' <summary>
''' SqlCommand で Const/Dim  を使う
''' </summary>
''' <remarks></remarks>
Public Sub TestConst()
    Dim s As String = toMD5(DateTime.Now.ToString())
    Dim cn As New SqlConnection(&quot;&quot;)

    ' ここは dim でも同じ
    Const sql As String = _
        &quot;SELECT * FROM DUAL0 &quot; + _
        &quot; WHERE 1 = 1 &quot; + _
        &quot; AND col0 = @col0 &quot; + _
        &quot; AND col1 = @col1 &quot; + _
        &quot; AND col2 = @col2 &quot; + _
        &quot; AND col3 = @col3 &quot; + _
        &quot; AND col4 = @col4 &quot; + _
        &quot; AND col5 = @col5 &quot; + _
        &quot; AND col6 = @col6 &quot; + _
        &quot; AND col7 = @col7 &quot; + _
        &quot; AND col8 = @col8 &quot; + _
        &quot; AND col9 = @col9 ; &quot;

    Dim cmd As New SqlCommand(sql, cn)
    cmd.Parameters.Add(New SqlParameter(&quot;@col0&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col1&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col2&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col3&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col4&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col5&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col6&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col7&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col8&quot;, Nothing))
    cmd.Parameters.Add(New SqlParameter(&quot;@col9&quot;, Nothing))

    For j As Integer = 0 To max - 1
        cmd.Parameters(&quot;@col0&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col1&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col2&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col3&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col4&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col5&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col6&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col7&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col8&quot;).Value = s : s = toMD5(s)
        cmd.Parameters(&quot;@col9&quot;).Value = s : s = toMD5(s)
        Dim dt As New DataTable
        Dim da As New SqlDataAdapter(cmd) 'dummy
    Next
End Sub

以上、こんな風に実験してみると普通に string を使っている限りはスピードに変化はありません、ってことです。
ただし、データベースアクセスに関しては、単純な SqlDataAdapter の呼び出しよりも、SqlCommand でプリコンパイル SQL を使ったほうが、10 倍以上早くなるので、件数が多い場合はパフォーマンスに注意が必要です。

カテゴリー: 開発, C# | 5件のコメント

意外と遅い DataTable 、なので List を使うと 5 倍早くなる

以前から気になっていたのですが、DataTable/DataSet を使うと遅いのでは?と思っていました。
実際、Visual Studio で自動生成する型付の DataTable を使うと思ったように性能がでないことが多く、結局 SQL でチューニング、ってことになります。

で、具体的に遅そうなところを実験してみました。

単純に DataTable の性能を比較したいので、データベースには使わず値の代入だけ実験します。

  1. 列が 100 のテーブルを想定する。
  2. 行数を 10000 件挿入する。

これを次のパターンで比較します。

  1. 普通に DataTable を使う
  2. With 構文を使って、高速化する?
  3. for earc を使ってカウンタを使わない方法
  4. 名前を使わずに index を使う
  5. generic list を使う
  6. generic list で構造体/クラスを使う

先に結論から書くと、1 から 4 までは大してスピードは変わりません。
期待してたのは for each を使うと早くなれば良かったんですけどね。5 % ぐらいしか早くなりません。

ですが、DataTable を使うのを止めて、List コレクションを使うと 5 倍ぐらい早くなります。

avg. Go1 13.88 sec
avg. Go2 13.09 sec
avg. Go3 12.98 sec
avg. Go4 12.93 sec
avg. Go5 2.12 sec
avg. Go6 2.08 sec

どうも、単純に DataTable.Rows へのアクセスが複雑怪奇なんでしょうねぇ。DataSet, DataTable が出来たのは generic ができる前なので、そのあたりの制限で遅くなっているのかもしれません。
.NET Framework の標準ライブラリ(あるいは自動生成したもの)も generic を考慮していないコードがあるので、そのあたり、パフォーマンス的に無駄になっている可能性があるということのなのかも。

以下は、VB のサンプルコードです。

■素直に DataTalbe を使う

''' <summary>
''' 素直に Item に名前を指定する
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go1() As Double
	Dim dt As DataTable = MakeDataTable()

	' あえて最初に行を追加しておく
	For i = 0 To 10000 - 1
		Dim row As DataRow = dt.NewRow()
		dt.Rows.Add(row)
	Next

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1
		dt.Rows(i).Item(&quot;col000&quot;) = tm.ToString() : tm.AddSeconds(1)
		dt.Rows(i).Item(&quot;col001&quot;) = tm.ToString() : tm.AddSeconds(1)
		dt.Rows(i).Item(&quot;col002&quot;) = tm.ToString() : tm.AddSeconds(1)
		...
		dt.Rows(i).Item(&quot;col097&quot;) = tm.ToString() : tm.AddSeconds(1)
		dt.Rows(i).Item(&quot;col098&quot;) = tm.ToString() : tm.AddSeconds(1)
		dt.Rows(i).Item(&quot;col099&quot;) = tm.ToString() : tm.AddSeconds(1)
	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine(&quot;Go1 {0:#.00} sec&quot;, span)
	Return span
End Function

■With 構文を使う

いわゆるポインタアクセスの代わりに with 構文を使います。
dt.Rows(i) のところが無くなるので、コード的には安全になるのですが、スピードは 2,3 % 程度早くなる程度です。

''' <summary>
''' With 構文を使ってみる
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go2() As Double
	Dim dt As DataTable = MakeDataTable()

	' あえて最初に行を追加しておく
	For i = 0 To 10000 - 1
		Dim row As DataRow = dt.NewRow()
		dt.Rows.Add(row)
	Next

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1
		With dt.Rows(i)
			.Item(&quot;col000&quot;) = tm.ToString() : tm.AddSeconds(1)
			.Item(&quot;col001&quot;) = tm.ToString() : tm.AddSeconds(1)
			.Item(&quot;col002&quot;) = tm.ToString() : tm.AddSeconds(1)
			...
			.Item(&quot;col097&quot;) = tm.ToString() : tm.AddSeconds(1)
			.Item(&quot;col098&quot;) = tm.ToString() : tm.AddSeconds(1)
			.Item(&quot;col099&quot;) = tm.ToString() : tm.AddSeconds(1)
		End With
	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine(&quot;Go2 {0:#.00} sec&quot;, span)
	Return span
End Function

■for each を使う

for each を使うと劇的に早くなる…のが良かったのですが、あまり早くなりません。
10 % 弱ほど早くなります。

''' <summary>
''' for each を使う
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go3() As Double
	Dim dt As DataTable = MakeDataTable()

	' あえて最初に行を追加しておく
	For i = 0 To 10000 - 1
		Dim row As DataRow = dt.NewRow()
		dt.Rows.Add(row)
	Next

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For Each row As DataRow In dt.Rows
		With row
			.Item(&quot;col000&quot;) = tm.ToString() : tm.AddSeconds(1)
			.Item(&quot;col001&quot;) = tm.ToString() : tm.AddSeconds(1)
			.Item(&quot;col002&quot;) = tm.ToString() : tm.AddSeconds(1)
			...
			.Item(&quot;col097&quot;) = tm.ToString() : tm.AddSeconds(1)
			.Item(&quot;col098&quot;) = tm.ToString() : tm.AddSeconds(1)
			.Item(&quot;col099&quot;) = tm.ToString() : tm.AddSeconds(1)
		End With
	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine(&quot;Go3 {0:#.00} sec&quot;, span)
	Return span

End Function

■名前を使わずに数値を使う

比較のために列名ではなくて、番号を使ってアクセスさせてみます…が、あまり早くなりません。
for each と同じで 10 % ぐらいですね。

''' <summary>
''' 名前を使わずに index を使う
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go4() As Double
	Dim dt As DataTable = MakeDataTable()

	' あえて最初に行を追加しておく
	For i = 0 To 10000 - 1
		Dim row As DataRow = dt.NewRow()
		dt.Rows.Add(row)
	Next

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1
		With dt.Rows(i)
			.Item(0) = tm.ToString() : tm.AddSeconds(1)
			.Item(1) = tm.ToString() : tm.AddSeconds(1)
			.Item(2) = tm.ToString() : tm.AddSeconds(1)
			...
			.Item(97) = tm.ToString() : tm.AddSeconds(1)
			.Item(98) = tm.ToString() : tm.AddSeconds(1)
			.Item(99) = tm.ToString() : tm.AddSeconds(1)
		End With
	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine(&quot;Go4 {0:#.00} sec&quot;, span)
	Return span

End Function

■list を使う

これ以上は無理かなぁと思って、list に変えてみます。
最初は、list(of object()) で、object の配列にしてみます。
すると、すさまじく早くなります。5,6 倍は早くなりますね。

''' <summary>
''' generic List を使う
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go5() As Double

	Dim dt As New List(Of Object())

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1
		Dim item(100) As Object
		item(0) = tm.ToString() : tm.AddSeconds(1)
		item(1) = tm.ToString() : tm.AddSeconds(1)
		item(2) = tm.ToString() : tm.AddSeconds(1)
		...
		item(97) = tm.ToString() : tm.AddSeconds(1)
		item(98) = tm.ToString() : tm.AddSeconds(1)
		item(99) = tm.ToString() : tm.AddSeconds(1)

		dt.Add(item)

	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine(&quot;Go5 {0:#.00} sec&quot;, span)
	Return span

End Function

■list に構造体を使う

object の配列では扱いづらいので、型付 DataTable 風にフィールドでアクセスできるように POJO なクラスを作ります。
アクセスが遅くなると思いきや…全然変わりませんね。
DataRow のところにクラスを使えるのが generic の良いところです。

''' <summary>
''' generic List で構造体を使う
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function Go6() As Double

	Dim dt As New List(Of TITEM)

	Dim start As Date = Date.Now
	Dim tm As Date = Date.Now
	' あえて1カラムずつ追加する
	For i = 0 To 10000 - 1

		Dim it As New TITEM
		With it
			.col000 = tm.ToString() : tm.AddSeconds(1)
			.col001 = tm.ToString() : tm.AddSeconds(1)
			.col002 = tm.ToString() : tm.AddSeconds(1)
			...
			.col097 = tm.ToString() : tm.AddSeconds(1)
			.col098 = tm.ToString() : tm.AddSeconds(1)
			.col099 = tm.ToString() : tm.AddSeconds(1)
		End With

		dt.Add(it)

	Next
	Dim tend As Date = Date.Now
	Dim span As Double = (tend - start).TotalSeconds()
	Console.WriteLine(&quot;Go6 {0:#.00} sec&quot;, span)
	Return span

End Function

使うクラス

Public Class TITEM
	Public col000 As String
	Public col001 As String
	Public col002 As String
	...
	Public col097 As String
	Public col098 As String
	Public col099 As String
End Class

という訳で、DataTable.Rows を使うよりも List(Of クラス) を使ったほうが 5 倍早いわけです。
このあたり、SQL Server へのアクセス(SqlCommand, SqlDataAdapter, Linq)の組み合わせで考えると、

  • データベースは SqlCommand でアクセス
  • 取得したデータのアクセスは generic List を利用

とする組み合わせ一番早いわけです。

さて、これをどのように実装するのか?あるいは、既にあるのか?ってことですね。これは後程。

~~~

追記 2015/06/02 C# で書き換えたのはこちら
DataTable よりも List を使うと 10 倍早くなる(続編) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/7217

カテゴリー: 開発, C# | 6件のコメント

SELECT のパフォーマンスチェック(続き)

.NET を使って SQL Server に対して SELECT するには、ってことでパフォーマンスのチェックをしました。

  1. 10 個のテーブルに 20000 件ずつデータを入れておきます。
  2. 10 個のテーブルに対して、2000 回ずつ検索します。
  3. テーブルには主キーを付けておきます。

という形でデータを作っておいて、以下のパターンで検索をします。

  1. 個別に SqlDataAdapter を使って呼び出し
  2. 10 個のテーブルの検索をひとつにまとめて、SqlDataAdapter で呼び出し
  3. 10 個のテーブルの検索をひとつにまとめて、SqlCommand で呼び出し
  4. LINQ to Entities を使って呼び出し
  5. LINQ to SQL を使って呼び出し
1.個別SELECT
56.5
59.3
57.5

2.まとめてSELECT
※ 1 より遅くなったのでパス

3.SqlCommand
7.6
7.5
7.4

4.LINQ to Entities
112.5
110.6
108.2

5.LINQ to SQL
92.4
112.3
93.9

結論から言えば、

  1. 個別SELECTよりも、LINQ を使うと 2 倍ぐらい遅くなる。
  2. LINQ to Entities と LINQ to SQL はスピードが変わらない。
  3. SqlCommand を使うと、個別SELECTよりも 5 倍以上、LINQ よりも 10 倍以上早い。

という結果になりました。

LINQ の場合、10 個のテーブルに対して、検索を実行するために 10 回呼び出してしまうので、この手の方法は不利なのですが、SqlDataAdapter よりも遅いのは意外です…スピードが要求されるところで、LINQ を使うのは禁物ですね。

パフォーマンスのチューニングとしては、SqlCommand が一番やりやすく、10 個のテーブルに対する SELECT を 1 回で済ませられるのも SQL Server で複数の検索結果(DataTable)を返せるからです。
また、prepared statements という形で、あらかじめ SQL 文を発行させて再利用できるとろも SqlCommand の有利なところですね。

実験ソースを晒しておきます。

■テーブルの作成

/// <summary>
/// テーブル作成
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void button1_Click(object sender, EventArgs e)
{
    string sql = &quot;&quot;;
    SqlConnection cn = new SqlConnection(CNSTR);
    cn.Open();
    SqlCommand cmd;
    for (int i = 0; i < 10; i++)
    {
        sql = string.Format(&quot;drop table table{0}&quot;, i);
        cmd = new SqlCommand(sql, cn);
        try
        {
            cmd.ExecuteNonQuery();
        }
        catch { }

        sql = string.Format(&quot;create table table{0} (&quot;, i);
        for (int j = 0; j < 10; j++)
        {
			if (j == 0)
			{
				sql += string.Format(&quot;col{0} varchar(50) not null,&quot;, j);
			}
			else
			{
				sql += string.Format(&quot;col{0} varchar(50),&quot;, j);
			}
        }
		sql += string.Format(&quot; CONSTRAINT pk_table{0} PRIMARY KEY ( col0 ) &quot;, i );
        sql += &quot;)&quot;;
        cmd = new SqlCommand(sql, cn);
        cmd.ExecuteNonQuery();
    }
    cn.Close();
}

■20000件のデータ insert

/// <summary>
/// データ作成
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void button2_Click(object sender, EventArgs e)
{
    int max = int.Parse(textBox1.Text);
    SqlConnection cn = new SqlConnection(CNSTR);
    cn.Open();

    string s = toMD5(DateTime.Now.ToString());
    int count = 0;
    for (int i = 0; i < 10; i++)
    {
        string sql = string.Format(&quot;insert into table{0} values (&quot;, i);
        SqlCommand cmd = new SqlCommand(&quot;&quot;, cn);
        for (int k = 0; k < 10; k++)
        {
            cmd.Parameters.Add(new SqlParameter(
                string.Format(&quot;@col{0}&quot;, k), SqlDbType.VarChar, 50));
            sql += string.Format(&quot;@col{0},&quot;, k);
        }
        sql = sql.Substring(0, sql.Length - 1);
        sql += &quot;)&quot;;
        cmd.CommandText = sql;

        for (int j = 0; j < max; j++)
        {
            for (int k = 0; k < 10; k++)
            {
                s = toMD5(s);
                cmd.Parameters[k].Value = s;
            }
            cmd.ExecuteNonQuery();
            count++;
            if (count % 100 == 0)
            {
                toStatus(count, max * 10);
            }
        }
    }
    cn.Close();
}

■個別SELECT

まずいやり方ですが、string.Format で値込みで文字列を作っています。
よくあるパターンですね。

/// <summary>
/// 個別SELECT
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void button3_Click(object sender, EventArgs e)
{
    // 律儀にSELECT文を10回発行します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for ( int j=0; j<10; j++ ) {
            DataTable dt = new DataTable();
            SqlDataAdapter da = new SqlDataAdapter(
                string.Format(&quot;SELECT * FROM table{0} WHERE col0 = '{1}'&quot;, j, s), cn);
            s = toMD5(s);
            da.Fill( dt );
        }
        if (++count % 10 == 0)
            toStatus(count, max);
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString(&quot;#.0&quot;);
}

■まとめてSELECT

SqlDataAdapter に渡す SQL 文をひとまとめにしたのですが、全然駄目です。

/// <summary>
/// まとめてSELECT
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
/// <remarks>
/// 遅くてぜんぜんだめ
/// </remarks>
private void button4_Click(object sender, EventArgs e)
{
    // 10回のSELECTをひとまとめにして DataSet に保存します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    string sql = &quot;&quot;;
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            sql += string.Format(&quot;SELECT * FROM table{0} WHERE col0 = '{1}' &quot;, j, s);
            s = toMD5(s);
        }
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(sql, cn);
        da.Fill(ds);
        if (++count % 10 == 0)
            toStatus(count, max );
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString(&quot;#.0&quot;);
}

■SqlCommand を使う

あらかじめコンパイルされるように SqlCommand を使います。
ループの中で SqlCommand を使ってはいけません。ループの外で作成して、パラメータで渡します。

/// <summary>
/// SqlCommand の利用
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void button5_Click(object sender, EventArgs e)
{
    // 10回のSELECTをひとまとめにして DataSet に保存します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    string sql = &quot;&quot;;
</p>
<p>
    SqlCommand cmd = new SqlCommand(&quot;&quot;,cn);
    for (int j = 0; j < 10; j++)
    {
        sql += string.Format(&quot;SELECT * FROM table{0} WHERE col0 = @param{1} &quot;, j, j);
        s = toMD5(s);
        cmd.Parameters.Add(new SqlParameter(
            string.Format(&quot;@param{0}&quot;, j), SqlDbType.VarChar, 50));
    }
    cmd.CommandText = sql;
</p>
<p>
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            cmd.Parameters[j].Value = s;
            s = toMD5(s);
        }
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(ds);
        if (++count % 10 == 0)
            toStatus(count, max);
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString(&quot;#.0&quot;);
}

■LINQ to Entities

比較のために LINQ to Entities でも作ってみます。
LINQ の場合は、複数の SELECT 文をまとめることができないので、ループの中で 10 回呼び出します。この方法は、SqlDataAdapter と同じですよね。
スピードを比較したものがないので、初めて試してみたのですが、個別SELECTよりも2倍ほどおそくなります。

/// <summary>
/// LINQ to Entities の利用
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void button6_Click(object sender, EventArgs e)
{
	// 律儀にSELECT文を10回発行します。
	DateTime start = DateTime.Now;
	string s = toMD5(start.ToString());
	int count = 0;

	stressEntities ent = new stressEntities();
	int max = 2000;
	for (int i = 0; i < max; i++)
	{
		var items0 = from t in ent.table0
					where t.col0 == s
					select t;
		s = toMD5(s);
		var item1 = from t in ent.table1
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items2 = from t in ent.table2
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items3 = from t in ent.table3
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items4 = from t in ent.table4
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items5 = from t in ent.table5
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items6 = from t in ent.table6
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items7 = from t in ent.table7
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items8 = from t in ent.table8
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items9 = from t in ent.table9
						where t.col0 == s
						select t;
		s = toMD5(s);

		var it0 = items0.SingleOrDefault();
		var it1 = items0.SingleOrDefault();
		var it2 = items0.SingleOrDefault();
		var it3 = items0.SingleOrDefault();
		var it4 = items0.SingleOrDefault();
		var it5 = items0.SingleOrDefault();
		var it6 = items0.SingleOrDefault();
		var it7 = items0.SingleOrDefault();
		var it8 = items0.SingleOrDefault();
		var it9 = items0.SingleOrDefault();

		if (++count % 10 == 0)
			toStatus(count, max);
	}
	DateTime end = DateTime.Now;
	textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString(&quot;#.0&quot;);
}

■LINQ to SQL を利用

比較のために LINQ to SQL でも動かしてみます。
不利なのは、LINQ to Entities と同じで、10 回呼び出さないといけないところですね。
スピードに関しては、LINQ to Entities と変わりません。

/// <summary>
/// LINQ to SQL の利用
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void button7_Click(object sender, EventArgs e)
{
	// 律儀にSELECT文を10回発行します。
	DateTime start = DateTime.Now;
	string s = toMD5(start.ToString());
	int count = 0;

    SqlConnection cn = new SqlConnection(CNSTR);
	DataClasses1DataContext cnt = new DataClasses1DataContext(cn);

	int max = 2000;
	for (int i = 0; i < max; i++)
	{
		var items0 = from t in cnt.Dtable0
						where t.col0 == s
						select t;
		s = toMD5(s);
		var item1 = from t in cnt.Dtable1
					where t.col0 == s
					select t;
		s = toMD5(s);
		var items2 = from t in cnt.Dtable2
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items3 = from t in cnt.Dtable3
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items4 = from t in cnt.Dtable4
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items5 = from t in cnt.Dtable5
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items6 = from t in cnt.Dtable6
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items7 = from t in cnt.Dtable7
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items8 = from t in cnt.Dtable8
						where t.col0 == s
						select t;
		s = toMD5(s);
		var items9 = from t in cnt.Dtable9
						where t.col0 == s
						select t;
		s = toMD5(s);

		var it0 = items0.SingleOrDefault();
		var it1 = items0.SingleOrDefault();
		var it2 = items0.SingleOrDefault();
		var it3 = items0.SingleOrDefault();
		var it4 = items0.SingleOrDefault();
		var it5 = items0.SingleOrDefault();
		var it6 = items0.SingleOrDefault();
		var it7 = items0.SingleOrDefault();
		var it8 = items0.SingleOrDefault();
		var it9 = items0.SingleOrDefault();

		if (++count % 10 == 0)
			toStatus(count, max);
	}
	DateTime end = DateTime.Now;
	textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString(&quot;#.0&quot;);
}

そんな訳で、実行時のパフォーマンスが気にかかる場合は、SqlCommand にすると 10 倍ほど早くなるよ、という話です。逆に言えば、たいしてパフォーマンスが気にならないときは、書きやすい LINQ を使ったほうがよいでしょうってことで。

カテゴリー: 開発, C# | SELECT のパフォーマンスチェック(続き) はコメントを受け付けていません

SELECT のパフォーマンスチューニング(メモ)

SELECT のパフォーマンスチューニング(メモ)

ちょっと、メモ的に書き下しておきます。

1. 10 個のテーブルに 20000 件ずつデータを入れておきます。
2. 10 個のテーブルに対して、2000 回ずつ検索します。

というパターンがあったとき、どうやると早いでしょうか?という問題。

■1回ずつ SqlDataAdapter を呼び出す

だいたい 200 秒ぐらいかかります。

/// <summary>
/// 個別SELECT
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void button3_Click(object sender, EventArgs e)
{
    // 律儀にSELECT文を10回発行します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for ( int j=0; j<10; j++ ) {
            DataTable dt = new DataTable();
            SqlDataAdapter da = new SqlDataAdapter(
                string.Format(&quot;SELECT * FROM table{0} WHERE col0 = '{1}'&quot;, j, s), cn);
            s = toMD5(s);
            da.Fill( dt );
        }
        if (++count % 10 == 0)
            toStatus(count, max);
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString(&quot;#.0&quot;);
}

■10回の呼び出しをひとつにまとめる

SQL Server の場合、複数の検索結果を取れるので DataSet を使って 1 回にまとめます。
SQL Server 2008 だと 10 倍ぐらい早くなって 15 秒程度なんですが、
SQL Server 2000 だと、最初のパターンよりも遅くなってしまうんですよね…何故だろう?

/// <summary>
/// まとめてSELECT
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
/// <remarks>
/// 遅くてぜんぜんだめ
/// SQL Server 2008 だと ok なのだが、SQL Server 2000 だと駄目らしい。
/// </remarks>
private void button4_Click(object sender, EventArgs e)
{
    // 10回のSELECTをひとまとめにして DataSet に保存します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    string sql = &quot;&quot;;
    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            sql += string.Format(&quot;SELECT * FROM table{0} WHERE col0 = '{1}' &quot;, j, s);
            s = toMD5(s);
        }
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(sql, cn);
        da.Fill(ds);
        if (++count % 10 == 0)
            toStatus(count, max );
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString(&quot;#.0&quot;);
}

■あらかじめ SqlCommand を作成する

prepared sql statement ということで、あらかじめ SqlCommand で作成しておきます。
これをやると、12,3 秒になります。
ループの中で SqlCommand を作ると早くならないので注意が必要ですね。

/// <summary>
/// SqlCommand の利用
/// </summary>
/// <param name=&quot;sender&quot;></param>
/// <param name=&quot;e&quot;></param>
private void button5_Click(object sender, EventArgs e)
{
    // 10回のSELECTをひとまとめにして DataSet に保存します。
    DateTime start = DateTime.Now;
    string s = toMD5(start.ToString());
    int count = 0;
    SqlConnection cn = new SqlConnection(CNSTR);
    string sql = &quot;&quot;;

    SqlCommand cmd = new SqlCommand(&quot;&quot;,cn);
    for (int j = 0; j < 10; j++)
    {
        sql += string.Format(&quot;SELECT * FROM table{0} WHERE col0 = @param{1} &quot;, j, j);
        s = toMD5(s);
        cmd.Parameters.Add(new SqlParameter(
            string.Format(&quot;@param{0}&quot;, j), SqlDbType.VarChar, 50));
    }
    cmd.CommandText = sql;

    int max = 2000;
    for (int i = 0; i < max; i++)
    {
        for (int j = 0; j < 10; j++)
        {
            cmd.Parameters[j].Value = s;
            s = toMD5(s);
        }
        DataSet ds = new DataSet();
        SqlDataAdapter da = new SqlDataAdapter(cmd);
        da.Fill(ds);
        if (++count % 10 == 0)
            toStatus(count, max);
    }
    DateTime end = DateTime.Now;
    textBox2.Text = ((TimeSpan)(end - start)).TotalSeconds.ToString(&quot;#.0&quot;);
}

更に高速化する場合はどうするんでしょうね?
.NET の文だけおそくなるから、C++ で書き直すと早くなるのかな?

カテゴリー: 開発, C# | SELECT のパフォーマンスチューニング(メモ) はコメントを受け付けていません

時には機動警察パトレーバーのように、あるいは、600 万件のデータ加工を

たまぁーに、1巻だけ取り出して読み進めて、最後の熊耳巡査が出てくるところ(手元にあるのは文庫本)で、毎度思うところがある。

泉巡査がイングラムの能力を十分引き出せないのは何故か?格闘技術に疎いから、という結論になるのだけれど、イングラムというハードウェアに、イングラムを動かすソフトウェア(学習機能付き)が乗り、更に人の動かし方が最終的にイングラムの能力を決定づける(「個性」ともいうし、「個体差」ともいう)、という流れになる。

ハードウェアというのは、パソコンだったり、CPU だったり、メモリだったりするわけで、OS というのは、Windows だったり、Mac だったりする。いや、OSの取り方をもうちょっと広げれば、SQL Server だったり、Oracle だったり、.NET Framework だたり、するわけです。で、これを使うのが人なわけで、この場合はプログラマなのですね。

最近のハードウェアは 10 年前よりもぐんと良くなっているわけで、メモリが 2GB なんてのは当たり前で、HDD も 500GB という形で桁が違います。なので、同じ大量のデータを扱うにしても、昔のデータベースの扱い方と、最近の扱い方では全く違う…と言いますか、ハードウェアが違うところにが関わって、でてくる性能に大きな違いがでます…と言いますか、作り方によって全く違うってのを実感しています。

たまたま、600 万件のアクセスログを解析しようと思ったわけですが、この 600 万件というデータ、10 年前だったら Oracle でもひいこらいう程度のものなので、ちょっと躊躇しました。テキストデータで 2GB 弱あるわけです。
で、ひとまず、手元の SQL Server 2008 Experss Edition にテキストデータを入れて、データ解析をしてみると、これが早い早い。最初のインポート(C# で作りました)はツールの作りが悪いのか、2 時間程データ挿入に掛かるのですが、その後データを加工する場合は、クエリを使うと結構のスピードで動きます。

インデックスが無い状態でも、全検索で 3 分位で結果がでるし、適当なインデックスをつければ 2,3 秒かからずに結果がでてきます。データの加工ですら、1 分位で済みます。

データ加工するところは、

25/May/2011:23:58:39 +0900

のようなアクセスログの日付データを、データベースの Datetime 型に直します。
なので、update 文で使えるように、

2011-5-25 23:58:39

な風に変えないと駄目なわけです。

最初は、C# のツールを使ってやろうと思ったのですが、そもそも SqlCommand を使って insert 文を使ってデータ挿入をすると、600 万件挿入するのに 2 時間かかるわけです。ということは、同じように SqlCommand を使って update 文を使ってデータを変更すると、2 時間掛かるのではないか?という予想が経ちますね。
データ加工のたびに、こんなに時間が掛かってしまってはろくなデータ解析ができません。

で、この遅い理由としては、

  1. SqlCommand の呼び出しで、SQL Server との通信が入っている。
  2. SqlCommand の呼び出しで、.NET Framework とネイティブデータの変換が入っている。
  3. SqlCommand の呼び出しで、insert 文の解析が Sql Server で行われている。

が考えられるわけです。

1 の場合は、データ通信がなくなるように直接 SQL Server 上で行います。今回は、同じパソコン内でデータベースを動かしているので、あまり関係がないでしょう。
2 の場合は、C# でツールを作る限り駄目です。.NET Framework(実は Java も同じ)の場合は、.NET とネイティブデータの変換が必ず入ってしまうので、C/C++ で直接データを書き込むよりも遅くなるのは当たり前なのです。これは、SQL Server 自体が C/C++ で書かれている(内部データ自体は単なるバイナリ形式)であろうことから予想ができます。
3. SQL 文を解析しないようにして、bulk データを挿入できればよいのですが、あいにく SQL Server には bulk 以外にデータを挿入する方法がありません。SQLite みたいに SQL 文ではない形でデータ加工ができればよいんですけどね。

という訳で、2 時間の作業を短縮させるためには、2 のように .NET で書かないという方法を取らないと駄目なのです。

さて、ツールを作るならば C/C++ を使ってもよいのですが、結構面倒です(ADOを扱えばいいんですが、まぁ、面倒と言えば面倒)。なので、SQL Server で直接クエリを実行するようにします。

直接クエリを実行する場合は、SQL Server Management Studio でクエリ文を作るか、最終的にファンクションあるいはストアドプロシージャにしてしまうかです。

で、ざっくりと作ったの以下です。

CREATE function [dbo].[getltime]
( @d as varchar(50) )
returns datetime
as
begin
declare @dt varchar(50)
declare @i int
declare @day   varchar(10)
declare @month varchar(10)
declare @year  varchar(10)
declare @time  varchar(20)
declare @gmt   varchar(20)
declare @ltime datetime
declare @gtime datetime

if @d is null 
  return null

set @dt = @d
set @i = CHARINDEX( '/', @dt )
if @i = 0 
 return @d

set @day = SUBSTRING( @dt,0,@i )
set @dt = RIGHT(@dt,LEN(@dt)-@i)
set @i = CHARINDEX( '/', @dt )
if @i = 0 
 return @d
set @month = SUBSTRING( @dt,0,@i )
set @dt = RIGHT(@dt,LEN(@dt)-@i)
set @i = CHARINDEX( ':', @dt )
if @i = 0 
 return @d
set @year = SUBSTRING( @dt,0,@i )
set @dt = RIGHT(@dt,LEN(@dt)-@i)
set @i = CHARINDEX( ' ', @dt )
if @i = 0 
 return @d
set @time = SUBSTRING( @dt,0,@i )
set @gmt = RIGHT( @dt, len(@dt)-@i )
set @month = REPLACE(@month,'May','5')

set @gtime = CAST( @year+'-'+@month+'-'+@day+' '+@time as datetime )
if SUBSTRING(@gmt,0,1) = '+' 
begin
	set @gmt = (SUBSTRING(@gmt,2,2)+':'+SUBSTRING(@gmt,4,2))
	set @ltime = @gtime - @gmt
end
else
begin
	set @gmt = (SUBSTRING(@gmt,2,2)+':'+SUBSTRING(@gmt,4,2))
	set @ltime = @gtime + @gmt
end 

-- select @year, @month, @day, @time, @gmt, @ltime, @gtime , @gmt 
return @ltime
end

月の表示(May)が、いい加減ですが、まぁ汎用的に作るつもりはないので、これで良しとします。

これをあらかじめ作成しておいて、

update log set ltime = getltime([date]) 

で動かせばよいわけです。

これがどのくらいのスピードで動くのかというと、600 万件のデータを加工するに 3 分程度なんですね。
その、なんといいますか、ADO.NET が如何に遅いか、というのが分かるのです。いや、こんなに違うとは思わなかったのですが…ちょっと、.NET でデータベースを扱うときは、加工の仕方を考え直したほうがいいですね。

という話でした。

カテゴリー: 開発, C# | 5件のコメント