文章目錄
- 相關文章
- 一. 前言
- 二. `Explain` 用途
- 三. `Explain`語法
- 四. 執行效果
- 五. Explain 字段詳解
- 5.1. 一覽全局字段
- 5.2. Id字段
- 5.3. `select_type`字段
- 5.3.1.類型如下
- 5.3.2. `SIMPLE `
- 5.3.2. `PRIMARY` 與 `SUBQUERY`
- 5.3.3. `DERIVED`
- 5.3.4. `UNION RESULT` 與`UNION`
- 5.4. `table`字段
- 5.5. `type`列
- 5.6. `possible_keys` 與 `key`列
- 5.7. `key_len`列
- 5.7.1 字符類型
- 5.7.2 字符類型-索引字段爲char類型+==不可爲Null時==
- 5.7.3 字符類型-索引字段爲char類型+==允許爲Null時==
- 5.7.4 索引字段爲varchar類型+==不可爲Null時==
- 5.7.5 索引字段爲varchar類型+==允許爲Null時==
- 5.7.6 數值類型
- 5.7.7 日期和時間
- 5.7.8 總結
- 5.8 `Ref` 列
- 5.8 `Rows` 列
- 5.9 `Extra` 列
- 案例腳本
相關文章
標題 | 鏈接 |
---|---|
SQL優化 實戰 | >>點擊查看<<正在編寫ing |
一. 前言
在日常開發過程中,由於時間緊迫,項目組成員水平不一,會導致很多的SQL執行很慢. 此時我們一般會使用Explain
命令來查看這些SQL語句的執行計劃,來分析SQL的執行過程.查看該SQL語句有沒有使用上索引,有沒有做全表掃描 等等. 我們深入的瞭解MySQl基於開銷的優化器,還可以獲得很多可能被優化器考慮到的訪問策略的細節,以及當運行SQL語句時哪種策略預計會被優化器採用。
使用Explain
關鍵字可以模擬優化器執行SQL語句,分析查詢語句或是結構的性能瓶頸。在select語句之前增加explaion關鍵字,MySQL會在查詢上設置一個標記,執行查詢會返回執行計劃的信息,而不是執行SQL。
二. Explain
用途
- 表的讀取順序如何
- 數據讀取操作有哪些操作類型
- 哪些索引可以使用
- 哪些索引被實際使用
- 表之間是如何引用
- 每張表有多少行被優化器查詢
… …
三. Explain
語法
explain SQL語句
例:
explain select * from user
四. 執行效果
4.1. 命令行
mysql> explain select * from userInfo where id = 1 \G
******************************************************
id: 1
select_type: SIMPLE
table: userInfo
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: const
rows: 1
filtered: 100.00
Extra: NULL
******************************************************
4.2. 可視化工具–HeidiSQL Portable 9.4
explain select * from user
五. Explain 字段詳解
5.1. 一覽全局字段
字段 | 解釋 |
---|---|
id | select查詢的序列號,包含一組數字,表示查詢中執行select子句或操作表的順序 |
select_type | 查詢類型 |
tab | 正在訪問哪個表 |
partitions | 匹配的分區 |
type | 訪問的類型 |
possible_keys | 顯示可能應用在這張表中的索引,一個或多個,但不一定實際使用到 |
key | 實際使用到的索引,如果爲NULL,則沒有使用索引 |
key_len | 表示索引中使用的字節數,可通過該列計算查詢中使用的索引的長度 |
ref | 顯示索引的哪一列被使用了,如果可能的話,是一個常數,哪些列或常量被用於查找索引列上的值 |
rows | 根據表統計信息及索引選用情況,大致估算出找到所需的記錄所需讀取的行數 |
filtered | 查詢的錶行佔表的百分比 |
Extra | 包含不適合在其它列中顯示但十分重要的額外信息 |
5.2. Id字段
- id列的編號是select的序列號,有幾個select就有幾個id,並且id的順序是按select出現的順序增長的。
- id越大執行優先級越高,id相同則從上往下執行,id爲NULL最後執行。
5.2.1. id相同
說明:
從上至下 順序執行
腳本:
explain
select t1.* , t2.* , t3.*
from actor t1 , film t2 , film_actor t3
where t3.film_id = t2.id and t3.actor_id = t1.id
執行結果:
解說:
如圖所示,ID列的值全爲1,代表執行順序從t1表開始加載,依次爲t3,t2
5.2.2. id不相同
說明:
如果是子查詢,id的序號會遞增,id值越大優先級越高,越先被執行
腳本:
explain
select t1.*
from actor t1
where t1.id = (select t2.actor_id from film_actor t2 where t2.id=3)
執行結果:
解說:
如上圖所示 , t2表的id值爲2 , 所以執行順序將先從t2表開始加載,然後再執行t1表
5.2.3. id相同,又不相同
說明:
- 將id不相同的部分可以分爲一組, id值越大,越限執行
- id 相同的一組 , 從上至下順序執行
腳本:
explain
select t1.* from actor t1 left join film_actor t2 on t1.id = t2.actor_id
union
select t1.* from actor t1 right join film_actor t2 on t1.id = t2.actor_id
執行結果:
解說:
- id如果相同,可以認爲是一組,從上往下順序執行;
- 在所有組中,id值越大,優先級越高,越先執行
5.3. select_type
字段
Select_type:查詢的類型,
要是用於區別:普通查詢、聯合查詢、子查詢等的複雜查詢
5.3.1.類型如下
類型 | 描述 |
---|---|
SIMPLE | 簡單的select查詢中不好喊子查詢或者UNION |
PRIMARY | 查詢中若包含任何複雜的子部分,最外層查詢則被標記爲 |
SUBQUERY | 在SELECT或WHERE列表中包含了子查詢 |
DERIVED(衍生) | 在FROM列表中包含的子查詢被標記爲DERIVED(衍生). MySQL會遞歸執行這些子查詢, 把結果放在臨時表裏。 |
UNION | 若第二個SELECT出現在UNION之後,則被標記爲UNION |
UNION RESULT | 從UNION表獲取結果的SELECT |
5.3.2. SIMPLE
說明:
簡單的select查詢中不好喊子查詢或者UNION
腳本:
explain
select t1.* from actor t1 ,film_actor t2 , film t3
where t1.id = t2.actor_id and t3.id = t2.film_id
執行結果:
5.3.2. PRIMARY
與 SUBQUERY
說明:
PRIMARY
: 查詢中若包含任何複雜的子部分,最外層查詢則被標記爲主查詢
SUBQUERY
: 在SELECT或WHERE列表中包含了子查詢
腳本:
explain
select t1.*
from actor t1
where t1.id = (select t2.actor_id from film_actor t2 where t2.id=3)
執行結果:
5.3.3. DERIVED
說明:
在FROM列表中包含的子查詢被標記爲DERIVED(衍生)
MySQL會遞歸執行這些子查詢, 把結果放在臨時表裏。
MySQL5.7+ 進行優化了,增加了derived_merge(派生合併),默認開啓,可加快查詢效率
5.3.4. UNION RESULT
與UNION
說明:
UNION
:若第二個SELECT出現在UNION
之後,則被標記爲UNION
;
UNION RESULT
:從UNION
表獲取結果的SELECT
腳本:
explain
select t1.* from actor t1 left join film_actor t2 on t1.id = t2.actor_id
union
select t1.* from actor t1 right join film_actor t2 on t1.id = t2.actor_id
執行結果:
5.4. table
字段
顯示這一行的數據是關於哪張表的
腳本:
explain
select * from actor
執行結果:
5.5. type
列
type顯示的是訪問類型,是較爲重要的一個指標,結果值從最好到最壞依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
需要記憶的
NULL>system>const>eq_ref>ref>range>index>ALL
一般來說,得保證查詢至少達到range級別,最好能達到ref。
5.5.1. System
與 const
說明:
System
:表只有一行記錄(等於系統表),這是const類型的特列,平時不會出現,這個也可以忽略不計
const
:表示通過索引一次就找到了. const
用於比較primary key
或者unique
索引。因爲只匹配一行數據,所以很快. 如將主鍵置於where列表中,MySQL就能將該查詢轉換爲一個常量
腳本:
explain
select * from (select * from actor t1 where t1.id = 1) s1 ;
執行結果:
5.5.2. eq_ref
說明:
唯一性索引掃描,對於每個索引鍵,表中只有一條記錄與之匹配。常見於主鍵或唯一索引掃描
腳本:
explain
select * from actor t1 left join film_actor t2 on t2.id = t1.id ;
執行結果:
5.5.3. ref
說明:
非唯一性索引掃描,返回匹配某個單獨值的所有行. 本質上也是一種索引訪問,它返回所有匹配某個單獨值的行,然而,它可能會找到多個符合條件的行,所以他應該屬於查找和掃描的混合體
表示的是聯合索引.
腳本:
explain
select count(distinct actor_id) from film_actor t1 where t1.actor_id = 1
執行結果:
5.5.4. Range
說明:
- 只檢索給定範圍的行,使用一個索引來選擇行。key 列顯示使用了哪個索引
- 一般就是在你的where語句中出現了between、<、>、in等的查詢
- 這種範圍掃描索引掃描比全表掃描要好,因爲它只需要開始於索引的某一點,而結束語另一點,不用掃描全部索引。
腳本:
explain
select t1.* from actor t1 where t1.id between 1 and 3
執行結果:
5.6. possible_keys
與 key
列
possible_keys
:可能使用的key
Key
:實際使用的索引。如果爲NULL,則沒有使用索引
查詢中若使用了覆蓋索引
,則該索引和查詢的select字段重疊
這裏的覆蓋索引非常重要,後面會單獨的來講
解說:
其中key和possible_keys都可以出現null的情況(結婚邀請朋友的例子)
- 結婚時,邀請了自己認爲回來的朋友—> possible_keys
- 實際到場的朋友---->key
5.7. key_len
列
說明:
Key_len
表示索引中使用的字節數,可通過該列計算查詢中使用的索引的長度。在不損失精確性的情況下,長度越短越好
key_len
顯示的值爲索引字段的最大可能長度,並非實際使用長度,即key_len
是根據表定義計算而得,不是通過表內檢索出的
注意:
更具底層使用的不同存儲引擎,受影響的行數這個指標可能是一個估計值,也可能是一個精確值. 即使受影響的行數是一個估計值(例如 當使用InnoDB存儲引擎管理表存儲時),通常情況下這個估計只是一個足以使優化器租出一個有充分依據 的決定
- key_len表示索引使用的字節數,
- 根據這個值,就可以判斷索引使用情況,特別是在組合索引的時候,判斷所有的索引字段是否都被查詢用到。
- char和varchar跟字符編碼也有密切的聯繫,
- latin1佔用1個字節,gbk佔用2個字節,utf8佔用3個字節。(不同字符編碼佔用的存儲空間不同)
5.7.1 字符類型
以上這個表列出了所有字符類型,但真正建所有的類型常用情況只是CHAR、VARCHAR
5.7.2 字符類型-索引字段爲char類型+不可爲Null時
腳本:
CREATE TABLE `s1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(10) NOT NULL,
`addr` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
explain select * from s1 where name='enjoy';
執行結果:
解說:
name這一列爲char(10),字符集爲utf-8佔用3個字節
Keylen=10*3
5.7.3 字符類型-索引字段爲char類型+允許爲Null時
腳本:
CREATE TABLE `s2` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(10) DEFAULT NULL,
`addr` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
explain select * from s2 where name='enjoyedu';
執行結果:
解說:
name這一列爲char(10),字符集爲utf-8佔用3個字節,外加需要存入一個null值
Keylen=10*3+1(null) 結果爲31
結果允許爲null時,需要+1
5.7.4 索引字段爲varchar類型+不可爲Null時
腳本:
CREATE TABLE `s3` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL,
`addr` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
explain select * from s3 where name='enjoyeud';
執行結果:
解說:
Keylen=varchar(n)變長字段+不允許Null=n*(utf8=3,gbk=2,latin1=1)+2
5.7.5 索引字段爲varchar類型+允許爲Null時
腳本:
CREATE TABLE `s4` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL,
`addr` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
explain select * from s4 where name='enjoyeud';
執行結果:
解說:
Keylen=varchar(n)變長字段+允許Null=n*(utf8=3,gbk=2,latin1=1)+1(NULL)+2
5.7.6 數值類型
CREATE TABLE `numberKeyLen ` (
`c0` int(255) NOT NULL ,
`c1` tinyint(255) NULL DEFAULT NULL ,
`c2` smallint(255) NULL DEFAULT NULL ,
`c3` mediumint(255) NULL DEFAULT NULL ,
`c4` int(255) NULL DEFAULT NULL ,
`c5` bigint(255) NULL DEFAULT NULL ,
`c6` float(255,0) NULL DEFAULT NULL ,
`c7` double(255,0) NULL DEFAULT NULL ,
PRIMARY KEY (`c0`),
INDEX `index_tinyint` (`c1`) USING BTREE ,
INDEX `index_smallint` (`c2`) USING BTREE ,
INDEX `index_mediumint` (`c3`) USING BTREE ,
INDEX `index_int` (`c4`) USING BTREE ,
INDEX `index_bigint` (`c5`) USING BTREE ,
INDEX `index_float` (`c6`) USING BTREE ,
INDEX `index_double` (`c7`) USING BTREE
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
ROW_FORMAT=COMPACT
;
EXPLAIN
select * from numberKeyLen where c1=1
EXPLAIN
select * from numberKeyLen where c2=1
EXPLAIN
select * from numberKeyLen where c3=1
EXPLAIN
select * from numberKeyLen where c4=1
EXPLAIN
select * from numberKeyLen where c5=1
EXPLAIN
select * from numberKeyLen where c6=1
EXPLAIN
select * from numberKeyLen where c7=1
5.7.7 日期和時間
datetime類型在5.6中字段長度是5個字節
datetime類型在5.5中字段長度是8個字節
CREATE TABLE `datatimekeylen ` (
`c1` date NULL DEFAULT NULL ,
`c2` time NULL DEFAULT NULL ,
`c3` year NULL DEFAULT NULL ,
`c4` datetime NULL DEFAULT NULL ,
`c5` timestamp NULL DEFAULT NULL ,
INDEX `index_date` (`c1`) USING BTREE ,
INDEX `index_time` (`c2`) USING BTREE ,
INDEX `index_year` (`c3`) USING BTREE ,
INDEX `index_datetime` (`c4`) USING BTREE ,
INDEX `index_timestamp` (`c5`) USING BTREE
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
ROW_FORMAT=COMPACT
;
EXPLAIN
SELECT * from datatimekeylen where c1 = 1
EXPLAIN
SELECT * from datatimekeylen where c2 = 1
EXPLAIN
SELECT * from datatimekeylen where c3 = 1
EXPLAIN
SELECT * from datatimekeylen where c4 = 1
EXPLAIN
SELECT * from datatimekeylen where c5 = 1
5.7.8 總結
5.7.8.1 字符類型
- 變長字段需要額外的2個字節(VARCHAR值保存時只保存需要的字符數,另加一個字節來記錄長度(如果列聲明的長度超過255,則使用兩個字節),所以VARCAHR索引長度計算時候要加2),固定長度字段不需要額外的字節。
- 而NULL都需要1個字節的額外空間,所以索引字段最好不要爲NULL,因爲NULL讓統計更加複雜並且需要額外的存儲空間。
- 複合索引有最左前綴的特性,如果複合索引能全部使用上,則是複合索引字段的索引長度之和,這也可以用來判定複合索引是否部分使用,還是全部使用。
5.6.7.2 整數/浮點數/時間類型的索引長度
NOT NULL=字段本身的字段長度
NULL=字段本身的字段長度+1(因爲需要有是否爲空的標記,這個標記需要佔用1個字節)
datetime類型在5.6中字段長度是5個字節,datetime類型在5.5中字段長度是8個字節
5.8 Ref
列
說明:
顯示索引的哪一列被使用了,如果可能的話,是一個常數。哪些列或常量被用於查找索引列上的值
腳本:
explain
select * from actor t1 , film_actor t2 where t1.id = t2.actor_id and t2.film_id=2
執行結果:
解說:
由key_len
可知t1表的actor_id
被充分使用,
actor_id
匹配t2表的id
,film_id
匹配了一個常量,即 1
其中 【mysql_explain.t2.actor_id】 爲 【數據庫.表.列】
5.8 Rows
列
說明:
根據表統計信息及索引選用情況,大致估算出找到所需的記錄所需要讀取的行數
腳本:
explain
select * from actor t1 , film_actor t2 where t1.id = t2.actor_id and t2.film_id=2
執行結果:
5.9 Extra
列
包含不適合在其它列中顯示但十分重要的額外信息
5.9.1 選項描述
值 | 描述 |
---|---|
Using filesort | 說明mysql會對數據使用一個外部的索引排序,而不是按照表內的索引順序進行讀取。MySQL中無法利用索引完成的排序操作稱爲"文件排序" |
Using temporary | 使了用臨時表保存中間結果,MySQL在對查詢結果排序時使用臨時表。常見於排序 order by 和分組查詢 group by。 |
Using index | 表示相應的select操作中使用了覆蓋索引(Covering Index),避免訪問了表的數據行,效率不錯! 如果同時出現using where,表明索引被用來執行索引鍵值的查找; 如果沒有同時出現using where,表明索引用來讀取數據而非執行查找動作 |
Using where | 使用了where條件 |
Using join buffer | 使用了連接緩存 |
impossible where | where子句的值總是false,不能用來獲取任何元素 |
distinct | 一單mysql找到了與形相聯合匹配的行,就不在搜索了 |
說明:
- 由於Markdown 編輯格式問題, 描述不是很全
- 在下面各個值說明中,比較完整
5.9.2 Using filesort
說明:
說明mysql會對數據使用一個外部的索引排序,而不是按照表內的索引順序進行讀取。
MySQL中無法利用索引完成的排序操作稱爲“文件排序”
當發現有Using filesort 後,實際上就是發現了可以優化的地方
腳本:
explain
select * from film_actor t1 where t1.film_id=1 order by t1.actor_id
執行結果:
解說:
上圖其實是一種索引失效的情況,後面會講,可以看出查詢中用到了個聯合索引,索引分別爲actor_id,film_id
5.9.3 Using temporary
說明:
使用了臨時表保存中間結果,MySQL在對結果排序時使用臨時表,常見於排序order by 和分組查詢group by
腳本:
explain
select * from film_actor t1 where t1.film_id=1 group by t1.remark
執行結果:
解說:
尤其發現在執行計劃裏面有using filesort而且還有Using temporary的時候,特別需要注意
5.9.3 Using index
說明:
表示相應的select操作中使用了覆蓋索引(Covering Index),避免訪問了表的數據行,效率不錯!
如果同時出現using where,表明索引被用來執行索引鍵值的查找
腳本:
explain
select * from film t1 where t1.name="我的快樂購"
執行結果:
如果沒有同時出現using where,表明索引用來讀取數據而非執行查找動作
腳本:
explain
select * from film t1
執行結果:
5.9.3.1 覆蓋索引
- 覆蓋索引(Covering Index),一說爲索引覆蓋。
- 理解方式一:就是select的數據列只用從索引中就能夠取得,不必讀取數據行,MySQL可以利用索引返回select列表中的字段,而不必根據索引再次讀取數據文件,換句話說查詢列要被所建的索引覆蓋。
- 理解方式二:索引是高效找到行的一個方法,但是一般數據庫也能使用索引找到一個列的數據,因此它不必讀取整個行。畢竟索引葉子節點存儲了它們索引的數據;當能通過讀取索引就可以得到想要的數據,那就不需要讀取行了。一個索引包含了(或覆蓋了)滿足查詢結果的數據就叫做覆蓋索引
注意:
如果要使用覆蓋索引,一定要注意select列表中只取出需要的列,不可select *,
因爲如果將所有字段一起做索引會導致索引文件過大,查詢性能下降。所以,千萬不能爲了查詢而在所有列上都建立索引,會嚴重影響修改維護的性能。
5.9.4 Using where
與 using join buffer
說明:
Using where
:表明使用where過濾
using join buffer
:使用了連接緩存
很容易理解,不做案例演示
腳本:
-- 查詢MySQl 默認的join_buffer_size
show VARIABLES like '%join_buffer_size%'
執行結果:
5.9.5 impossible where
說明:
where子句的值總是false,不能用來獲取任何元組
腳本:
-- where 條件的字句總是爲false
explain
select * from film t1 where 1=2
--
explain
select * from film t1 where t1.name="隨機填入" and t1.name="隨機填入"
執行結果:
說明:
一旦mysql找到了與行相聯合匹配的行,就不再搜索了
腳本:
explain
select distinct t1.id from actor t1 ,film_actor t2 , film t3 where t1.id = t2.actor_id and t2.film_id = t3.id
執行結果:
解說:
5.9.7 Select tables optimized away
說明:
SELECT操作已經優化到不能再優化了(MySQL根本沒有遍歷表或索引就返回數據了)
腳本:
explain
select min(t1.id) from actor t1
執行結果:
案例腳本
1. 建表腳本
-- actor建表語句:
CREATE TABLE `actor` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
-- film建表語句:
CREATE TABLE `film` (
`id` int(11) NOT NULL,
`name` varchar(10) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
-- film_actor建表語句:
CREATE TABLE `film_actor` (
`id` int(11) NOT NULL,
`film_id` int(11) NOT NULL,
`actor_id` int(11) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2. 插入數據sql
-- actor 表
INSERT INTO `actor` (`id`, `name`, `update_time`) VALUES (3, '麗麗', '2020-02-28 22:50:57');
INSERT INTO `actor` (`id`, `name`, `update_time`) VALUES (1, '媛媛', '2020-02-28 22:49:53');
INSERT INTO `actor` (`id`, `name`, `update_time`) VALUES (2, '銳銳', '2020-02-28 22:50:44');
-- film 表
INSERT INTO `film` (`id`, `name`) VALUES (3, '授課老師看到');
INSERT INTO `film` (`id`, `name`) VALUES (1, '我的快樂購');
INSERT INTO `film` (`id`, `name`) VALUES (2, '哈哈杜拉拉');
-- film_actor 表
INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`, `remark`) VALUES (5, 3, 2, NULL);
INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`, `remark`) VALUES (6, 3, 2, NULL);
INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`, `remark`) VALUES (2, 2, 1, NULL);
INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`, `remark`) VALUES (4, 2, 3, NULL);
INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`, `remark`) VALUES (1, 1, 1, NULL);
INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`, `remark`) VALUES (3, 1, 3, NULL);