fc2ブログ
 

Technology へようこそ
ここは技術者の「経験」と「ノウハウ」のブログです


2010年06月28日

カーソルループの高速化(2)

前回、current of を使用して高速化を図る方法について書いたのですが、
ここにヒントがあります。
前回のサンプルを見ていただくとわかるのですが、
update文が非常にシンプルな形になっていると思います。

カーソルループ中で実行されるSQL文をいかに簡単な形にするか、
という観点が比較的有効です。
つまり、join を最小限にし、キー項目をwhere節で指定して
やることなんですね。つまり、

insert文は、select句を使わずvalue句を使う。
update、delete文は、current ofを使う。

ということです。
具体的には、

カーソル宣言で、ループ内実行SQL文で使用する項目を定義し、フェッチする。
フェッチした変数を使用してSQL文を実行する。

ということです。

declare cursor curTest
select
a,b,c
from
tableB
join
tableC
on
   ・・・・・・      

open curTest

fetch next from curTest into @a,@b,@c

while (@@fetch_status = 0)
begin

update
tableA
set
c = c * 1
where
a = @a

if @@ROWCOUNT = 0 -- 更新対象が無ければInsert
begin
insert
tableA
values
(@a,@b,@c)
end

fetch next from curTest into @a,@b,@c

end

close curTest
deallocate curTest

これにより、カーソルオープン時のコストは多少かかりますが、
join時のコストが削減されます。
カーソルオープンは初回1回のみですが、joinは処理件数分行われますので、
高速化が可能になるというわけです。
しかし、これでカーソルループを使用しない処理よりも高速化が可能か、というと、
残念ながらそうではありません。

例えば、1万件を処理する場合、処理の内容にもよりますので、一概には言えないのですが、

カーソルループ使用:約160秒
カーソルループ未使用:約10秒

くらいの差がでてしまいます。
結局、繰り返しの処理は単純に1回の処理時間×処理件数になってしまいますので、
1回の処理時間をいかにはやくするか?という視点で高速化を行うわけですが、
やはり限界があるわけで・・・。

できるだけ繰り返しを使用せずに処理を記述することが、性能問題には有効です。

[ posted by H.K ]
スポンサーサイト





2010年06月14日

カーソルループの高速化(1)

前々回、「ループ処理内で連番を振る」テーマについて書いたのですが、
「カーソルループ自体を高速化したい」という要望があったりします。
処理ロジックは変えたくない、でも速くしたい。
問題の本質は、多くの場合、性能試験がシステムテストの一部になっていることでしょう。
システムテストに入る時点で機能テストは終わっているわけですから、
処理ロジックは変えたくない、という意識が働くのはあたりまえのことです。
しかし、性能問題が発現した場合、その多くは「処理ロジックの大幅変更」を
伴うことが多いものです。

今回はカーソルループを残したままでの高速化を考えた場合、
どういう方法があるのか?ということで、「ループ処理を可能な限り排除する」という
本来の高速化の指針から言えば、少々例外的なトピックになってしまいますが・・・。
もちろん、大幅な変更ではないにしろ、ある程度の変更は必要になります。

カーソルループの処理を高速化するのに一番最初に浮かぶのは
カーソル自体を速くできないか?ということです。
T-SQLのカーソルにはいろいろなオプションがありますが、
あまり効果は期待薄。・・・。強いて言うなら、カーソル属性をreadonly にせず、
current of を使って更新するようなロジックを組むこと。
ま、これはカーソル対象テーブルを更新する場合とかにしか使えませんが。

declare cursor curTest
select
a,b,c
from
tableA

open curTest

fetch next from curTest into @a,@b,@c

while (@@fetch_status = 0)
begin

・・・・

update
tableA
set
c = c * 1
where current of cueTest

fetch next from curTest into @a,@b,@c

end

close curTest
deallocate curTest

あとはループのなかで行われているSQL文の最適化。
インデックスを試したり、実行プランを解析してjoin の条件順序の変更や
ヒントを付与したりして調整していくのですが、
多くの場合、なかなか性能は上がらないものです。

では、これ以上、何ができるのか?次回。

[ posted by H.K ]


2010年05月17日

INSERT文に "ORDER BY"

「レガシーシステムに渡すデータに連番を振る」という処理はSQLにとって
苦手な処理のひとつかもしれません。

そもそも、COBOLなどの一般的な事務処理言語はREAD~WRITEの繰り返しによっ
て処理が構成され、READとWRITEの処理の間(項目転送とか)で採番された
連番をアウトプットとして書き出す、というのはさほど難しい処理ではあり
ません。

しかし、同様の処理をSQLを使用して連番を振る、ということは繰り返し
(ループ)が前提となり、本来、SQLの文法を拡張したカーソル処理を行う
必要がある場合が多いものです。

以前からやってみたかったテーマだったのですが、SQLServerでT-SQLを使用
してカーソルを使用することなく、連番が振れないか?

以下のような作業用テーブルを用意します。
create table [dbo].[sWorkTable_SL] (
[TENPO] decimal(3, 0)
, [BUMON] decimal(2, 0)
, [JANCD] decimal(13, 0)
, [YMD] varchar(6)
, [SURYO] decimal(5,1)
, primary key clustered (
[TENPO]
, [BUMON]
)

create table [dbo].[sWorkTable_SL_SEQ] (
[TENPO] decimal(3, 0)
, [BUMON] decimal(2, 0)
, [JANCD] decimal(13, 0)
, [YMD] varchar(6)
, [SURYO] decimal(5,1)
, [SEQNO] int IDENTITY(1,1)
, primary key clustered (
[SEQNO]
)

[dbo].[WorkTable_BASE]から[dbo].[WorkTable_SEQ]にデータを写すとき、
[SEQNO]をIDENTITYとして定義しておき、任意の順番でINSERTすることができ
れば、連番を振れるわけですが・・・。

用意したSQLは

insert into
[dbo].[WorkTable_SL_SEQ]
( [TENPO]
, [BUMON]
, [JANCD]
, [YMD]
, [SURYO]
) select
[TENPO]
, [BUMON]
, [JANCD]
, [YMD]
, [SURYO]
from
[dbo].[WorkTable_SL]
order by
[TENPO]
, [BUMON]

というもの。INSERT-SELECT文にORDER BY句が付くと、ちょっと奇異な感じが
しますが・・・。

実行プランを見てみます。(クリックで拡大)

インデックスが合致している場合

上記のINSERT文では、ORDER BY句が[dbo].[WorkTable_SL]のインデックスと同じ
なので、少々わかりにくいですが、インデックスに掛からない(使用されない)
ような項目(例えば [SURYO]など)に変えてみると、(クリックで拡大)

インデックスが合致しない場合

おわかりですか?sortが現われます。つまり、order by句が効いているということ
になります。
今回の例は単純なので、すべての連番に応用できるものではないことも事実なの
ですが。
もうひとつ気になるのは性能です。ためしに18万件ほどでやってみました。

cursor-loop: 120秒
INSERT-Order by: 20秒

差は歴然です。
[ posted by H.K ]


2010年05月07日

クエリ文字列結合の罠

SQLServer2005以降、nvarchar/varchar 型のサイズとして「max」が指定できるようになりました。
それまでは最大で4000文字(varcharは8000文字)という制限があり、ストアドプロシージャなどで長めの動的クエリを記述する場合、ひとつの変数にはクエリ文字列が収まりきらず、仕方なく複数のクエリ格納用変数を使って用意して、実行時に文字列結合、なんていう技を使っていたりしたものですが、「max」登場により、そういった見苦しいコードを書かなくて済むようになったかと思いきや、コトはそう単純な話ではなかったようです。

そう、たしかに格納先変数のサイズ制限自体は緩くなってはいるのですが、値をセットする際の「リテラル文字列自体の長さ」については従来通りの制限があるのです。
つまり、4000文字(あるいは8000文字)の文字列を一気に変数にセットできない、ということ。
ちなみにマイクロソフトはこの仕様について変更する気はなさげ

もちろん、従来通りリテラル文字列を分割して順次代入していく形にすれば回避はできますが、やはりどう考えても見苦しいうえに面倒臭い。
もう少しスマートに回避する方法はないかと思いきや、
文字列連結のリファレンスにこんな記述を発見。
文字列の連結の結果が8,000バイトを超える場合、結果は切り捨てられます。
ただし、連結する文字列の少なくとも一方が大きな値の型の場合、切り捨ては行われません。
というわけで、試してみたところ、下記「OK Case」のように、単純にmaxサイズの文字型変数を結合してやれば良いことが判明。
これならほとんど違和感なく記述できそうです、というより
これはもはやコーディング規約にしてしまったほうが良いかもしれない世界。
declare @sql nvarchar(max)

set @sql = ''
set @sql = N'start' + replicate(N'X', 4000) + N'end'
print 'NG Case: ' + str(len(@sql))

set @sql = ''
set @sql = @sql + N'start' + replicate(N'X', 4000) + N'end'
print 'OK Case: ' + str(len(@sql))

処理結果)
NG Case: 4000
OK Case: 4008

注意)
print文では4000文字(あるいは8000文字)を超える文字列は常に切り詰めて出力されます。
変数の内容を確認する際にはselect文等を利用しませう。

[ posted by ken ]


2010年02月12日

曜日の名前

VB 2005で作成したWindowsフォームアプリの多言語対応化の作業をしています。
アプリではSQL Server 2005のデータベースからの情報を
Crystal Reports for VB 2005より帳票出力しています。

多言語対応化として幾つか対応事項となった中の一つに曜日の取扱い方がありました。

帳票上に日付項目(表示形式"2010/02/12(金)10:00")があり、
そのまま中国OS環境上で実行した場合は"2010/02/12(星)09:00"となりました。
これは単にSQL Serverのストアドプロシージャで強制的に曜日の名前より
先頭1文字を取っている為、中国OS環境では曜日の名前が
「星期一、星期二、星期三、星期四、星期五、星期六、星期日」となり
いずれの曜日でも先頭1文字で"星"となったというオチです。
そもそも曜日の名前をそのまま表示できる仕様であれば問題なさそうですが
日本では"(金)"などの表示形式は往々にしてありそうなものです。

対応方法としてまずCrystal Reports の帳票デザイン上にて日付項目の式フィールド内に
関数WeekDayName(曜日,省略タイプ)を用いて編集という方法を試みました、
結果は日本OS環境では"金"となりましたが中国OS環境では"星期五"となってしまい断念、
今回はSQL Server側からストアドプロシージャ内にて以下のような対応方法を採用しました。
-- 現在の Microsoft Windows のロケール ID 取得
declare @iLcid int
select @iLcid = [lcid] from [sys].[syslanguages] where [name] = @@LANGUAGE

-- ロケール IDに該当する国毎に曜日の名前を編集
if @iLcid = 1041
-- Japanese
select left(datename(dw, getdate()), 1)
else if @iLcid = 2052
-- Simplified Chinese
select right(datename(dw, getdate()), 1)
else if @iLcid = 1033
-- English
select left(datename(dw, getdate()), 3)
else
-- etc.
select datename(dw, getdate())
[ posted by kami ]


2010年01月29日

tempdb

T-SQLの性能試験。大量データ発生が予想される処理での性能問題は
フェーズが後になればなるほど費用がかかります。当社ではできるだ
け単体テスト時にこの芽を摘むようにしているのですが、数万件を
ターゲットにテストを開始。1分足らずで終了。1件あたり2ミリ秒。
まあまあですか。と安心していたのもつかの間、5万件に件数を上げた
ところで異常終了。あらら、バグですか!

調査してみたものの、それらしいバグは見つからず調査は暗礁に。

時を同じくして名古屋の方も「10万件でテストすると突然異常終了
するんですよ。」と連絡が。

同じ処理をテストしているわけではないんですが、なにか共通性が
あるのでは?ということで環境周りを調査。

すると、tempdbが肥大化してHDDを圧迫しているではないですか。
なんとそのサイズ120GB!

一旦、インスタンスを停止して再起動(tempdbがリセットされる)
再度、処理を実行してエクスプローラを観察していると見る間に
ディスクの空きが無くなっていくではないですか!

うーん、こりゃ困ったなあ・・・。どう対処するか。

名古屋から「ここのSQLが異常に遅いことがわかりました」という
連絡があり、そのSQLの実行プランを見てみると件数から見て明らか
にLOOP JOIN を使うべきところにHASH JOINが使用されているでは
ないですか!

早速、"JOIN" キーワードを "INNER LOOP JOIN" に変更して実行。

1分20秒で何事もなく終了。tempdbのサイズはたったの数百MB!
こんなことってあるんですねえ!?

[ posted by H.K ]


2010年01月05日

桁落ち

SQL Serverで、除算した結果の少数点以下5桁目を
端数処理するという処理をしておりました。

ところが、思ったような結果にならないので調べたところ
原因が判明しました。

計算は、A(money型)÷B(money型)だったのですが、
実はmoney型は少数点以下4桁までしか保証されず、
計算においても同様に4桁までの保証となります。

AとBを、decimalにcastすることで回避できましたが、
どちらか一方でもcastすれば、大丈夫です。

[ posted by S.S ]


2009年12月28日

月末・月初

ある日付の月末を求めます。

ちなみに、なぜこんなことを書いたかと言いますと、
SQL Server2005には、月末を求める関数がない為です。

ということで、考えてみました。

例1)

-- ある日付(yyyy/mm/dd)の月末を求める場合の考え方
-- ①対象となる日付
select getdate()
-- ②月を翌月にする
select dateadd(month, 1, getdate())
-- ③翌月の1日
select left(convert(varchar,dateadd(month, 1, getdate()), 111), 8) + '01'

-- ④月末を求める式(①~③をまとめる)
select dateadd(day, -1, convert(datetime, left(convert(varchar, dateadd(month,1 , getdate()), 111), 8) + '01'))

例2)

-- あるテーブルに年、月、日がそれぞれ別の項目として値が登録されている場合の月末
declare @YR smallint
declare @MT smallint
declare @DT smallint

-- DBから取得できる値
set @YR = 2009
set @MT = 12
set @DT = 24

-- ひとつずつ組み立ててみます
-- 例1)同様、翌月の月初を求めるので「@DT」は使いません。

-- 2009/12/24ではなく2009/12/01を組み立てる
select convert(datetime, convert(varchar, @YR) + '/' + convert(varchar, @MT) + '/01', 111)
-- 翌月の月初、すなわち元旦
select dateadd(month, 1 ,convert(datetime, convert(varchar, @YR) + '/' + convert(varchar, @MT) + '/01', 111))
-- もちろん、元旦の前日は大晦日
select dateadd(day, -1, dateadd(month, 1 ,convert(datetime, convert(varchar, @YR) + '/' + convert(varchar, @MT) + '/01', 111)))

上記の例2)では各項目のデータ型が数値ということもあり、
少々長ったらしい書き方にはなっていますが、理想はもっとスマートに書きたいものです。
(ブログ上、改行されてしまいました・・・)

月の初めは必ず【1日】ですが、文字列連結でわざわざ'01'と
指定しなければいけないのが、一番残念です・・・

[ posted by T.K. ]


2009年12月18日

変更データ キャプチャ

SQL Server 2008では、UPDATEやINSERT、DELETEステートメントによる更新履歴を保管できる
変更データ キャプチャ機能があります。指定した時間の過去のデータを参照したり、
オペレーションミス時のデータ回復などで利用できます。
機能を使用する場合、SQL Server Agentサービスを開始しておく必要があります。

例)
-- データベース作成
create database sample_db;

-- サンプルテーブル、データ作成
create table sample_tbl ( col1 int, col2 varchar(20) );
insert into sample_tbl values (1, 'test'), (2, 'テスト'), (3, 'てすと');

-- サンプルデータベースに対して変更データ キャプチャを有効に設定
use sample_db
go
exec sys.sp_cdc_enable_db;

-- サンプルテーブルに対して変更データ キャプチャを有効に設定
exec sys.sp_cdc_enable_table @source_schema = N'dbo',
@source_name = N'sample_tbl', @role_name = N'cdc_Admin';
サンプルテーブルのデータを編集
-- col1 = 1 の行を更新 ( 'test' → '更新した行' )
update sample_tbl set col2 = '更新した行' where col1 = 1;

-- 新しい行を追加
insert into sample_tbl values (4, '追加した行');

-- col1 = 2 の行を削除 ( 'テスト' )
delete from sample_tbl where col1 = 2;
サンプルテーブルのデータ内容
col1  col2
----- --------------------
1 更新した行
3 てすと
4 追加した行
cdc スキーマにあるdbo_sample_tbl_CT(スキーマ名_テーブル名_CT)を確認
select * from cdc.dbo_sample_tbl_CT;

_$start_lsn _$end_lsn _$seqval _$operation _$update_mask col1 col2
---------------------- --------- ---------------------- ----------- ------------- ---- -----------
0x000000230000007D0004 NULL 0x000000230000007D0002 3 0x02 1 test
0x000000230000007D0004 NULL 0x000000230000007D0002 4 0x02 1 更新した行
0x000000230000007E0003 NULL 0x000000230000007E0002 2 0x03 4 追加した行
0x000000230000007F0003 NULL 0x000000230000007F0002 1 0x03 2 テスト

operation列の内容から更新前データ(3)、更新後データ(4)、追加(2)、削除(1)と確認できます。
テスト時などに利用できそうな機能です。

[ posted by kami ]


2009年10月30日

透過的なデータ暗号化

SQL Server 2008の透過的なデータ暗号化(TDE)は、データベースに対して暗号化を設定するだけでデータベース内のすべてのオブジェクトを暗号化できます。
アプリケーションからは透過的に利用でき、SQL Server 2005と比べアプリケーションの修正いらずと便利です。
また、バックアップファイルも自動的に暗号化(データファイル全体を暗号化)されます。

設定例)
-- サンプルデータベース(sample_db)を作成
use master
go
create database sample_db
on primary ( name = 'sample_db', filename = 'd:\sample_db.mdf' )
go

-- マスターキーを作成
-- ※パスワードは強固なものを設定
create master key encryption by password = 'master key password string'
go

-- サーバー証明書を作成
create certificate SampleServerCert with subject = 'Sample Server Certificate'
go

-- サーバー証明書のバックアップ
backup certificate SampleServerCert to file = 'd:\SampleServerCert'
with private key ( file = 'd:\SamplePrivateKey',
encryption by password = 'server certificate password string' )
go

-- データベース暗号化キー作成
-- ※暗号化アルゴリズムには、AES_128、AES_192、AES_256、3DESのいずれかを選択できます
create database encryption key with algorithm = aes_128 encryption
by server certificate SampleServerCert
go

-- データベースに対して透過的なデータ暗号化を有効化に設定
alter database sample_db set encryption on
別マシンへデータを移動する場合
-- データベースのバックアップ作成
use sample_db
go
backup database sample_db to disk = 'd:\sample_db.bak'
go

-- 別マシンのSQL Server 2008にてマスターキーを作成
create master key encryption by password = 'master key password string'
go

-- サーバー証明書バックアップをリストア
-- 別マシン上へサーバー証明書バックアップファイル
-- (SampleServerCert, SamplePrivateKey)をコピーしたものを適用
create certificate SampleServerCert
from file = 'd:\SampleServerCert'
with private key ( file = 'd:\SamplePrivateKey',
decryption by password = 'server certificate password string' )
go

-- データベースをリストア
-- 別マシン上へデータベースバックアップファイル(sample_db.bak)をコピーしたものを適用
restore database sample_db from disk = 'd:\sample_db.bak'
go
備えあれば憂いなしと言います。
セキュリティー関連技術も覚えておくと今後に役立つかもしれません。

[ posted by kami ]