SQLにおいて、一意であることを保証しなければならない列については、教科書的には一意制約をつけろということで終わりかと思いますが、現実には一意である必要はあるのに一意制約がついていないケースはよく見かけます。著名ソフトの例としては、WordPress、Joomla!、MovableType等のユーザIDには、DBの一意制約はつけられていますせん。
セキュリティの観点からは、「一意制約つけましょうね」で終わらせてもいいところですが、なんらかの理由で一意制約をつけられない(つけたくない)状況で、特定列の一意性を保証する方法のベスト・プラクティスは知られているのでしょうか?
通常の排他制御では、行ロック等を使用して列値の重複を防止するわけですが、これからINSERTしようという値の場合は、まだ値がないので、行ロックはかけられないと思います。
ということで、一意制約を使わないで一意性を保証するINSERT方法についてお伺いします。
すみません、意図が通じにくいようですので追記します。
例えば、列 userid にログインIDを保存するとして、これは一意である必要があります。ところが、なんらかの理由(私がヒアリングした事例だと論理削除をする都合等)で一意制約をつけられない場合、SQLの工夫で userid に重複したIDが登録されることを防ぐ(たとえば、userid として tanaka が二重に登録されることを防ぐ) にはどうすればよいかという質門です。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答6件
0
ベストアンサー
トランザクションを張ろうというところまではyambejp様と同意見ですが、さらに分離レベルをSERIALIZABLEにしましょうというところを強調しておきたいです。
無論、デッドロックまたは直列化エラーによる失敗がありますのでリトライ処理も記述することが必須です。
分離レベルをSERIALIZABLEにする必要性については先日記事にしました。
https://qiita.com/yuba/items/89496dda291edb2e558c
この記事はPostgreSQLの振る舞いを扱っていますが、トランザクションによる一意性保証をするにはRDB製品の個性と無関係に直列化可能性が必要です。
投稿2018/01/09 16:59
総合スコア5570
0
一意制約を使わないで一意性を保証するINSERT方法についてお伺いします。
一意にするための条件があれば、その条件でデータが存在するかどうかのチェックを行えば可能かと思います。
SQL
1insert into テーブル select 値・・・ where not exists(select 1 from テーブル where 一意条件)
追記
なんらかの理由(私がヒアリングした事例だと論理削除をする都合等)で一意制約をつけられない場合
例示されているものでは、一意制約が付けられないとは思えませんが、一意制約は付けないとしても、
一意にするための条件についてはパフォーマンスを考慮してインデックスを作成するケースはあるかと思います。
仮に制約条件にNull値が含まれているとしても、Nullを置き換えるようなファンクションインデックスにすれば良いかと思います。
結局のところインデックスを作成することも考え、一意制約を行うことについて再考されてはどうでしょうか。
投稿2018/01/09 12:08
編集2018/01/09 14:13総合スコア25430
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
RDB的には一意制約なしに運用するなんてノーロープバンジーです。
一意制約を付けた状態でいくらでも回避策があります。
と言うかそれを設計するのがプロの仕事です。
1.論理削除はせず物理的に削除済みテーブルにデータを移す。←お勧め
2.NULLにして別列にIDを移す(SQL Server では不可)
一意制約を付けない場合は、
1.フロントエンドで対応するのでマスターメンテスタッフを増員する。
2.SQLで対応するので、とっても遅くなってるけど我慢する。
3.ストアドやトリガで対応するので後でテンヤワンヤになるけど諦める。
などの方法があります。
投稿2018/01/09 07:40
総合スコア4152
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
プロシージャを利用してトランザクションで処理してはどうでしょうか?
※MySQLを利用する前提
元データ作成
SQL
1create table tbl(id int,data varchar(10)); 2insert into tbl values(1,'aaa'),(2,'bbb'),(3,'ccc');
プロシージャの作成
SQL
1drop procedure if exists add_tbl; 2delimiter // 3create procedure add_tbl(in in_id int,in_data varchar(10)) 4begin 5start transaction; 6insert into tbl values(in_id,in_data); 7set @a:=(select count(*) from tbl where id=in_id); 8if @a>=2 then 9rollback; 10else 11commit; 12end if; 13end 14//
実行
- 競合するデータを投入
※投入されない
SQL
1call add_tbl(1,'ddd'); 2select * from tbl;
- 競合しないデータを投入
※投入される
SQL
1call add_tbl(4,'eee'); 2select * from tbl;
transaction同士の競合テスト
SQL
1drop procedure if exists add_tbl; 2delimiter // 3create procedure add_tbl(in in_id int,in_data varchar(10)) 4begin 5start transaction; 6insert into tbl values(in_id,in_data); 7select sleep(5); 8set @a:=(select count(*) from tbl where id=in_id); 9if @a>=2 then 10rollback; 11else 12commit; 13end if; 14end 15//
insert後に5秒待たせる処理をしてクロスした処理が競合するかチェックします
この処理は非同期ではないので、コンソールを2つひらいて投入します。
コンソールで
SQL
1call add_tbl(5,'xxx');
別コンソールで
SQL
1call add_tbl(5,'yyy');
上記の実際の処理順は
- (5,'xxx')を投入(その後wait)
- (5,'yyy')を投入(その後wait)
- id=5をチェックして、競合がないのでcommit
- id=5をチェックして、競合があるのでないのでrollback
結果としてデータを確認すれば
5,xxxのみ投入されているのがわかります。
投稿2018/01/09 07:17
編集2018/01/09 08:35総合スコア117702
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/01/09 08:35
2018/01/09 08:58

0
.Netでキーを無指定でupdateクエリを自動生成させると、Where句に更新前のカラムをすべて指定して強引に一意性を作っていたのを思い出しました。(Nullの場合もうまく処理してた記憶があります。)
実行速度に難があるかもしれませんが、論理削除には効果がありそうです。
訊かれているのはinsertでしたね・・・
失礼しました。
テーブルロックして、対象レコードがないことを確認してinsertするか。
insertしてから、select で行数数えてだめならrollbackあたりくらいしか思いつきません・・・
投稿2018/01/10 06:47
編集2018/01/10 07:07総合スコア2884
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
投稿2018/01/09 06:33
総合スコア16417
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/01/09 21:42
2018/01/09 23:30
2018/01/10 00:46