web-dev-qa-db-ja.com

一致する行を削除するより速い方法は?

データベースに関しては、私は比較的初心者です。 MySQLを使用しており、現在、実行に時間がかかると思われるSQLステートメントを高速化しようとしています。私はSOで同様の質問を探しましたが、見つかりませんでした。

目標は、テーブルBに一致するIDを持つテーブルAのすべての行を削除することです。

現在、次のことを行っています。

DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE b.id = a.id);

テーブルaには約100K行、テーブルbには約22K行があります。列 'id'は、両方のテーブルのPKです。

このステートメントは、テストボックスで実行するのに約3分かかります-Pentium D、XP SP3、2GB ram、MySQL 5.0.67。これは私には時間がかかりそうです。物事をスピードアップしたいと思っていました。これを達成するためのより良い/より速い方法はありますか?


編集:

役に立つかもしれないいくつかの追加情報。テーブルAとBの構造は、テーブルBを作成するために次のことを行ったものと同じです。

CREATE TABLE b LIKE a;

テーブルa(およびテーブルb)には、テーブルに対して行われるクエリを高速化するためのインデックスがいくつかあります。繰り返しになりますが、私はDB作業の比較的初心者であり、まだ学んでいます。これが物事にどれほどの影響を与えるかはわかりません。インデックスもクリーンアップする必要があるため、効果があると思いますか?また、速度に影響する可能性のある他のDB設定があるかどうか疑問に思っていました。

また、私はINNO DBを使用しています。


役立つ情報をいくつか紹介します。

テーブルAの構造は次のようになります(これを少しサニタイズしました)。

DROP TABLE IF EXISTS `frobozz`.`a`;
CREATE TABLE  `frobozz`.`a` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `fk_g` varchar(30) NOT NULL,
  `h` int(10) unsigned default NULL,
  `i` longtext,
  `j` bigint(20) NOT NULL,
  `k` bigint(20) default NULL,
  `l` varchar(45) NOT NULL,
  `m` int(10) unsigned default NULL,
  `n` varchar(20) default NULL,
  `o` bigint(20) NOT NULL,
  `p` tinyint(1) NOT NULL,
  PRIMARY KEY  USING BTREE (`id`),
  KEY `idx_l` (`l`),
  KEY `idx_h` USING BTREE (`h`),
  KEY `idx_m` USING BTREE (`m`),
  KEY `idx_fk_g` USING BTREE (`fk_g`),
  KEY `fk_g_frobozz` (`id`,`fk_g`),
  CONSTRAINT `fk_g_frobozz` FOREIGN KEY (`fk_g`) REFERENCES `frotz` (`g`)
) ENGINE=InnoDB AUTO_INCREMENT=179369 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

問題の一部は、このテーブルに多数のインデックスがあることだと思います。表Bは表Bに似ていますが、列idおよびhのみが含まれています。

また、プロファイリングの結果は次のとおりです。

starting 0.000018
checking query cache for query 0.000044
checking permissions 0.000005
Opening tables 0.000009
init 0.000019
optimizing 0.000004
executing 0.000043
end 0.000005
end 0.000002
query end 0.000003
freeing items 0.000007
logging slow query 0.000002
cleaning up 0.000002

[〜#〜] solved [〜#〜]

すべての回答とコメントに感謝します。彼らは確かに私に問題について考えさせました。 dotjoeに感謝します。「他のテーブルはa.idを参照していますか?」

問題は、ストアドプロシージャを呼び出して他の2つのテーブルCおよびDを更新するテーブルAにDELETE TRIGGERがあったことです。 、それは声明を持っていた、

DELETE FROM c WHERE c.id = theId;

EXPLAINステートメントを調べて、これを次のように書き直しました。

EXPLAIN SELECT * FROM c WHERE c.other_id = 12345;

だから、私はこれが何をしていたのかを見ることができ、それは私に次の情報を与えました:

id            1
select_type   SIMPLE
table         c
type          ALL
possible_keys NULL
key           NULL
key_len       NULL
ref           NULL
rows          2633
Extra         using where

これにより、作成するのは骨の折れる操作であり、(削除される特定のデータセットに対して)22500回呼び出されるため、それが問題であることがわかりました。 other_id列にINDEXを作成し、EXPLAINを再実行すると、次の結果が得られました。

id            1
select_type   SIMPLE
table         c
type          ref
possible_keys Index_1
key           Index_1
key_len       8
ref           const
rows          1
Extra         

はるかに良い、実際には本当に素晴らしい。

Index_1と削除時間は、mattkempによって報告される時間と一致していることを追加しました。これは、土壇場でいくつかの追加機能をシューホーンにしたため、私の側では本当に微妙なエラーでした。 Danielが述べたように、提案された代替のDELETE/SELECTステートメントのほとんどは、本質的に同じ時間とsoulmergeが述べたように、このステートメントは、私がする必要があることに基づいて構築することができようとしていたほとんど最高のものでした。この他のテーブルCにインデックスを提供すると、DELETEは高速になりました。

死後
この演習から得られた2つの教訓。まず、SQLクエリの影響をよりよく理解するためにEXPLAINステートメントの力を活用しなかったことは明らかです。それは新人の間違いですので、私はそのことについて自分自身を打ち負かすつもりはありません。その間違いから学びます。第二に、問題のあるコードは「すぐに実行」という考え方の結果であり、不適切な設計/テストにより、この問題はすぐには現れませんでした。この新しい機能のテスト入力として使用するために、かなりの数のテストデータセットを生成しておけば、時間もあなたのものも無駄にしませんでした。 DB側での私のテストでは、アプリケーション側の適切な深さが欠けていました。今、私はそれを改善する機会を得ました。

参照:EXPLAINステートメント

57
itsmatt

InnoDBからデータを削除することは、要求できる最も費用のかかる操作です。既に発見したように、クエリ自体は問題ではありません-それらのほとんどはとにかく同じ実行計画に最適化されます。

すべてのケースのDELETEが最も遅い理由を理解するのは難しいかもしれませんが、かなり簡単な説明があります。 InnoDBはトランザクションストレージエンジンです。つまり、クエリが途中で中止された場合でも、何も起こらなかったかのようにすべてのレコードがまだ配置されていることになります。完了すると、すべてが同じ瞬間に消えます。 DELETE中、サーバーに接続している他のクライアントは、DELETEが完了するまでレコードを参照します。

これを実現するために、InnoDBはMVCC(Multi Version Concurrency Control)と呼ばれる技術を使用します。基本的には、各接続に、トランザクションの最初のステートメントが開始されたときのデータベース全体のスナップショットビューを提供します。これを実現するために、InnoDBのすべてのレコードは、内部的に複数の値(スナップショットごとに1つ)を持つことができます。これが、InnoDBでのCOUNTingに時間がかかる理由でもあります。これは、その時点で表示されるスナップショットの状態によって異なります。

DELETEトランザクションの場合、クエリ条件に従って識別される各レコードはすべて、削除のマークが付けられます。他のクライアントが同時にデータにアクセスしている可能性があるため、削除の原子性を保証するためにそれぞれのスナップショットを確認する必要があるため、それらをすぐにテーブルから削除することはできません。

すべてのレコードが削除対象としてマークされると、トランザクションは正常にコミットされます。そして、それでも、DELETEトランザクションの前にスナップショット値で機能していた他のすべてのトランザクションが同様に終了する前に、それらを実際のデータページからすぐに削除することはできません。

したがって、トランザクションの安全な方法で削除する準備をするためにすべてのレコードを変更する必要があることを考えると、実際には3分はそれほど遅くありません。おそらく、ステートメントの実行中にハードディスクの動作を「聞く」でしょう。これは、すべての行にアクセスすることにより発生します。パフォーマンスを向上させるには、サーバーのInnoDBバッファープールサイズを増やして、削除中にデータベースへの他のアクセスを制限し、InnoDBがレコードごとに保持する必要がある履歴バージョンの数を減らします。追加メモリにより、InnoDBはテーブルを(ほとんど)メモリに読み込み、ディスクシーク時間を回避できる場合があります。

75

3分の時間は本当に遅いようです。私の推測では、id列のインデックスが適切に作成されていません。使用している正確なテーブル定義を提供できると便利です。

テストデータを生成する単純なpythonスクリプトを作成し、同じデータセットに対して複数バージョンの削除クエリを実行しました。テーブル定義は次のとおりです。

drop table if exists a;
create table a
 (id bigint unsigned  not null primary key,
  data varchar(255) not null) engine=InnoDB;

drop table if exists b;
create table b like a;

次に、100k行をaに挿入し、25k行をbに挿入しました(そのうち22.5kもaにありました)。さまざまな削除コマンドの結果を次に示します。ちなみに、実行と実行の間にテーブルをドロップして再作成しました。

mysql> DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE a.id=b.id);
Query OK, 22500 rows affected (1.14 sec)

mysql> DELETE FROM a USING a LEFT JOIN b ON a.id=b.id WHERE b.id IS NOT NULL;
Query OK, 22500 rows affected (0.81 sec)

mysql> DELETE a FROM a INNER JOIN b on a.id=b.id;
Query OK, 22500 rows affected (0.97 sec)

mysql> DELETE QUICK a.* FROM a,b WHERE a.id=b.id;
Query OK, 22500 rows affected (0.81 sec)

すべてのテストは、Intel Core2クアッドコア2.5GHz、2GB RAM Ubuntu 8.10およびMySQL 5.0で実行されました。1つのSQLステートメントの実行はまだシングルスレッドであることに注意してください。


更新:

Itsmattのスキーマを使用するようにテストを更新しました。自動インクリメント(合成データを生成しています)と文字セットエンコーディング(機能していませんでした-掘り下げていません)を削除して、少し変更しました。

新しいテーブル定義は次のとおりです。

drop table if exists a;
drop table if exists b;
drop table if exists c;

create table c (id varchar(30) not null primary key) engine=InnoDB;

create table a (
  id bigint(20) unsigned not null primary key,
  c_id varchar(30) not null,
  h int(10) unsigned default null,
  i longtext,
  j bigint(20) not null,
  k bigint(20) default null,
  l varchar(45) not null,
  m int(10) unsigned default null,
  n varchar(20) default null,
  o bigint(20) not null,
  p tinyint(1) not null,
  key l_idx (l),
  key h_idx (h),
  key m_idx (m),
  key c_id_idx (id, c_id),
  key c_id_fk (c_id),
  constraint c_id_fk foreign key (c_id) references c(id)
) engine=InnoDB row_format=dynamic;

create table b like a;

次に、aに100k行、bに25k行を使用して同じテストを再実行しました(実行間に再配置しました)。

mysql> DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE a.id=b.id);
Query OK, 22500 rows affected (11.90 sec)

mysql> DELETE FROM a USING a LEFT JOIN b ON a.id=b.id WHERE b.id IS NOT NULL;
Query OK, 22500 rows affected (11.48 sec)

mysql> DELETE a FROM a INNER JOIN b on a.id=b.id;
Query OK, 22500 rows affected (12.21 sec)

mysql> DELETE QUICK a.* FROM a,b WHERE a.id=b.id;
Query OK, 22500 rows affected (12.33 sec)

ご覧のとおり、これはおそらく複数のインデックスが原因で、以前よりもかなり遅くなります。しかし、それは3分のマークの近くにどこにもありません。

他に見たいものは、ロングテキストフィールドをスキーマの最後に移動することです。サイズが制限されているすべてのフィールドが最初にあり、テキスト、ブロブなどが最後にある場合、mySQLのパフォーマンスが向上することを覚えているようです。

9
mattkemp

これを試して:

DELETE a
FROM a
INNER JOIN b
 on a.id = b.id

サブクエリの使用は、外部クエリの各レコードに対して実行されるため、結合よりも遅くなる傾向があります。

8

これは、非常に大きなデータを操作する必要があるときに常に行うことです(ここでは、150000行のサンプルテストテーブル)。

drop table if exists employees_bak;
create table employees_bak like employees;
insert into employees_bak 
    select * from employees
    where emp_no > 100000;

rename table employees to employees_todelete;
rename table employees_bak to employees;

この場合、SQLは50000行をフィルター処理してバックアップテーブルに入れます。クエリカスケードは、低速のマシンで5秒で実行されます。独自のフィルタークエリによって、select to insertを置き換えることができます。

これは、大きなデータベースで大量削除を実行するためのトリックです!; =)

5
Tom Schaefer

これを試してください:

DELETE QUICK A.* FROM A,B WHERE A.ID=B.ID

通常のクエリよりもはるかに高速です。

構文の参照: http://dev.mysql.com/doc/refman/5.0/en/delete.html

3
Webrsk

この質問は、OPのインデックスの欠落によりほぼ解決されていることは知っていますが、この問題のより一般的な場合に有効な追加のアドバイスを提供したいと思います。

私は個人的に、別のテーブルにある1つのテーブルから多くの行を削除することに対処しましたが、私の経験では、特に多くの行が削除されることが予想される場合は、次のことを行うのが最善です。この手法は、最も重要なことに、各単一ミューテータークエリが長く実行されるほど、ラグが悪化するため、レプリケーションスレーブラグが改善されます(レプリケーションはシングルスレッドです)。

したがって、ここにあります:最初にSELECTを個別のクエリとして実行し、スクリプト/アプリケーションで返されたIDを覚えてから、バッチで削除を続行します(たとえば、一度に50,000行)。これにより、次のことが実現します。

  • 各deleteステートメントはテーブルを長時間ロックしないため、レプリケーションが制御不能になるのを遅らせません。レプリケーションを利用して比較的最新のデータを提供する場合は特に重要です。バッチを使用する利点は、各DELETEクエリに時間がかかりすぎることがわかった場合、DB構造に触れることなく、クエリを小さく調整できることです。
  • 別のSELECTを使用する別の利点は、特に何らかの理由で最適なDBインデックスを使用できない場合、SELECT自体の実行に時間がかかる場合があることです。 SELECTがDELETEの内部にある場合、ステートメント全体がスレーブに移行するとき、SELECTを最初からやり直す必要があり、長い選択を最初からやり直す必要があるため、潜在的にスレーブを遅らせる必要があります。再び、スレーブラグはひどく苦しみます。別のSELECTクエリを使用する場合、渡すのはIDのリストだけなので、この問題はなくなります。

どこかに私のロジックに欠陥があるかどうかを教えてください。

レプリケーションラグとこれと戦う方法の詳細については、これと同様に、 MySQLスレーブラグ(遅延)の説明と7つの戦い方 を参照してください。

追伸注意すべきことの1つは、もちろん、SELECTが終了してからDELETEが開始するまでの間にテーブルを編集する可能性があることです。アプリケーションに関連するトランザクションやロジックを使用して、このような詳細を処理できるようにします。

3

「a」のすべての行に対して「b」でサブクエリを実行しています。

試してください:

DELETE FROM a USING a LEFT JOIN b ON a.id = b.id WHERE b.id IS NOT NULL;
3
Evert
DELETE FROM a WHERE id IN (SELECT id FROM b)
2
chaos

たぶん、このようなヒュークエリを実行する前に、インデックスを再構築する必要があります。さて、定期的に再構築する必要があります。

REPAIR TABLE a QUICK;
REPAIR TABLE b QUICK;

そして、上記のクエリのいずれかを実行します(つまり)

DELETE FROM a WHERE id IN (SELECT id FROM b)
2
Scoregraphic

クエリ自体はすでに最適な形式であるため、インデックスを更新すると、操作全体に時間がかかります。 キーを無効にする 操作の前にそのテーブルで、それは物事をスピードアップする必要があります。すぐにそれらを必要としない場合、後でそれらを再びつけることができます。

別のアプローチは、テーブルにdeletedフラグ列を追加し、その値を考慮に入れるように他のクエリを調整することです。 mysqlの最速のブール型はCHAR(0) NULL(true = ''、false = NULL)です。これは高速な操作であり、後で値を削除できます。

SQLステートメントで表される同じ考え:

ALTER TABLE a ADD COLUMN deleted CHAR(0) NULL DEFAULT NULL;

-- The following query should be faster than the delete statement:
UPDATE a INNER JOIN b SET a.deleted = '';

-- This is the catch, you need to alter the rest
-- of your queries to take the new column into account:
SELECT * FROM a WHERE deleted IS NULL;

-- You can then issue the following queries in a cronjob
-- to clean up the tables:
DELETE FROM a WHERE deleted IS NOT NULL;

それもあなたが望むものではない場合、mysql docsが delete文の速度 について何を言っているかを見ることができます。

2
soulmerge

ところで、私のブログに上記を投稿した後、Perconaの Baron Schwartz は、彼の maatkit が既にこの目的のためのツールであるmk-archiverを持っていることに気付きました。 http://www.maatkit.org/doc/mk-archiver.html

それはおそらく、仕事に最適なツールです。

2

ターミナルを使用してデータベースを接続し、以下のコマンドを実行し、それぞれの結果時間を確認すると、delete 10、100、1000、10000、100000レコードの時間が乗算されていないことがわかります。

  DELETE FROM #{$table_name} WHERE id < 10;
  DELETE FROM #{$table_name} WHERE id < 100;
  DELETE FROM #{$table_name} WHERE id < 1000;
  DELETE FROM #{$table_name} WHERE id < 10000;
  DELETE FROM #{$table_name} WHERE id < 100000;

1万件のレコードを削除する時間は、10万件のレコードを削除する時間の10倍ではありません。次に、レコードをより速く削除する方法を見つけることを除いて、いくつかの間接的な方法があります。

1、table_nameの名前をtable_name_bakに変更してから、table_name_bakからtable_nameにレコードを選択できます。

2、10000レコードを削除するには、1000レコードを10回削除できます。例がありますRubyそれを行うスクリプト。

#!/usr/bin/env Ruby
require 'mysql2'


$client = Mysql2::Client.new(
  :as => :array,
  :Host => '10.0.0.250',
  :username => 'mysql',
  :password => '123456',
  :database => 'test'
)


$ids = (1..1000000).to_a
$table_name = "test"

until $ids.empty?
  ids = $ids.shift(1000).join(", ")
  puts "delete =================="
  $client.query("
                DELETE FROM #{$table_name}
                WHERE id IN ( #{ids} )
                ")
end
1
yanyingwang

明らかにSELECT操作の基盤を構築するDELETEクエリは非常に高速なので、クエリが非常に遅いのは外部キー制約またはインデックスが原因だと思います。

試して

SET foreign_key_checks = 0;
/* ... your query ... */
SET foreign_key_checks = 1;

これにより、外部キーのチェックが無効になります。残念ながら、InnoDBテーブルを使用してキー更新を無効にすることはできません(少なくとも私にはわかりません)。 MyISAMテーブルを使用すると、次のようなことができます

ALTER TABLE a DISABLE KEYS
/* ... your query ... */
ALTER TABLE a ENABLE KEYS 

これらの設定がクエリ期間に影響するかどうかは実際にはテストしませんでした。しかし、試してみる価値はあります。

1
Stefan Gehrig