はじめに
DB運用で INSERT や UPDATE を行うとき、
いちばん怖いのは「本当にこのSQLを流して大丈夫か?」という不安だと思います。
- WHERE 条件を一つ書き間違えただけで全件更新
- INSERT が混ざって、結果が見づらくなる
- Excelから直接DBを更新する設計にはしたくない
こうした経験から、今回私は次の方針でツールを作りました。
Excelは「更新用SQLを作るだけ」
実テーブルは決して更新しない
この記事では、そのツールの仕組みと便利さを簡単に紹介し、
最後にソースコード全文を掲載します。
このツールのコンセプト
このExcelツールは「DB更新ツール」ではありません。
❌ やらないこと
- ExcelからDBを直接更新しない
- UPDATE / INSERT を自動実行しない
- COMMIT を流さない
✅ やること
- ExcelをUIとして使う
- INSERT / UPDATE 用のSQLを自動生成する
- ROLLBACK前提の検証用SQLを生成する
流れとしては、非常にシンプルです。
Excelで編集
↓
INSERT / UPDATE 用SQLを生成
↓
SSMSなどでSQLを実行して確認
↓
問題なければ COMMIT(人が判断)
「便利さ」と「安全性」を両立するための設計です。
このツールでは、A列を「更新区分」として使っています。
| 値 | 意味 |
|---|---|
| I | INSERT |
| U | UPDATE |
| 空白 | 変更なし |
ただし、ここが重要なポイントです。
I / U は手入力しません
Worksheet_Change による自動判定
セルを編集すると Worksheet_Change が発火し、
- ORIGINAL(取得時点のデータ)に存在しない行
→ INSERT(I) - ORIGINAL と値が変わった行
→ UPDATE(U) - 元に戻した行
→ 自動的に解除
という判定を 完全に自動で行います。
そのため、
- I / U の付け忘れ
- U を消し忘れる
- 意図しないSQL生成
といった事故を防げます。
見た目にも分かりやすく
Excel上では次のように視覚的に分かるようにしています。
- INSERT 行:薄いグリーン
- UPDATE 行:薄いオレンジ
- 変更されたセル:赤文字+太字
Excelを開いた瞬間に、
「どの行がINSERT対象で、どの行がUPDATE対象か」
が一目で分かります。
ROLLBACK前提で「更新前・更新後・差分」を確認できる
このツール最大のポイントがここです。
生成されるSQLは、単なる UPDATE / INSERT 文ではなく、
必ず ROLLBACK 前提の検証用SQLになります。
構成としては、次のような流れです。
BEGIN TRANSACTION;
-- 更新前の状態
SELECT * INTO #Before FROM テーブル名;
-- INSERT / UPDATE(Excelが生成)
INSERT INTO ...
UPDATE ...
-- 更新後の状態
SELECT * INTO #After FROM テーブル名;
-- 差分
SELECT * FROM #After
EXCEPT
SELECT * FROM #Before;
ROLLBACK TRANSACTION;この方式のメリット
- 実DBを使って「本番と同じ条件」で確認できる
- SQLを実行しても 実データは一切変わらない
- 更新前/更新後/差分をすべて目視できる
つまり、
DB更新の“リハーサル”が安全にできる
という状態になります。
INSERTがある場合の工夫(行ズレ防止)
INSERT が含まれると、
単純な SELECT * FROM テーブル では行順がズレてしまい、
更新前後の比較が非常にやりづらくなります。
そこでこのツールでは、
- 元から存在していたデータ
- INSERTされたデータ
を SQL上で分けて表示することで、
- 既存データは同じ順序
- INSERT行は最後にまとまって表示
されるようにしています。
これにより、
✅ 実行前後の比較
✅ 差分確認
がかなり楽になります。
本番実行はどうするのか?
本番で更新する場合も、Excel側で特別なことはしません。
- ExcelでSQLを生成
- SSMSでコピペして実行
- 問題がなければ
ROLLBACK TRANSACTION;を
COMMIT TRANSACTION;に 人が書き換えるだけです。
Excelが勝手にDBを更新することはありません。
このツールを作って良かった点
- SQL更新に対する心理的ハードルが下がった
- UPDATE事故の不安が減った
- 非DBエンジニアにもレビューしてもらいやすい
- Excelを「危険なDB更新ツール」にしなくて済んだ
「安全にSQLを確認できる」
という安心感は、想像以上に大きいです。
まとめ
- ExcelはDBを更新しない
- INSERT / UPDATE は SQL として生成するだけ
- ROLLBACK前提で更新前後・差分を検証する
この3点を守るだけで、
DB運用はかなり安全になります。
ソースコード全文
以下に、本記事で紹介した
Excel VBA ソースコードの全文を掲載します。
(※ 接続文字列や環境依存部分は適宜置き換えてください)
① Sheet モジュール(Worksheet_Change だけ)
✅ I / U 自動判定
✅ 行色・差分セル強調
✅ 多セル変更ガード、EnableEvents 安全復帰済み
Private Sub Worksheet_Change(ByVal Target As Range)
' 複数セル変更は無視(貼り付け事故防止)
If Target.Rows.Count > 1 Or Target.Columns.Count > 1 Then Exit Sub
Dim wsOrg As Worksheet
Dim r As Long, c As Long
Dim lastCol As Long
Dim orgLastRow As Long
Dim hasDiff As Boolean, hasData As Boolean
Dim newVal As String, orgVal As String
On Error GoTo CLEANUP
Application.EnableEvents = False
Set wsOrg = Worksheets("_ORIGINAL")
If wsOrg Is Nothing Then GoTo CLEANUP
' データ領域以外は無視
If Intersect(Target, _
Range(Cells(DATA_START_ROW, DATA_START_COL), _
Cells(Rows.Count, Columns.Count))) Is Nothing Then GoTo CLEANUP
r = Target.Row
lastCol = Cells(HEADER_ROW, Columns.Count).End(xlToLeft).Column
orgLastRow = wsOrg.Cells(wsOrg.Rows.Count, DATA_START_COL).End(xlUp).Row
' -------------------------
' INSERT 判定(ORIGINALに無い行)
' -------------------------
hasData = False
For c = DATA_START_COL To lastCol
If Trim(Cells(r, c).Value) <> "" Then
hasData = True
Exit For
End If
Next c
If r > orgLastRow Then
If hasData Then
Cells(r, MODE_COL).Value = "I"
Range(Cells(r, MODE_COL), Cells(r, lastCol)).Interior.Color = COLOR_INSERT_ROW
Else
Cells(r, MODE_COL).ClearContents
Range(Cells(r, MODE_COL), Cells(r, lastCol)).Interior.ColorIndex = xlNone
End If
UpdateExtractCounts
GoTo CLEANUP
End If
' -------------------------
' UPDATE 判定(差分ベース)
' -------------------------
hasDiff = False
For c = DATA_START_COL To lastCol
newVal = Trim(Cells(r, c).Value)
orgVal = Trim(wsOrg.Cells(r, c).Value)
If newVal <> orgVal Then
hasDiff = True
With Cells(r, c).Font
.Color = COLOR_CHANGED_FONT
.Bold = True
End With
Else
With Cells(r, c).Font
.ColorIndex = xlAutomatic
.Bold = False
End With
End If
Next c
If hasDiff Then
Cells(r, MODE_COL).Value = "U"
Range(Cells(r, MODE_COL), Cells(r, lastCol)).Interior.Color = COLOR_UPDATE_ROW
Else
Cells(r, MODE_COL).ClearContents
Range(Cells(r, MODE_COL), Cells(r, lastCol)).Interior.ColorIndex = xlNone
End If
UpdateExtractCounts
CLEANUP:
Application.EnableEvents = True
End Sub② 標準モジュール(それ以外全部)
✅ 定数・ユーティリティ
✅ データ取得/ORIGINAL退避
✅ 件数集計
✅ 実更新しない INSERT/UPDATE SQL生成
✅ ROLLBACK前提 検証用SQL出力
✅ SQLシートは常に最後/全行ハイライト
Option Explicit
'=================================================
' 定数
'=================================================
Public Const HEADER_ROW As Long = 9
Public Const COMMENT_ROW As Long = 10
Public Const TYPE_ROW As Long = 11
Public Const DATA_START_ROW As Long = 12
Public Const MODE_COL As Long = 1 ' A列(I/U)
Public Const DATA_START_COL As Long = 2 ' B列~
Public Const UI_TABLE_ROW As Long = 3
Public Const UI_TABLE_COL As Long = 2 ' B3
Public Const UI_TOTAL_ROW As Long = 5
Public Const UI_UPDATE_ROW As Long = 6
Public Const UI_INSERT_ROW As Long = 7
Public Const UI_VALUE_COL As Long = 2 ' B列
Public Const COLOR_UPDATE_ROW As Long = &HCCF2FF
Public Const COLOR_INSERT_ROW As Long = &HDAEFDA
Public Const COLOR_CHANGED_FONT As Long = &HC0
'=================================================
' ユーティリティ
'=================================================
Private Function Nz(v As Variant) As String
If IsNull(v) Then Nz = "" Else Nz = CStr(v)
End Function
Public Function IsNumericType(dataType As String) As Boolean
Select Case LCase(dataType)
Case "int", "bigint", "smallint", "tinyint", _
"decimal", "numeric", "float", "real", _
"money", "smallmoney"
IsNumericType = True
Case Else
IsNumericType = False
End Select
End Function
'=================================================
' 主キー取得(※接続情報は各自の環境で)
'=================================================
Public Function GetPrimaryKeys(conn As Object, tableName As String) As Collection
Dim rs As Object, sql As String
Dim pk As New Collection
sql = _
"SELECT c.name FROM sys.key_constraints kc " & _
"JOIN sys.index_columns ic ON kc.parent_object_id=ic.object_id " & _
" AND kc.unique_index_id=ic.index_id " & _
"JOIN sys.columns c ON ic.object_id=c.object_id " & _
" AND ic.column_id=c.column_id " & _
"WHERE kc.type='PK' AND kc.parent_object_id=OBJECT_ID('" & tableName & "') " & _
"ORDER BY ic.key_ordinal"
Set rs = conn.Execute(sql)
Do While Not rs.EOF
pk.Add CStr(rs(0))
rs.MoveNext
Loop
rs.Close
Set GetPrimaryKeys = pk
End Function
'=================================================
' 件数集計(UI表示)
'=================================================
Public Sub UpdateExtractCounts()
Dim lastRow As Long, lastCol As Long
Dim r As Long, c As Long
Dim totalCnt As Long, updateCnt As Long, insertCnt As Long
Dim hasData As Boolean
lastCol = Cells(HEADER_ROW, Columns.Count).End(xlToLeft).Column
lastRow = Cells(Rows.Count, DATA_START_COL).End(xlUp).Row
For r = DATA_START_ROW To lastRow
hasData = False
For c = DATA_START_COL To lastCol
If Trim(Cells(r, c).Value) <> "" Then
hasData = True: Exit For
End If
Next c
If hasData Then
totalCnt = totalCnt + 1
If Cells(r, MODE_COL).Value = "U" Then updateCnt = updateCnt + 1
If Cells(r, MODE_COL).Value = "I" Then insertCnt = insertCnt + 1
End If
Next r
Cells(UI_TOTAL_ROW, UI_VALUE_COL).Value = totalCnt
Cells(UI_UPDATE_ROW, UI_VALUE_COL).Value = updateCnt
Cells(UI_INSERT_ROW, UI_VALUE_COL).Value = insertCnt
End Sub
'=================================================
' SQL生成(実更新しない)
'=================================================
Public Sub GenerateInsertUpdateSQL_ToSheet()
Dim ws As Worksheet, conn As Object
Dim tableName As String
Dim lastRow As Long, lastCol As Long
Dim r As Long, col As Long
Dim mode As String, value As String, orgVal As String
Dim colName As String, dataType As String
Dim colList As String, valList As String
Dim setClause As String, whereClause As String
Dim pk As Collection, pkName As Variant
Dim sqlBody As Collection: Set sqlBody = New Collection
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
On Error GoTo CLEANUP
Set ws = ActiveSheet
tableName = Trim(ws.Cells(UI_TABLE_ROW, UI_TABLE_COL).Value)
If tableName = "" Then GoTo CLEANUP
lastCol = ws.Cells(HEADER_ROW, ws.Columns.Count).End(xlToLeft).Column
lastRow = ws.Cells(ws.Rows.Count, DATA_START_COL).End(xlUp).Row
' PK取得(※接続情報は各自置換)
Set conn = CreateObject("ADODB.Connection")
conn.Open "Provider=SQLOLEDB;Data Source=YOUR_SERVER;Initial Catalog=YOUR_DB;User ID=USER;Password=PASSWORD;"
Set pk = GetPrimaryKeys(conn, tableName)
conn.Close
For r = DATA_START_ROW To lastRow
mode = UCase(Trim(ws.Cells(r, MODE_COL).Value))
If mode <> "I" And mode <> "U" Then GoTo NextRow
colList = "": valList = "": setClause = "": whereClause = ""
If mode = "I" Then
For col = DATA_START_COL To lastCol
colName = ws.Cells(HEADER_ROW, col).Value
dataType = ws.Cells(TYPE_ROW, col).Value
value = Trim(ws.Cells(r, col).Value)
colList = colList & colName & ", "
If IsNumericType(dataType) Then
valList = valList & value & ", "
Else
valList = valList & "'" & Replace(value, "'", "''") & "', "
End If
Next col
sqlBody.Add "INSERT INTO " & tableName & _
" (" & Left(colList, Len(colList)-2) & _
") VALUES (" & Left(valList, Len(valList)-2) & ");"
End If
If mode = "U" Then
For col = DATA_START_COL To lastCol
value = Trim(ws.Cells(r, col).Value)
orgVal = Trim(Worksheets("_ORIGINAL").Cells(r, col).Value)
If value <> orgVal Then
setClause = setClause & ws.Cells(HEADER_ROW, col).Value & _
" = '" & Replace(value, "'", "''") & "', "
End If
Next col
If setClause <> "" Then
For Each pkName In pk
For col = DATA_START_COL To lastCol
If ws.Cells(HEADER_ROW, col).Value = pkName Then
whereClause = whereClause & pkName & _
" = '" & ws.Cells(r, col).Value & "' AND "
End If
Next col
Next pkName
sqlBody.Add "UPDATE " & tableName & _
" SET " & Left(setClause, Len(setClause)-2) & _
" WHERE " & Left(whereClause, Len(whereClause)-5) & ";"
End If
End If
NextRow:
Next r
If sqlBody.Count > 0 Then OutputVerificationSQL sqlBody, tableName
CLEANUP:
Application.ScreenUpdating = True
Application.EnableEvents = True
Application.Calculation = xlCalculationAutomatic
End Sub
'=================================================
' 検証用SQL出力(ROLLBACK前提・1行ずつ)
'=================================================
Public Sub OutputVerificationSQL(sqlBody As Collection, tableName As String)
Dim wsOut As Worksheet
Dim r As Long, i As Long
On Error Resume Next
Worksheets("SQL").Delete
On Error GoTo 0
Set wsOut = Worksheets.Add(After:=Worksheets(Worksheets.Count))
wsOut.Name = "SQL"
r = 1
wsOut.Cells(r, 1).Value = "BEGIN TRANSACTION;": r = r + 2
wsOut.Cells(r, 1).Value = "SELECT * INTO #Before FROM " & tableName & ";": r = r + 2
For i = 1 To sqlBody.Count
wsOut.Cells(r, 1).Value = sqlBody(i)
r = r + 1
Next i
wsOut.Cells(r, 1).Value = "SELECT * INTO #After FROM " & tableName & ";": r = r + 2
wsOut.Cells(r, 1).Value = "SELECT * FROM #After EXCEPT SELECT * FROM #Before;": r = r + 2
wsOut.Cells(r, 1).Value = "ROLLBACK TRANSACTION;"
With wsOut.Range(wsOut.Cells(1, 1), wsOut.Cells(r, 1))
.Interior.Color = RGB(226, 239, 218)
End With
wsOut.Columns(1).AutoFit
End Sub


コメント