「使用しないSELECT *
」は、MySQLで使用される黄金のルールになりつつあります。「AliJava開発マニュアル」でさえ、*
クエリフィールドリストとして使用すべきではないと明確に述べているため、このルールはより信頼できるものになります。
SELECT *
ただし、次の2つの理由から、開発プロセスで直接使用しています。
- その単純さのために、開発効率は非常に高く、フィールドが後で頻繁に追加または変更される場合は、SQLステートメントを変更する必要はありません。
- 最終的に実際に必要なフィールドを最初に決定し、それらに適切なインデックスを作成できない限り、時期尚早に最適化するのは悪い習慣だと思います。そうでない場合は、問題が発生したときにSQLを最適化することを選択します。もちろん、トラブルは致命的ではないという前提があります。
ただし、直接使用することが推奨されない理由を常に知っておく必要がありますSELECT *
。この記事では、4つの側面から理由を説明します。
1.不要なディスクI/O
MySQLは基本的にユーザーレコードをディスクに保存するため、クエリ操作はディスクIOの動作です(クエリ対象のレコードがメモリにキャッシュされていない場合)。
クエリされるフィールドが多いほど、読み取られるコンテンツが多くなり、ディスクIOのオーバーヘッドが増加します。特に、一部のフィールドがタイプTEXT
、MEDIUMTEXT
またはBLOB
などの場合、その影響は特に明白です。
SELECT *
MySQLの使用はより多くのメモリを消費しますか?
理論的にはそうではありません。サーバー層の場合、完全な結果セットをメモリに格納して一度にクライアントに渡す代わりに、ストレージエンジンから行が取得されるたびに、次のようにnet_buffer
呼ばれるメモリスペースに書き込まれます。このメモリはシステム変数によって制御されnet_buffer_length
、デフォルトは16KBです。net_buffer
いっぱいになると、ローカルネットワークスタックのメモリスペースにデータを書き込みsocket send buffer
、クライアントに送信します。送信が成功した後(クライアントの読み取りが完了した後) 、空net_buffer
になり、読み取りを続行します。次の行と書き込み。
つまり、デフォルトでは、結果セットが占める最大メモリスペースはnet_buffer_length
サイズのみであり、フィールドが増えるため、追加のメモリスペースを占有することはありません。
2.ネットワーク遅延を増やす
前のポイントを続けるとsocket send buffer
、データは毎回クライアントに送信されますが、一度にデータ量は多くないようですが、誰かが使用したことは耐えられません*またはタイプTEXT
のフィールドも見つかります、また、データの総量が多いため、ネットワークの送信回数が直接増加します。MEDIUMTEXT
BLOB
MySQLとアプリケーションが同じマシン上にない場合、このオーバーヘッドは非常に顕著です。MySQLサーバーとクライアントが同じマシン上にあり、使用されるプロトコルがTCPである場合でも、通信には余分な時間がかかります。
3.カバーインデックスを使用できません
これを説明するために、テーブルを作成する必要があります
CREATE TABLE `user_innodb` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`gender` tinyint(1) DEFAULT NULL,
`phone` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `IDX_NAME_PHONE` (`name`,`phone`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
复制代码
ストレージエンジンがInnoDBであるテーブルを作成し、それを主キーとしてuser_innodb
設定し、とのジョイントインデックスを作成し、最後に500W以上のデータをランダムにテーブルに初期化しました。id
name
phone
id
InnoDBは、主キーの主キーインデックス(クラスター化インデックスとも呼ばれます)と呼ばれるB +ツリーを自動的に作成します。このB+ツリーの最も重要な機能は、リーフノードに次のような完全なユーザーレコードが含まれていることです。
このステートメントを実行すると
SELECT * FROM user_innodb WHERE name = '蝉沐风';
复制代码
EXPLAIN
ステートメントの実行プランを表示するために使用します。
IDX_NAME_PHONE
このSQLステートメントは、セカンダリインデックスであるインデックスを使用することがわかります。セカンダリインデックスのリーフノードは次のようになります。
InnoDBストレージエンジンは、検索条件に従ってセカンダリインデックスのリーフノードでレコードを検索name
します蝉沐风
が、レコードname
とphone
プライマリキーid
フィールドのみがセカンダリインデックス(使用するように指示されたSELECT *
)に記録されるため、InnoDBは次のことを行う必要があります。主キーid
を使用して、主キーインデックスでこの完全なレコードを検索することをリターンテーブルと呼びます。
想一下,如果二级索引的叶子节点上有我们想要的所有数据,是不是就不需要回表了呢?是的,这就是覆盖索引。
举个例子,我们恰好只想搜索name
、phone
以及主键字段。
SELECT id, name, phone FROM user_innodb WHERE name = "蝉沐风";
复制代码
使用EXPLAIN
查看一下语句的执行计划:
可以看到Extra一列显示Using index
,表示我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是使用了覆盖索引,能够直接摒弃回表操作,大幅度提高查询效率。
4. 可能拖慢JOIN连接查询
我们创建两张表t1
,t2
进行连接操作来说明接下来的问题,并向t1
表中插入了100条数据,向t2
中插入了1000条数据。
CREATE TABLE `t1` (
`id` int NOT NULL,
`m` int DEFAULT NULL,
`n` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT;
CREATE TABLE `t2` (
`id` int NOT NULL,
`m` int DEFAULT NULL,
`n` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT;
复制代码
如果我们执行下面这条语句
SELECT * FROM t1 STRAIGHT_JOIN t2 ON t1.m = t2.m;
复制代码
这里我使用了STRAIGHT_JOIN强制令
t1
表作为驱动表,t2
表作为被驱动表
对于连接查询而言,驱动表只会被访问一遍,而被驱动表却要被访问好多遍,具体的访问次数取决于驱动表中符合查询记录的记录条数。由于已经强制确定了驱动表和被驱动表,下面我们说一下两表连接的本质:
t1
作为驱动表,针对驱动表的过滤条件,执行对t1
表的查询。因为没有过滤条件,也就是获取t1
表的所有数据;- 对上一步中获取到的结果集中的每一条记录,都分别到被驱动表中,根据连接过滤条件查找匹配记录
用伪代码表示的话整个过程是这样的:
// t1Res是针对驱动表t1过滤之后的结果集
for (t1Row : t1Res){
// t2是完整的被驱动表
for(t2Row : t2){
if (满足join条件 && 满足t2的过滤条件){
发送给客户端
}
}
}
复制代码
这种方法最简单,但同时性能也是最差,这种方式叫做嵌套循环连接
(Nested-LoopJoin,NLJ)。怎么加快连接速度呢?
其中一个办法就是创建索引,最好是在被驱动表(t2
)连接条件涉及到的字段上创建索引,毕竟被驱动表需要被查询好多次,而且对被驱动表的访问本质上就是个单表查询而已(因为t1
结果集定了,每次连接t2
的查询条件也就定死了)。
既然使用了索引,为了避免重蹈无法使用覆盖索引的覆辙,我们也应该尽量不要直接SELECT *
,而是将真正用到的字段作为查询列,并为其建立适当的索引。
但是如果我们不使用索引,MySQL就真的按照嵌套循环查询的方式进行连接查询吗?当然不是,毕竟这种嵌套循环查询实在是太慢了!
在MySQL8.0之前,MySQL提供了基于块的嵌套循环连接
(Block Nested-Loop Join,BLJ)方法,MySQL8.0又推出了hash join
方法,这两种方法都是为了解决一个问题而提出的,那就是尽量减少被驱动表的访问次数。
这两种方法都用到了一个叫做join buffer
的固定大小的内存区域,其中存储着若干条驱动表结果集中的记录(这两种方法的区别就是存储的形式不同而已),如此一来,把被驱动表的记录加载到内存的时候,一次性和join buffer
中多条驱动表中的记录做匹配,因为匹配的过程都是在内存中完成的,所以这样可以显著减少被驱动表的I/O代价,大大减少了重复从磁盘上加载被驱动表的代价。使用join buffer
的过程如下图所示:
我们看一下上面的连接查询的执行计划,发现确实使用到了hash join
(前提是没有为t2
表的连接查询字段创建索引,否则就会使用索引,不会使用join buffer
)。
最良の場合、join buffer
駆動テーブルの結果セットにすべてのレコードを保持するのに十分な大きさであるため、結合操作を完了するために駆動テーブルに1回アクセスするだけで済みます。このシステム変数を使用join_buffer_size
して構成できます。デフォルトのサイズは256KB
です。それでもロードできない場合は、ドライバテーブルの結果セットをバッチで配置します。メモリ内の比較が完了したら、接続が完了するまで結果セットの次のバッチをjoin buffer
空にしてロードします。join buffer
ここにポイントがあります!ドライブテーブルレコードのすべての列が入力されるわけではjoin buffer
なく、クエリリストの列とフィルター条件の列のみが入力されるため、クエリリストとしてjoin buffer
使用しないことをお勧めします。*
気になる列をクエリリストにjoin buffer
入れるだけで、より多くのレコードを入れることができ、バッチの数を減らし、当然、ドリブンテーブルへのアクセス数を減らすことができます。