データの更新
このドキュメントでは、さまざまなプログラミング言語で次の SQL ステートメントを使用して TiDB のデータを更新する方法について説明します。
- アップデート : 指定されたテーブル内のデータを変更するために使用されます。
- 重複キー更新時の挿入 : 主キーまたは一意キーの競合がある場合に、データを挿入し、そのデータを更新するために使用されます。一意キー(主キーを含む)が複数ある場合は、このステートメントの使用は推奨されません。これは、このステートメントが一意キー(主キーを含む)の競合を検出すると、すぐにデータを更新するためです。複数の行の競合がある場合は、1行のみが更新されます。
始める前に
このドキュメントを読む前に、次のものを準備する必要があります。
- TiDB Cloudサーバーレスクラスタの構築 。
- スキーマ設計の概要 データベースを作成する セカンダリインデックスを作成するでくださいテーブルを作成する
UPDATE
データが必要な場合は、まずデータを挿入するが必要です。
UPDATE
を使用する
テーブル内の既存の行を更新するには、 UPDATE
ステートメントとWHERE
句を使用して更新する列をフィルターする必要があります。
注記:
多数の行(例えば1万行以上)を更新する必要がある場合は、一度にすべての行を更新するのではなく、すべての行が更新されるまで、部分的な更新を繰り返すことをお勧めします。この操作をループさせるスクリプトやプログラムを作成することもできます。詳細は一括更新ご覧ください。
UPDATE
SQL構文
SQL では、 UPDATE
文は通常次の形式になります。
UPDATE {table} SET {update_column} = {update_value} WHERE {filter_column} = {filter_value}
パラメータ名 | 説明 |
---|---|
{table} | テーブル名 |
{update_column} | 更新するカラム名 |
{update_value} | 更新するカラムの値 |
{filter_column} | フィルターに一致するカラム名 |
{filter_value} | フィルターに一致するカラムの値 |
詳細についてはUPDATE構文参照してください。
ベストプラクティスUPDATE
次に、データを更新するためのベスト プラクティスをいくつか示します。
UPDATE
ステートメントでは必ずWHERE
句を指定してください。5UPDATE
ステートメントにWHERE
句がない場合、TiDBはテーブル内のすべての行を更新します。
- 多数の行(例えば1万行以上)を更新する必要がある場合は、 一括更新使用してください。TiDBはデフォルトで1トランザクションのサイズを100MBに制限しているため、一度に大量のデータ更新を行うと、ロックが長時間保持される( 悲観的取引 )か、競合が発生する( 楽観的取引 )可能性があります。
UPDATE
例
ある著者がHelen Harukiに改名したとします。3 著者テーブルを変更する必要があります。彼女の固有id
が1だとすると、フィルターはid = 1
になります。
UPDATE `authors` SET `name` = "Helen Haruki" WHERE `id` = 1;
// ds is an entity of com.mysql.cj.jdbc.MysqlDataSource
try (Connection connection = ds.getConnection()) {
PreparedStatement pstmt = connection.prepareStatement("UPDATE `authors` SET `name` = ? WHERE `id` = ?");
pstmt.setString(1, "Helen Haruki");
pstmt.setInt(2, 1);
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
INSERT ON DUPLICATE KEY UPDATE
を使用する
テーブルに新しいデータを挿入する必要があるが、一意キー(主キーも一意キー)の競合が発生した場合、最初に競合が発生したレコードが更新されます。1 INSERT ... ON DUPLICATE KEY UPDATE ...
ステートメントで挿入または更新を実行できます。
INSERT ON DUPLICATE KEY UPDATE
SQL構文
SQL では、 INSERT ... ON DUPLICATE KEY UPDATE ...
文は通常次の形式になります。
INSERT INTO {table} ({columns}) VALUES ({values})
ON DUPLICATE KEY UPDATE {update_column} = {update_value};
パラメータ名 | 説明 |
---|---|
{table} | テーブル名 |
{columns} | 挿入するカラム名 |
{values} | 挿入するカラムの値 |
{update_column} | 更新するカラム名 |
{update_value} | 更新するカラムの値 |
INSERT ON DUPLICATE KEY UPDATE
ベストプラクティス
INSERT ON DUPLICATE KEY UPDATE
、一意のキーが 1 つしかないテーブルにのみ使用してください。このステートメントは、一意のキー(主キーを含む)の競合が検出された場合、データを更新します。競合行が複数ある場合、更新されるのは 1 行のみです。したがって、競合行が 1 行だけであることを保証できない限り、複数の一意のキーを持つテーブルでINSERT ON DUPLICATE KEY UPDATE
ステートメントを使用することは推奨されません。- データを作成または更新するときにこのステートメントを使用します。
INSERT ON DUPLICATE KEY UPDATE
例
例えば、 評価テーブルを更新して、ユーザーによる書籍の評価を含める必要があります。ユーザーがまだ書籍を評価していない場合は、新しい評価が作成されます。ユーザーがすでに評価している場合は、以前の評価が更新されます。
次の例では、主キーはbook_id
とuser_id
の結合主キーです。ユーザーuser_id = 1
書籍book_id = 1000
に5
の評価を付けています。
INSERT INTO `ratings`
(`book_id`, `user_id`, `score`, `rated_at`)
VALUES
(1000, 1, 5, NOW())
ON DUPLICATE KEY UPDATE `score` = 5, `rated_at` = NOW();
// ds is an entity of com.mysql.cj.jdbc.MysqlDataSource
try (Connection connection = ds.getConnection()) {
PreparedStatement p = connection.prepareStatement("INSERT INTO `ratings` (`book_id`, `user_id`, `score`, `rated_at`)
VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE `score` = ?, `rated_at` = NOW()");
p.setInt(1, 1000);
p.setInt(2, 1);
p.setInt(3, 5);
p.setInt(4, 5);
p.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
一括更新
テーブル内の複数のデータ行を更新する必要がある場合は、 INSERT ON DUPLICATE KEY UPDATE
を使用するとWHERE
句を使用して、更新する必要があるデータをフィルター処理できます。
ただし、多数の行(たとえば1万行以上)を更新する必要がある場合は、データを繰り返し更新することをお勧めします。つまり、更新が完了するまで、各反復でデータの一部のみを更新します。これは、TiDBが1トランザクションのサイズをデフォルトで100 MBに制限しているためです。一度に多くのデータを更新すると、ロックが長時間保持される( 悲観的取引 )、または競合が発生する( 楽観的取引 )ことになります。プログラムまたはスクリプトでループを使用して、操作を完了することができます。
このセクションでは、反復更新を処理するスクリプトの記述例を示します。この例では、 SELECT
とUPDATE
組み合わせて一括更新を実行する方法を示します。
一括更新ループを書く
まず、アプリケーションまたはスクリプトのループ内にSELECT
クエリを記述します。このクエリの戻り値は、更新が必要な行の主キーとして使用できます。このSELECT
クエリを定義する際は、 WHERE
番目の句を使用して更新が必要な行をフィルタリングする必要があることに注意してください。
例
過去1年間、あなたのウェブサイトbookshop
でユーザーから多くの書籍評価が寄せられたとします。しかし、当初の5段階評価の設計では、書籍評価の差別化が不十分でした。ほとんどの書籍は3
評価です。そこで、評価を差別化するために、5段階評価から10段階評価に変更することにしました。
先ほどの5段階評価のratings
番目のテーブルのデータを2
倍し、評価テーブルに新しい列を追加して、行が更新されたかどうかを示します。この列を使用することで、 SELECT
に更新された行を除外できます。これにより、スクリプトがクラッシュして行を複数回更新し、不合理なデータが生成されることを防ぐことができます。
たとえば、10 点スケールかどうかの識別子として、データ型ブールを持つten_point
という名前の列を作成します。
ALTER TABLE `bookshop`.`ratings` ADD COLUMN `ten_point` BOOL NOT NULL DEFAULT FALSE;
注記:
この一括更新アプリケーションは、 DDL文を使用してデータテーブルのスキーマを変更します。TiDBのすべてのDDL変更操作はオンラインで実行されます。詳細については、 列を追加参照してください。
Golangでは、一括更新アプリケーションは次のようになります。
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"strings"
"time"
)
func main() {
db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:4000)/bookshop")
if err != nil {
panic(err)
}
defer db.Close()
bookID, userID := updateBatch(db, true, 0, 0)
fmt.Println("first time batch update success")
for {
time.Sleep(time.Second)
bookID, userID = updateBatch(db, false, bookID, userID)
fmt.Printf("batch update success, [bookID] %d, [userID] %d\n", bookID, userID)
}
}
// updateBatch select at most 1000 lines data to update score
func updateBatch(db *sql.DB, firstTime bool, lastBookID, lastUserID int64) (bookID, userID int64) {
// select at most 1000 primary keys in five-point scale data
var err error
var rows *sql.Rows
if firstTime {
rows, err = db.Query("SELECT `book_id`, `user_id` FROM `bookshop`.`ratings` " +
"WHERE `ten_point` != true ORDER BY `book_id`, `user_id` LIMIT 1000")
} else {
rows, err = db.Query("SELECT `book_id`, `user_id` FROM `bookshop`.`ratings` "+
"WHERE `ten_point` != true AND `book_id` > ? AND `user_id` > ? "+
"ORDER BY `book_id`, `user_id` LIMIT 1000", lastBookID, lastUserID)
}
if err != nil || rows == nil {
panic(fmt.Errorf("error occurred or rows nil: %+v", err))
}
// joint all id with a list
var idList []interface{}
for rows.Next() {
var tempBookID, tempUserID int64
if err := rows.Scan(&tempBookID, &tempUserID); err != nil {
panic(err)
}
idList = append(idList, tempBookID, tempUserID)
bookID, userID = tempBookID, tempUserID
}
bulkUpdateSql := fmt.Sprintf("UPDATE `bookshop`.`ratings` SET `ten_point` = true, "+
"`score` = `score` * 2 WHERE (`book_id`, `user_id`) IN (%s)", placeHolder(len(idList)))
db.Exec(bulkUpdateSql, idList...)
return bookID, userID
}
// placeHolder format SQL place holder
func placeHolder(n int) string {
holderList := make([]string, n/2, n/2)
for i := range holderList {
holderList[i] = "(?,?)"
}
return strings.Join(holderList, ",")
}
各反復で、 SELECT
主キーの順にクエリを実行します。10 段階評価 ( ten_point
はfalse
) に更新されていない最大1000
行の主キー値を選択します。各SELECT
文では、重複を防ぐため、前のSELECT
結果の最大値よりも大きい主キーを選択します。次に、一括更新を使用し、そのscore
列を2
で乗算して、 ten_point
をtrue
に設定します。 ten_point
更新する目的は、クラッシュ後の再起動時に更新アプリケーションが同じ行を繰り返し更新してデータ破損を引き起こすのを防ぐことです。 time.Sleep(time.Second)
各ループで、更新アプリケーションを 1 秒間一時停止して、更新アプリケーションがハードウェア リソースを過剰に消費するのを防ぎます。
Java (JDBC) では、一括更新アプリケーションは次のようになります。
コード:
package com.pingcap.bulkUpdate;
import com.mysql.cj.jdbc.MysqlDataSource;
import java.sql.*;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class BatchUpdateExample {
static class UpdateID {
private Long bookID;
private Long userID;
public UpdateID(Long bookID, Long userID) {
this.bookID = bookID;
this.userID = userID;
}
public Long getBookID() {
return bookID;
}
public void setBookID(Long bookID) {
this.bookID = bookID;
}
public Long getUserID() {
return userID;
}
public void setUserID(Long userID) {
this.userID = userID;
}
@Override
public String toString() {
return "[bookID] " + bookID + ", [userID] " + userID ;
}
}
public static void main(String[] args) throws InterruptedException {
// Configure the example database connection.
// Create a mysql data source instance.
MysqlDataSource mysqlDataSource = new MysqlDataSource();
// Set server name, port, database name, username and password.
mysqlDataSource.setServerName("localhost");
mysqlDataSource.setPortNumber(4000);
mysqlDataSource.setDatabaseName("bookshop");
mysqlDataSource.setUser("root");
mysqlDataSource.setPassword("");
UpdateID lastID = batchUpdate(mysqlDataSource, null);
System.out.println("first time batch update success");
while (true) {
TimeUnit.SECONDS.sleep(1);
lastID = batchUpdate(mysqlDataSource, lastID);
System.out.println("batch update success, [lastID] " + lastID);
}
}
public static UpdateID batchUpdate (MysqlDataSource ds, UpdateID lastID) {
try (Connection connection = ds.getConnection()) {
UpdateID updateID = null;
PreparedStatement selectPs;
if (lastID == null) {
selectPs = connection.prepareStatement(
"SELECT `book_id`, `user_id` FROM `bookshop`.`ratings` " +
"WHERE `ten_point` != true ORDER BY `book_id`, `user_id` LIMIT 1000");
} else {
selectPs = connection.prepareStatement(
"SELECT `book_id`, `user_id` FROM `bookshop`.`ratings` "+
"WHERE `ten_point` != true AND `book_id` > ? AND `user_id` > ? "+
"ORDER BY `book_id`, `user_id` LIMIT 1000");
selectPs.setLong(1, lastID.getBookID());
selectPs.setLong(2, lastID.getUserID());
}
List<Long> idList = new LinkedList<>();
ResultSet res = selectPs.executeQuery();
while (res.next()) {
updateID = new UpdateID(
res.getLong("book_id"),
res.getLong("user_id")
);
idList.add(updateID.getBookID());
idList.add(updateID.getUserID());
}
if (idList.isEmpty()) {
System.out.println("no data should update");
return null;
}
String updateSQL = "UPDATE `bookshop`.`ratings` SET `ten_point` = true, "+
"`score` = `score` * 2 WHERE (`book_id`, `user_id`) IN (" +
placeHolder(idList.size() / 2) + ")";
PreparedStatement updatePs = connection.prepareStatement(updateSQL);
for (int i = 0; i < idList.size(); i++) {
updatePs.setLong(i + 1, idList.get(i));
}
int count = updatePs.executeUpdate();
System.out.println("update " + count + " data");
return updateID;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public static String placeHolder(int n) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n ; i++) {
sb.append(i == 0 ? "(?,?)" : ",(?,?)");
}
return sb.toString();
}
}
hibernate.cfg.xml
構成:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- Database connection settings -->
<property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<property name="hibernate.dialect">org.hibernate.dialect.TiDBDialect</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:4000/movie</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password"></property>
<property name="hibernate.connection.autocommit">false</property>
<property name="hibernate.jdbc.batch_size">20</property>
<!-- Optional: Show SQL output for debugging -->
<property name="hibernate.show_sql">true</property>
<property name="hibernate.format_sql">true</property>
</session-factory>
</hibernate-configuration>
各反復で、 SELECT
主キーの順にクエリを実行します。10 段階評価 ( ten_point
はfalse
) に更新されていない最大1000
行の主キー値を選択します。各SELECT
文では、重複を防ぐため、前のSELECT
結果の最大値よりも大きい主キーを選択します。次に、一括更新を使用し、そのscore
列を2
で乗算して、 ten_point
をtrue
に設定します。 ten_point
更新する目的は、クラッシュ後の再起動時に更新アプリケーションが同じ行を繰り返し更新してデータ破損を引き起こすのを防ぐことです。 TimeUnit.SECONDS.sleep(1);
各ループで、更新アプリケーションを 1 秒間一時停止して、更新アプリケーションがハードウェア リソースを過剰に消費するのを防ぎます。
ヘルプが必要ですか?
不和またはスラック 、あるいはサポートチケットを送信するについてコミュニティに質問してください。