連番の空番を取得する方法
-
解説
今回は備忘録的なネタです。
連番の空番を調べたいってのは結構ある話ですが、わざわざVBAで探すのは面倒です。
(面倒というほどのコーディング量でもありませんが)今回は簡単なSQLでとる方法をいくつか紹介します。
INやEXISTSを使った方法がよく紹介されていますが、多くはのサンプルは先頭が欠番しているとそこが調べられなくなっています。
ここでは、先頭が欠番していても調べる方法を紹介します。なお、銀の匙さん、YU-TANGさんの強力な協力によりモジュールを使った二分探索による高速な検索方法も一緒に紹介します。
-
サンプル
- IN句使った方法
件数が増えると遅い。って言うか使えないSELECT MIN(連番+1) AS 欠番 FROM ( SELECT {[TOP 1] Or [DISTINCT]} ← これをつけるとちょっと早くなる TOPのほうが若干早い 0 AS 連番 ← 固定値は最小値-1とする(1が最小なら0、0が最小なら-1とする) FROM [テーブル名] UNION SELECT 連番 FROM [テーブル名] ) AS [適当な名前] WHERE 連番+1 NOT IN ( SELECT 連番 FROM [テーブル名] )
- EXISTS句使った方法
サンプル 1よりは速いSELECT MIN(連番+1) AS 欠番 FROM ( SELECT {[TOP 1] Or [DISTINCT]} ← これをつけるとちょっと早くなる TOPのほうが若干早い 0 AS 連番 ← 固定値は最小値-1とする(1が最小なら0、0が最小なら-1とする) FROM [テーブル名] UNION SELECT 連番 FROM [テーブル名] ) AS [適当な名前] WHERE NOT EXISTS ( SELECT 0 FROM [テーブル名] WHERE 連番=[適当な名前].連番+1 )
- IIfとLEFT JOINを使った方法(銀の匙さん提供)
これが今のところSQLでは最速SELECT IIf(先頭連番 > 1, 1, 欠番候補 + 1) AS 欠番 FROM ( SELECT MIN(T1.連番) AS 欠番候補, ( SELECT MIN(連番) FROM [テーブル名] ) AS 先頭連番 FROM [テーブル名] AS T1 LEFT JOIN [テーブル名] AS T2 ON T1.連番 + 1 = T2.連番 WHERE T2.連番 Is Null ) AS [適当な名前]
- IIfとEXISTSを使った方法(銀の匙さん提供)
サンプル 2よりはるかに速いSELECT IIf(先頭連番 > 1, 1, 欠番候補 + 1) AS 欠番 FROM ( SELECT MIN(連番) AS 欠番候補, ( SELECT MIN(連番) FROM [テーブル名] ) AS 先頭連番 FROM [テーブル名] AS T1 WHERE NOT EXISTS ( SELECT 1 FROM [テーブル名] AS T2 WHERE T2.連番 = T1.連番 + 1 ) ) AS [適当な名前]
- モジュールを使った方法 全件サーチ
速度はデータ件数に比例するがテストで行った300万件ではSQLとは比較にならないくらい速いPublic Function 欠番検索_全件サーチ() As Long Dim R_REC As Recordset 欠番検索_全件サーチ = 1 Set R_REC = CurrentDb.OpenRecordset _ ("Sample", dbOpenTable, dbReadOnly, dbReadOnly) R_REC.Index = "PrimaryKey" Do Until R_REC.EOF If R_REC![ID] <> 欠番検索_全件サーチ Then Exit Do End If 欠番検索_全件サーチ = 欠番検索_全件サーチ + 1 R_REC.MoveNext Loop R_REC.Close End Function
- モジュールを使った方法 二分探索 Find系メソッド版
Public Function 欠番検索_二分探索_Find系メソッド() As Long Dim R_REC As Recordset Dim StartValue As Long Dim EndValue As Long Dim FindValue As Long Set R_REC = CurrentDb.OpenRecordset _ ("SELECT ID FROM Sample ORDER BY ID", dbOpenDynaset, _ dbReadOnly, dbReadOnly) R_REC.MoveLast StartValue = 1 EndValue = R_REC![ID] FindValue = EndValue \ 2 If R_REC.RecordCount = EndValue Then 欠番検索_二分探索_Find系メソッド = EndValue + 1 Exit Function End If R_REC.FindFirst "ID=" & FindValue Do If R_REC.NoMatch = True Then If StartValue = FindValue Then 欠番検索_二分探索_Find系メソッド = FindValue Exit Do Else EndValue = FindValue FindValue = StartValue + (EndValue - StartValue) \ 2 R_REC.FindFirst "ID=" & FindValue End If ElseIf R_REC.AbsolutePosition + 1 = FindValue Then StartValue = FindValue + 1 FindValue = FindValue + (EndValue - FindValue) \ 2 + 1 R_REC.FindNext "ID=" & FindValue Else EndValue = FindValue FindValue = StartValue + (EndValue - StartValue) \ 2 R_REC.FindPrevious "ID=" & FindValue End If Loop R_REC.Close End Function
- モジュールを使った方法 二分探索 Move系メソッド版
Public Function 欠番検索_二分探索_Move系メソッド() As Long Dim R_REC As Recordset Dim StartValue As Long Dim EndValue As Long Dim FindValue As Long Dim CurrentPosition As Long Set R_REC = CurrentDb.OpenRecordset _ ("Sample", dbOpenTable, dbReadOnly, dbReadOnly) R_REC.Index = "PrimaryKey" R_REC.MoveLast StartValue = 1 EndValue = R_REC![ID] FindValue = EndValue \ 2 If R_REC.RecordCount = EndValue Then 欠番検索_二分探索_Move系メソッド = EndValue + 1 Exit Function End If R_REC.MoveFirst R_REC.Move FindValue - 1 CurrentPosition = FindValue Do If R_REC.EOF = True Then R_REC.MoveFirst EndValue = CurrentPosition FindValue = CurrentPosition - (EndValue - StartValue) \ 2 - 1 R_REC.Move FindValue - 1 CurrentPosition = FindValue ElseIf R_REC![ID] <> CurrentPosition Then If StartValue = CurrentPosition Then 欠番検索_二分探索_Move系メソッド = CurrentPosition Exit Do Else EndValue = CurrentPosition FindValue = -(EndValue - StartValue) \ 2 - 1 R_REC.Move FindValue CurrentPosition = CurrentPosition + FindValue End If Else StartValue = CurrentPosition + 1 FindValue = (EndValue - CurrentPosition) \ 2 + 1 R_REC.Move FindValue CurrentPosition = CurrentPosition + FindValue End If Loop R_REC.Close End Function
- モジュールを使った方法 二分探索 SQL版(銀の匙さんカスタマイズ)
Public Function 欠番検索_二分探索_SQL() As Long Dim DB As Database Dim Q_OBJ As QueryDef Dim R_REC As Recordset Dim StartValue As Long Dim EndValue As Long Dim FindValue As Long Set DB = CurrentDb Set R_REC = DB.OpenRecordset _ ("SELECT " & _ "MIN(ID) AS 最小値, MAX(ID) AS 最大値, COUNT(*) AS 件数 " & _ "FROM Sample", dbOpenForwardOnly, dbReadOnly, dbReadOnly) StartValue = 1 If R_REC![件数] = R_REC![最大値] Then 欠番検索_二分探索_SQL = R_REC![最大値] + 1 Exit Function ElseIf R_REC![最小値] <> StartValue Then 欠番検索_二分探索_SQL = StartValue Exit Function End If EndValue = R_REC![最大値] FindValue = EndValue \ 2 R_REC.Close Set Q_OBJ = DB.CreateQueryDef _ ("", _ "PARAMETERS START_VALUE LONG, END_VALUE LONG;" & _ "SELECT " & _ "MIN(ID) AS 最小値, MAX(ID) AS 最大値, " & _ "COUNT(*) AS 件数 " & _ "FROM Sample " & _ "WHERE ID BETWEEN [START_VALUE] AND [END_VALUE]") Do Q_OBJ.Parameters![START_VALUE] = StartValue Q_OBJ.Parameters![END_VALUE] = FindValue Set R_REC = Q_OBJ.OpenRecordset _ (dbOpenForwardOnly, dbReadOnly, dbReadOnly) If R_REC![件数] = 0 Then 欠番検索_二分探索_SQL = StartValue Exit Do ElseIf R_REC![最小値] <> StartValue Then 欠番検索_二分探索_SQL = StartValue Exit Do ElseIf R_REC![最小値] = StartValue And _ R_REC![最小値] + 1 = R_REC![最大値] And _ R_REC![件数] < (FindValue - StartValue + 1) Then 欠番検索_二分探索_SQL = R_REC![最大値] + 1 Exit Do ElseIf R_REC![最小値] + 1 < R_REC![最大値] And R_REC![件数] = 2 Then 欠番検索_二分探索_SQL = R_REC![最小値] + 1 Exit Do ElseIf R_REC![最小値] = StartValue And _ (R_REC![最大値] - R_REC![最小値] + 1) = R_REC![件数] And _ R_REC![件数] < (FindValue - StartValue + 1) Then 欠番検索_二分探索_SQL = R_REC![最大値] + 1 Exit Do End If If R_REC![最小値] = StartValue And R_REC![最大値] = FindValue And _ R_REC![最大値] - R_REC![最小値] + 1 = R_REC![件数] Then StartValue = FindValue + 1 FindValue = FindValue + (EndValue - FindValue) \ 2 + 1 Else EndValue = FindValue FindValue = StartValue + (EndValue - StartValue) \ 2 End If R_REC.Close Loop R_REC.Close DB.Close End Function
- モジュールを使った方法 二分探索 レコードポジション(YU-TANGさん提供)
Public Function 欠番検索_二分探索_位置() As Long Dim 未走査開始位置 As Long Dim 未走査終了位置 As Long Dim 未走査区間 As Long Dim 欠番あり As Boolean Dim i As Long With CurrentDb.OpenRecordset("select ID from Sample order by ID") If Not .EOF Then .MoveLast 未走査終了位置 = .AbsolutePosition ' ループで走査 Do While True i = .AbsolutePosition 欠番あり = .Collect(0) - i <> 1 If 欠番あり Then 未走査終了位置 = i - 1 Else 未走査開始位置 = i + 1 End If 未走査区間 = 未走査終了位置 - 未走査開始位置 + 1 ' 未走査区間がある場合 If 未走査区間 Then .AbsolutePosition = 未走査開始位置 + (未走査区間 \ 2) ' 未走査区間がない場合 Else Exit Do ' 走査終了 End If Loop End If 欠番検索_二分探索_位置 = 未走査開始位置 + 1 .Close End With End Function
- IN句使った方法
-
計測
計測結果
確認データ件数:300万件
テーブル構造:ID(長整数), データ(テキスト(50)
キー:IDなお、サンプル 1(IN句を使った方法)はお話にならないくらい遅いので計測していません。
また、サンプル 2(EXISTS句を使った方法)はTOP 1を使った結果を載せていますそれぞれ3回計測した平均値を載せています。
欠番 探索回数(上段)・計測時間(下段 単位:ミリ秒 ※小数点以下切捨て) 数字はサンプルの番号 2 3 4 5 6 7 8 9 1 1 1 1 1 22 21 1 23 50,890 8,494 22,489 0 864 406 3,964 640 450,000 1 1 1 450,000 23 25 20 23 51,765 8,458 22,567 1,171 890 390 8,364 734 1,500,000 1 1 1 1,500,000 21 21 1 22 51,953 8,463 22,573 3,906 885 401 5,609 1,609 2,500,000 1 1 1 2,500,000 22 21 21 23 51,234 8,442 22,614 6,568 948 411 6,609 2,750 なし(3,000,001) 1 1 1 3,000,001 1 1 1 1 51,922 8,494 22,552 7,822 536 0 2,677 745 処理平均 51,552 8,470 22,559 3,893 824 321 5,444 1,295 SQLの結果はほぼ同じ(誤差の範囲内だと思う)です。
モジュールの全件サーチは件数が増えれば増えるほど欠番が後ろになればなるほど遅くなっていきます。
全件調べますから当然ですね。二文探索法は今回の結果ではMove系メソッド版が最速となっています。
この速度だと実運用にも耐えられるのではないでしょうか?Find系メソッド版もレコードポジション版もdbOpenDynasetを使っているのでキャッシュの効き具合などが影響し結果は一定しません。計測方法によっては逆転する可能性もあります。
-
応用
これらを応用すると他にキーがあっても調べられます。
SELECT IIf(先頭連番 > 1, 1, 欠番候補 + 1) AS 欠番 FROM ( SELECT MIN(T1.連番) AS 欠番候補, ( SELECT MIN(連番) FROM [テーブル名] WHERE 日付=#2009/05/29# ) AS 先頭連番 FROM [テーブル名] AS T1 LEFT JOIN [テーブル名] AS T2 ON (T1.日付 = T2.日付 AND T1.連番 + 1 = T2.連番) WHERE T1.日付=#2009/05/29# AND T2.連番 Is Null ) AS [適当な名前]
モジュール版でやるにはFind系メソッド版とSQL版なら比較的楽にできると思います。
Move系メソッド版とレコードポジション版はかなり面倒なことになるでしょう。 -
その他
ほかにも方法はあると思います。こんなのもあるというのがありましたらぜひお知らせください。
通常、空番は欠番として扱うのがコンピュータシステムの基本です。取り扱いには十分ご注意ください。
-
謝辞
SQLサンプルと二分探索のアイディアを下さった銀の匙さん、二分探索関数を下さったYU-TANGさんには心からお礼申し上げます。ありがとうございます。
スポンサーリンク
連番の空番を取得する方法のサンプル
スポンサーリンク
スポンサーリンク
説明がわからないなどありましたらお問い合わせでお知らせください。
ここに掲載された情報を使用したことによって発生した、いかなる損害に対しても
管理者である雅は一切責任を負いません。