「【総まとめ】UPSERTとは?DBごとの違いは?」では、主要なDBごとの書き方をざっくり説明しました。
今回は、MySQLでのUpsertに焦点を絞り、さらに掘り下げていきたいと思います。
参考として、DataFrameからUpsertを生成するPython関数のサンプルも紹介しているので、必要に応じてコピぺしてお使いください。
MySQLは Insert into ~ on duplicate key ~ で Upsertを行う
MySQLでUpsertを行う場合は、Insert into ~ on duplicate key~を使います。

要約すると、Insert Into ~にインサート文を記述し on duplicate key update ~ にアップデート文を記述します。
MySQLの場合、更新先テーブルに対するキーの存在チェックは、プライマリキーで判断しているため、他のデータベースの様にユニークキーを記述する必要はありません。
これは記述が簡素化できるメリットがありますが、逆にプライマリキー以外のユニークキーを使ったUpsertが出来ないという意味でもあります。
ただ、実際の利用シーンではプライマリキーでUpsertすることがほとんどなので、それほど問題にはならないかと思います。
尚、value 句で指定した値を update句で参照するには、values(列名) と記述しますが、table_A にエイリアス名を付けて参照することも可能です。直接テーブル名を指定してもうまく動作しませんのでご注意ください
Insert Into の後に value ではなく select * from テーブル名 を記述すると、2つのテーブル間でのUpsert が行えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
-- table_A に1行 Upsert する場合(valuesを使う例) insert into table_A values(1,'2023/01/01 09:00:00',10000,5) on duplicate key update count=values(count) -- table_A に1行 Upsert する場合(エイリアスを使う例) insert into table_A values(1,'2023/01/01 09:00:00',10000,5) as t on duplicate key update count=t.count -- table_B からtable_A に Upsert する場合 -- ※テーブルのエイリアスは利用できない insert into table_A select * from table_B on duplicate key update table_A.count=table_B.count |
検証データ
本記事のUpsert文を実際に試してみたい場合は、事前に以下のSQLを実行しておいて下さい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
--サンプルテーブルの作成とテストデータ登録 create table oders ( shop_no integer not null, order_date timestamp not null, product_id integer not null, count integer not null, constraint oders_p_key primary key (shop_no,order_date,product_id) ); alter table oders add constraint oders_uniquekeys unique(shop_no,order_date,product_id) insert into oders values(1,'2023/01/01 09:00:00',10000,5); insert into oders values(1,'2023/01/01 09:10:00',10000,15); insert into oders values(1,'2023/01/01 09:20:00',10000,25); --下記はテーブル間のUpsert確認用(odersを丸ごとコピー) create table old_oders as select * from oders |
1行をUpsertする

下記は、サンプルテーブルに (1,’2023/01/01 09:00:00′,10000,15) のデータをInsertし、レコードが既に存在していれば 数量(count)を 15にUpdate するサンプルです。
1 2 |
insert into oders values(1,'2023/01/01 09:00:00',10000,15) on duplicate key update count=values(count) |
複数行をまとめてUpsertする

MySQLでは、1回のInsert文で複数行(複数レコード)をまとめて登録する マルチインサート文が利用できますが、マルチインサート文に続けて ON DUPLICATE KEY UPDATE ~ と記述することで、複数行をまとめてUpsertできます。
1 2 3 4 5 |
insert into oders values (1,'2023/01/01 09:01:00',10000,15), (1,'2023/01/01 09:02:00',10100,12), (1,'2023/01/01 09:03:00',10200,18) on duplicate key update count=values(count) |
この例では、on duplicate key updat ~ では values 関数を使って Updateの値を参照しています。こうすることで、Insertに失敗した場合、各Insert ごとの count の値が Update されるようになります。
MySQL では、on duplicate key updat count=5 の様に、直接値を指定することも可能ですが、この場合はInsertに失敗したレコード(つまりUpdateされるレコード)は一律 5 で更新されてしまいます(PostgreSQLやSQLiteの場合はエラーになる)。
また、1つのInsert文の中に、キーが重複するデータが含まれている場合は後勝ち(1回目でInsertされ、2回目でUpdateされるイメージ)になります(PostgreSQLやSQLiteの場合はエラーになる)。

テーブル間でUpsertする

2つのテーブルAとBがあって、Bの内容をAにUpsertしたい場合、select * from を使います。
insert into テーブル名A select * from テーブル名B ~
下記は old_oders から oders へ Upsertするサンプルです。
1 2 |
insert into oders select * from old_oders on duplicate key update oders.count=old_oders.count |
UpsertでInsertだけ行う(Updateしない)

なければインサート、あれば何もしたくない場合は、insert ignore ~ を使用します。
1 2 3 4 5 |
-- oders に向けてインサートを行うが、既に存在する場合は何もしない insert ignore into oders values(1,'2023/01/01 09:00:00',10000,15) -- old_odersから oders に向けてインサートを行うが、既に存在するレコードには何もしない insert ignore into oders select * from old_oders |
Pythonのサンプル
次に、参考例としてDataFrameの内容からUpsert文を生成するPythonのサンプルプログラムをご紹介します。
使い方のサンプル
create_upsert にDataFrame、テーブル名、ユニークキーのリストを指定するだけです。
尚、下記サンプルで生成したUpsert文は、最初に紹介したサンプルテーブルに対して実行が可能です。
1 2 3 4 5 6 7 8 9 10 |
datas = [ [1,'2023/01/01 09:00:00',10000,55], [1,'2023/01/01 09:10:00',10000,65], [1,'2023/01/01 09:20:00',10000,75] ] df = pd.DataFrame(datas,columns=['shop_no','order_date','product_id','count']) sqls = create_upsert(df,'oders',['shop_no','order_date','product_id']) for sql in sqls: print(sql) |
関数のソースコード
以下は、関数本体のソースコードです。数値以外のデータ(文字列、日付)はシングルクォートで括る必要がありますが、それを dtype が ‘object’ か否かで判断しています。
また、insert into ~ values ~ の values に渡す列の値に ‘nan’ という文字列が含まれている場合、無条件に NULL に置換しています。ほとんど発生しないと思いますが、列の値に ‘nan’ という文字列がが含まれていると、NULLに変換されてしまうのでご注意下さい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
MySQL用に書き換えが必要 import pandas as pd def create_upsert(df,table,key_columns): ''' DataFrameの内容からUpser文を作成する Parameters ---------- df : DataFrame データを格納したDataFrame table : str Upsert対象のテーブル名 key_columns : list of str ユニークキーのカラム 例:['shop_no','order_date',・・・]" Returns ---------- res:str 生成されたUpsertインサート文 ''' # df から全てのカラム名を取り出す columns = ",".join([i for i in df.columns]) # シングルクォート格納用のリストを作り、カラムの型がobjectの場合のみシングルクォートをセット quote = ["'" if str(dtype) == 'object' else '' for dtype in df.dtypes] sqls = [] # 全ての行を取り出す for row in df.itertuples(): # 0カラム名(行インデックス)を飛ばし、必要に応じて各カラム値をシングルクォートで括る。 items = [quote[i] + str(row[i+1]) + quote[i] for i in range(len(df.columns))] # Insert句の作成 values = ','.join(items).replace("'nan'", "NULL") insert = f'insert into {table}({columns}) values({values}) ' # conflict句の作成 keys = ','.join(key_columns) conflict = f'on conflict ({keys}) ' # update句の作成 set_list = [f'{x}=excluded.{x}' for x in df.columns if x not in key_columns] update = f"do update set " + ','.join(set_list) # insert、conflict、updateを結合 sqls.append(insert + conflict + update) return sqls |
まとめ
今回はMySQLのUpsertに焦点を絞り、掘り下げてみました。
MySQLでUpsertを行う場合、Insert into ~ にインサート文を、do ~ に update 文を記述します。
また、キーが存在した場合に何もしなくない場合は、insert ignore into ~ と記述します。
また、update ~ にvalues(列名) と記述することで、これから Insertしようとする値が参照できます。
MySQLでUpsertが必要になった時は、是非この記事を参考にしてください。