重複レコードの削除で失敗しやすいのは、消す処理そのものより、「どのレコードを残すか」を決めないまま実行することです。削除クエリで消したレコードは、[元に戻す]で戻せません。
この記事では、次の流れをVBAで1つの手順にまとめ、フォームのボタンから毎回同じ手順で実行できるようにします。
- キー列と、残す1件の基準を決める
- 削除対象の件数を数え、メッセージで確認する
- 削除対象を別テーブルに控えとして保存する
CurrentDb.Executeで削除し、削除件数が確認時の件数と一致するかを照合する- 削除件数と残りの件数を表示する
例として、次のテーブルを使います。
- テーブル名:T_顧客
- 列:ID(オートナンバー型・主キー)、顧客コード、顧客名、更新日
- 重複の判定:顧客コードが同じレコード
実行前に確認すること
- コピーで試す:最初は、accdbファイルをコピーしたもので実行します。
- 1件を特定できる列があるか:同じ顧客コードの中から1件を選ぶには、IDのように値が重ならない列が必要です。ない場合は、テーブルデザインでオートナンバー型の列を追加します。追加すると、既存のレコードにも番号が振られます。
- リレーションシップ:T_顧客 に関連する子テーブルがあり、「レコードの連鎖削除」を設定していると、子テーブルのレコードも一緒に消えます。参照整合性だけを設定している場合は、関連レコードのある顧客の削除がエラーになります。データベースツールの「リレーションシップ」で設定を確認します。
- ほかの人が使っていないか:共有しているデータベースなら、ほかの人がデータを編集していない時間に実行します。
- DAOの参照設定:コード中の
DAO.Databaseで「ユーザー定義型は定義されていません」と出る場合は、VBEの「ツール」→「参照設定」で、Access database engine の Object Library にチェックが入っているかを確認します。
残す1件の基準を決める
| 残す基準 | 向いている場面 | 注意 |
|---|---|---|
| IDが最小(最初に登録した1件) | 同じデータを誤って二重に取り込んだ | あとから修正した内容が別のレコードにある場合、そちらが消える |
| 更新日が最新 | 更新のたびに同じ顧客のレコードが追加されている | 同じ更新日のレコードが複数あるときの決め方が必要。更新日が空のレコードは別に扱う |
基準を決める前に、重複しているレコードを並べて中身を見ます。新しいクエリをSQLビューで開き、次のSQLを実行します。
SELECT t.*
FROM T_顧客 AS t
WHERE t.顧客コード IN (
SELECT 顧客コード FROM T_顧客
GROUP BY 顧客コード
HAVING COUNT(*) > 1
)
ORDER BY t.顧客コード, t.ID;
同じ顧客コードでも顧客名や住所が違うレコードがあれば、どれを残すかで結果が変わります。自動で決めてよいものか、先に人が直すべきものかを、ここで判断します。
削除条件を1つの式にする
IDが最小の1件を残す場合、削除するのは「同じ顧客コードで、自分より小さいIDのレコードがあるもの」です。
EXISTS (
SELECT * FROM T_顧客 AS t2
WHERE t2.顧客コード = t.顧客コード
AND t2.ID < t.ID
)
各顧客コードでIDが最小のレコードは、この条件に当てはまらないため残ります。
この条件式を、件数の確認、控えの保存、削除の3か所で共通に使います。条件を1か所で定義しておくと、確認した対象と実際に消す対象がずれることを防げます。
VBAのコード
標準モジュールに次のコードを貼り付けます。テーブル名・列名・残す基準の説明は、先頭の定数で変更します。
Option Compare Database
Option Explicit
' 対象のテーブルと列(環境に合わせて変更)
Private Const TBL_NAME As String = "T_顧客"
Private Const KEY_COL As String = "顧客コード"
Private Const ID_COL As String = "ID"
Private Const KEEP_RULE As String = "IDが最も小さい1件"
' 削除対象の条件:同じキーで、より小さいIDのレコードがあるもの
Private Function DuplicateCondition() As String
DuplicateCondition = "EXISTS (SELECT * FROM [" & TBL_NAME & "] AS t2" & _
" WHERE t2.[" & KEY_COL & "] = t.[" & KEY_COL & "]" & _
" AND t2.[" & ID_COL & "] < t.[" & ID_COL & "])"
End Function
Public Sub DeleteDuplicateRecords()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim cond As String
Dim totalBefore As Long
Dim targetCount As Long
Dim backupName As String
Dim deletedCount As Long
Dim inTrans As Boolean
On Error GoTo ErrHandler
Set db = CurrentDb
cond = DuplicateCondition()
' 1. 削除対象を数える
totalBefore = DCount("*", TBL_NAME)
Set rs = db.OpenRecordset( _
"SELECT COUNT(*) AS cnt FROM [" & TBL_NAME & "] AS t WHERE " & cond, _
dbOpenSnapshot)
targetCount = rs!cnt
rs.Close
If targetCount = 0 Then
MsgBox "削除対象の重複レコードはありません。", vbInformation
Exit Sub
End If
' 2. 件数を見せて確認する(既定のボタンは[キャンセル])
If MsgBox("全 " & totalBefore & " 件のうち " & targetCount & " 件を削除します。" & vbCrLf & _
"「" & KEY_COL & "」ごとに" & KEEP_RULE & "を残します。" & vbCrLf & vbCrLf & _
"実行しますか?", _
vbOKCancel + vbExclamation + vbDefaultButton2, "重複削除の確認") <> vbOK Then
MsgBox "処理を中止しました。", vbInformation
Exit Sub
End If
' 3. 削除対象を別テーブルに控えとして保存する
backupName = TBL_NAME & "_削除分_" & Format(Now, "yyyymmdd_hhnnss")
db.Execute "SELECT t.* INTO [" & backupName & "] FROM [" & TBL_NAME & "] AS t WHERE " & cond, _
dbFailOnError
Application.RefreshDatabaseWindow
If DCount("*", backupName) <> targetCount Then
MsgBox "控えに保存した件数が確認時と一致しないため、削除せずに終了します。", vbCritical
Exit Sub
End If
' 4. トランザクション内で削除し、件数が合わなければ取り消す
DBEngine.BeginTrans
inTrans = True
db.Execute "DELETE t.* FROM [" & TBL_NAME & "] AS t WHERE " & cond, dbFailOnError
deletedCount = db.RecordsAffected
If deletedCount <> targetCount Then
DBEngine.Rollback
inTrans = False
MsgBox "削除件数(" & deletedCount & " 件)が確認時(" & targetCount & " 件)と一致しないため、取り消しました。", vbCritical
Exit Sub
End If
DBEngine.CommitTrans
inTrans = False
' 5. 結果を表示する
MsgBox deletedCount & " 件を削除しました。" & vbCrLf & _
"残り:" & DCount("*", TBL_NAME) & " 件" & vbCrLf & _
"削除したレコードの控え:" & backupName, vbInformation
Exit Sub
ErrHandler:
If inTrans Then DBEngine.Rollback
MsgBox "エラーが発生したため処理を中止しました。" & vbCrLf & _
Err.Number & ":" & Err.Description, vbCritical
End Sub
コードのポイントは次のとおりです。
CurrentDbを変数dbに入れている:CurrentDbは呼び出すたびに新しいオブジェクトを返します。CurrentDb.ExecuteのあとにCurrentDb.RecordsAffectedを読んでも、削除件数は取れません。実行と件数の取得は、同じ変数で行います。dbFailOnErrorを付けている:これがないと、一部のレコードが削除できなくても、エラーにならずに処理が進むことがあります。- 控えのテーブル名に日時を付けている:実行のたびに別のテーブルとして保存されるため、前回の控えを上書きしません。
- トランザクションで囲んでいる:削除件数が確認時と違えば、
Rollbackで削除を取り消します。リンクテーブルなど接続先によっては扱いが異なる場合があるため、コピーで動作を確認してから使います。 - 確認メッセージの既定のボタンを[キャンセル]にしている:Enterキーを押しただけでは実行されません。
db.Execute は、削除クエリを手動で実行したときのような確認メッセージを表示しません。DoCmd.SetWarnings で警告を止める必要もありません。
更新日が最新の1件を残す場合
DuplicateCondition と KEEP_RULE を次のように変えます。更新日が同じレコードが複数あるときは、IDが大きいほうを残します。
Private Const KEEP_RULE As String = "更新日が最も新しい1件(同じ日ならIDが大きいもの)"
Private Function DuplicateCondition() As String
DuplicateCondition = "EXISTS (SELECT * FROM [" & TBL_NAME & "] AS t2" & _
" WHERE t2.[" & KEY_COL & "] = t.[" & KEY_COL & "]" & _
" AND (t2.[更新日] > t.[更新日]" & _
" OR (t2.[更新日] = t.[更新日] AND t2.[" & ID_COL & "] > t.[" & ID_COL & "])))"
End Function
空の値(Null)の扱い
- 顧客コードが空のレコードは、
=での比較が成立しないため、この条件では削除されません。 - 更新日が空のレコードは、日付の比較が成立しないため、残ります。そのため、同じ顧客コードで2件残ることがあります。
実行前に、空の値が何件あるかを確認しておきます。
SELECT COUNT(*) FROM T_顧客 WHERE 顧客コード Is Null OR 更新日 Is Null;
空のレコードがある場合は、それを削除対象にするかどうかを別に判断します。
キーが複数の列の場合
「店舗コード」と「顧客コード」の組み合わせで重複を判定する場合は、条件に AND t2.[店舗コード] = t.[店舗コード] を加えます。
フォームのボタンから実行する
- フォームをデザインビューで開き、ボタンを配置する(コマンドボタンウィザードが開いたら[キャンセル])
- プロパティシートの「その他」タブで、ボタンの名前を
cmdDeleteDupにする - 「イベント」タブの「クリック時」で[イベント プロシージャ]を選び、[…]を押してコードを開く
- 次のコードを書く
Private Sub cmdDeleteDup_Click()
If Me.Dirty Then Me.Dirty = False ' 編集中のレコードを保存してから実行
DeleteDuplicateRecords
Me.Requery ' フォームの表示を最新にする
End Sub
Me.Dirty と Me.Requery の行は、フォームに T_顧客 のレコードを表示している場合のためのものです。レコードを表示していないフォームなら削除して構いません。
実行後に確認すること
- 重複が残っていないか:次のSQLで、結果が0件になるかを確認します。顧客コードが空のレコードが複数あると、空のキーの行が1行表示されます。
SELECT 顧客コード, COUNT(*) AS 件数
FROM T_顧客
GROUP BY 顧客コード
HAVING COUNT(*) > 1;
- 件数が合っているか:実行前の件数 − 削除件数 = 残りの件数になっているか。
- 控えの中身:控えのテーブルを開き、想定したレコードが削除されたかを目で確認します。
- 戻す必要がある場合:控えのテーブルから追加クエリで戻します。IDも含めて戻せるかは、同じIDのレコードが残っていないかによります。
INSERT INTO T_顧客
SELECT * FROM [T_顧客_削除分_20261006_153000];
- 控えの片付け:控えのテーブルは実行のたびに増えます。確認が済んだら削除するか、保管期間を決めておきます。レコードやテーブルを削除しても、ファイルサイズは「データベースの最適化/修復」を実行するまで小さくなりません。
うまくいかないときの確認
| 症状 | 確認すること |
|---|---|
| 削除件数が0と表示されるのに、レコードは消えている | CurrentDb.RecordsAffected を直接読んでいないか。Execute と同じ変数で読む |
| 件数が一致せず取り消された | 実行中にほかの人がデータを変更していないか。連鎖削除の設定がないか |
| 参照整合性のエラーで止まった | 子テーブルに関連レコードがある。子レコードの付け替えなど、扱いを先に決める |
| 重複のはずのレコードが残る | キーの値に前後の空白などの違いがないか、空の値でないかを、確認用のクエリで見る |
| 実行に時間がかかる | キー列にインデックスを設定しているか |
| リンクテーブルで動かない | 接続先のデータベースによって、SQLの書き方やトランザクションの扱いが異なる場合がある。接続先の仕様を確認する |
