MaxCompute支持通過insert into
或insert overwrite
操作向目標表或靜態分區中插入、覆寫數據。
本文中的命令您可以在如下工具平臺執行:
前提條件
執行insert into
和insert overwrite
操作前需要具備目標表的更新權限(Update)及源表的元信息讀取權限(Select)。授權操作請參見MaxCompute權限。
功能介紹
在使用MaxCompute SQL處理數據時,insert into
或insert overwrite
操作可以將select
查詢的結果保存至目標表中。二者的區別是:
insert into
:直接向表或靜態分區中插入數據。您可以在insert
語句中直接指定分區值,將數據插入指定的分區。如果您需要插入少量測試數據,可以配合VALUES使用。insert overwrite
:先清空表或靜態分區中的原有數據,再向表或靜態分區中插入數據。說明MaxCompute的
insert
語法與通常使用的MySQL或Oracle的insert
語法有差別。在insert overwrite
后需要加table
關鍵字,非直接使用table_name
。insert into
可以省略table
關鍵字。在反復對同一個分區執行
insert overwrite
操作時,您通過desc
命令查看到的數據分區Size會不同。這是因為從同一個表的同一個分區select
出來再insert overwrite
回相同分區時,文件切分邏輯發生變化,從而導致數據的Size發生變化。數據的總長度在insert overwrite
前后是不變的,您不必擔心存儲計費會產生問題。并發寫入場景,MaxCompute會根據ACID保障并發寫入操作。關于ACID的具體語義,請參見ACID語義。
向動態分區插入數據的操作請參見插入或覆寫動態分區數據(DYNAMIC PARTITION)。
使用限制
執行
insert into
和insert overwrite
操作更新表或靜態分區數據的使用限制如下:insert into
:不支持向聚簇表中追加數據。insert overwrite
:不支持指定插入列,只能使用insert into
。例如create table t(a string, b string); insert into t(a) values ('1');
,a列插入1,b列為NULL或默認值。MaxCompute對正在操作的表沒有鎖機制,不要同時對一個表執行
insert into
或insert overwrite
操作。
對于Delta Table類型的表有如下限制。
Delta Table表用
Insert Overwrite
寫入數據時,相同PK值的多行記錄在寫入表之前會先去重,只選擇第一行寫入,最終寫入的結果依賴于計算過程的記錄順序,無法手動指定。由于該操作寫入的是全量數據,因此默認去重也是盡可能保證PK唯一性的屬性。Delta Table表用
Insert Into
寫入數據時,相同PK值的多行默認不去重,都會寫入表中,但如果設置Flag(odps.sql.insert.acidtable.deduplicate.enable
)的值為true,則會去重后再寫入表中。
命令格式
insert {into|overwrite} table <table_name> [partition (<pt_spec>)] [(<col_name> [,<col_name> ...)]]
<select_statement>
from <from_statement>
[zorder by <zcol_name> [, <zcol_name> ...]];
table_name:必填。需要插入數據的目標表名稱。
pt_spec:可選。需要插入數據的分區信息,不允許使用函數等表達式,只能是常量。格式為
(partition_col1 = partition_col_value1, partition_col2 = partition_col_value2, ...)
。col_name:可選。需要插入數據的目標表的列名稱。
insert overwrite
不支持指定[(<col_name> [,<col_name> ...)]
。select_statement:必填。
select
子句,從源表中查詢需要插入目標表的數據。更多select
信息,請參見SELECT語法。說明源表與目標表的對應關系依賴于
select
子句中列的順序,而不是表與表之間列名的對應關系。如果目標表是靜態分區,向某個分區插入數據時,分區列不允許出現在
select
子句中。
from_statement:必填。
from
子句,表示數據來源。例如,源表名稱。zorder by <zcol_name> [, <zcol_name> ...]:可選。向表或分區寫入數據時,支持根據指定的一列或多列(select_statement對應表中的列),把排序列數據相近的行排列在一起,提升查詢時的過濾性能,在一定程度上降低存儲成本。需要注意的是,
order by x, y
會嚴格地按照先x后y的順序對數據進行排序,zorder by x, y
會把相近的<x, y>盡量排列在一起。當SQL查詢語句的過濾條件中包含排序列時,order by
后的數據僅對包含x的表達式有較好的過濾效果,zorder by
后的數據對包含x或同時包含x、y的表達式均有較好的過濾效果,列壓縮比例更高。
zorder by
有兩種模式,默認模式為local zorder
。local模式只是單個文件內部按照zorder排序,并不是對全局數據做一個重分布,所以如果數據分散在各個文件,那么數據的聚集程度可能也不高,無法做到最有效的Data Skipping。鑒于該問題,在新版本中支持了global zorder
。local zorder。
global zorder:如果使用
global zorder
模式,需要增加參數set odps.sql.default.zorder.type=global;
。
sort by
語句用于指定單個文件內部排序的方式,如果不寫sort by
,則單個文件內部按照local zorder
排序。zorder by
的使用限制如下:對于分區表,一次只允許對1個分區進行
zorder by
排序。zorder by
字段數目只能在2~4之間。
目標表為聚簇表時,不支持
zorder by
子句。zorder by
可以與distribute by
一起使用,不能與order by
、cluster by
或sort by
一起使用。
說明使用
zorder by
子句寫入數據時,會占用較多資源,比不排序花費時間更多。
使用示例:普通表
示例1:執行
insert into
命令向非分區表websites
中追加數據。命令示例如下:--創建一張非分區表websites。 create table if not exists websites (id int, name string, url string ); --創建一張非分區表apps create table if not exists apps (id int, app_name string, url string ); --向表apps追加數據。其中:insert into table table_name可以簡寫為insert into table_name insert into apps (id,app_name,url) values (1,'Aliyun','https://www.aliyun.com'); --復制apps的表數據追加至websites表 insert into websites (id,name,url) select id,app_name,url from apps; --執行select語句查看表websites中的數據。 select * from websites; --返回結果。 +------------+------------+------------+ | id | name | url | +------------+------------+------------+ | 1 | Aliyun | https://www.aliyun.com | +------------+------------+------------+
示例2:執行
insert into
命令向分區表sale_detail
中追加數據。命令示例如下:--創建一張分區表sale_detail。 create table if not exists sale_detail ( shop_name string, customer_id string, total_price double ) partitioned by (sale_date string, region string); --向源表增加分區。非必需操作,如果不提前創建,寫入時會自動創建該分區。 alter table sale_detail add partition (sale_date='2013', region='china'); --向源表追加數據。其中:insert into table table_name可以簡寫為insert into table_name,但insert overwrite table table_name不可以省略table關鍵字。 insert into sale_detail partition (sale_date='2013', region='china') values ('s1','c1',100.1),('s2','c2',100.2),('s3','c3',100.3); --開啟全表掃描,僅此Session有效。執行select語句查看表sale_detail中的數據。 set odps.sql.allow.fullscan=true; select * from sale_detail; --返回結果。 +------------+-------------+-------------+------------+------------+ | shop_name | customer_id | total_price | sale_date | region | +------------+-------------+-------------+------------+------------+ | s1 | c1 | 100.1 | 2013 | china | | s2 | c2 | 100.2 | 2013 | china | | s3 | c3 | 100.3 | 2013 | china | +------------+-------------+-------------+------------+------------+
示例3:執行
insert overwrite
命令向表sale_detail_insert
中覆寫數據。命令示例如下:--創建目標表sale_detail_insert,與sale_detail有相同的結構。 create table sale_detail_insert like sale_detail; --給目標表增加分區。非必需操作,如果不提前創建,寫入時會自動創建該分區。 alter table sale_detail_insert add partition (sale_date='2013', region='china'); --從源表sale_detail中取出數據插入目標表sale_detail_insert。注意不需要聲明目標表字段,也不支持重排目標表字段順序。 --對于靜態分區目標表,分區字段賦值已經在partition()部分聲明,不需要在select_statement中包含,只要按照目標表普通列順序查出對應字段,按順序映射到目標表即可。動態分區表則需要在select中包含分區字段,詳情請參見插入或覆寫動態分區數據(DYNAMIC PARTITION)。 insert overwrite table sale_detail_insert partition (sale_date='2013', region='china') select shop_name, customer_id, total_price from sale_detail zorder by customer_id, total_price; --開啟全表掃描,僅此Session有效。執行select語句查看表sale_detail_insert中的數據。 set odps.sql.allow.fullscan=true; select * from sale_detail_insert; --返回結果。 +------------+-------------+-------------+------------+------------+ | shop_name | customer_id | total_price | sale_date | region | +------------+-------------+-------------+------------+------------+ | s1 | c1 | 100.1 | 2013 | china | | s2 | c2 | 100.2 | 2013 | china | | s3 | c3 | 100.3 | 2013 | china | +------------+-------------+-------------+------------+------------+
示例4:執行
insert overwrite
命令向表sale_detail_insert
中覆寫數據,調整select
子句中列的順序。源表與目標表的對應關系依賴于select
子句中列的順序,而不是表與表之間列名的對應關系。命令示例如下:insert overwrite table sale_detail_insert partition (sale_date='2013', region='china') select customer_id, shop_name, total_price from sale_detail; set odps.sql.allow.fullscan=true; select * from sale_detail_insert;
返回結果如下:
+------------+-------------+-------------+------------+------------+ | shop_name | customer_id | total_price | sale_date | region | +------------+-------------+-------------+------------+------------+ | c1 | s1 | 100.1 | 2013 | china | | c2 | s2 | 100.2 | 2013 | china | | c3 | s3 | 100.3 | 2013 | china | +------------+-------------+-------------+------------+------------+
在創建
sale_detail_insert
表時,列的順序為shop_name string、customer_id string、total_price bigint
,而從sale_detail
向sale_detail_insert
插入數據的順序為customer_id、shop_name、total_price
。此時,會將sale_detail.customer_id
的數據插入sale_detail_insert.shop_name
,將sale_detail.shop_name
的數據插入sale_detail_insert.customer_id
。示例5:向某個分區插入數據時,分區列不允許出現在
select
子句中。如下語句會返回報錯,sale_date
和region
為分區列,不允許出現在靜態分區的select
子句中。錯誤命令示例如下:insert overwrite table sale_detail_insert partition (sale_date='2013', region='china') select shop_name, customer_id, total_price, sale_date, region from sale_detail;
示例6:
partition
的值只能是常量,不可以為表達式。錯誤命令示例如下:insert overwrite table sale_detail_insert partition (sale_date=datepart('2016-09-18 01:10:00', 'yyyy') , region='china') select shop_name, customer_id, total_price from sale_detail;
示例7:執行
insert overwrite
命令向表mf_src
和表mf_zorder_src
中覆寫數據,并使用global zorder模式對表mf_zorder_src
進行排序。命令示例如下:--創建目標表mf_src。 create table mf_src (key string, value string); insert overwrite table mf_src select a, b from values ('1', '1'),('3', '3'),('2', '2') as t(a, b); select * from mf_src; --返回結果 +-----+-------+ | key | value | +-----+-------+ | 1 | 1 | | 3 | 3 | | 2 | 2 | +-----+-------+ --創建目標表mf_zorder_src,與mf_src有相同的結構。 create table mf_zorder_src like mf_src; --使用global zorder模式排序。 set odps.sql.default.zorder.type=global; insert overwrite table mf_zorder_src select key, value from mf_src zorder by key, value; select * from mf_zorder_src; --返回結果 +-----+-------+ | key | value | +-----+-------+ | 1 | 1 | | 2 | 2 | | 3 | 3 | +-----+-------+
示例8:執行
insert overwrite
命令覆寫存量表target
數據。命令示例如下:-- target表是存量表 set odps.sql.default.zorder.type=global; insert overwrite table target select key, value from target zorder by key, value;
使用示例:Delta Table類型表
示例:創建Delta Table表mf_dt,并執行insert
命令插入并覆寫數據。
--創建Delta Table表mf_dt。
create table if not exists mf_dt (pk bigint not null primary key,
val bigint not null)
partitioned by (dd string, hh string)
tblproperties ("transactional"="true");
--向mf_dt表dd='01'和hh='01'的分區中插入測試數據。
insert overwrite table mf_dt partition (dd='01', hh='01')
values (1, 1), (2, 2), (3, 3);
--查詢mf_dt表目標分區中的數據
select * from mf_dt where dd='01' and hh='01';
--返回結果
+------------+------------+----+----+
| pk | val | dd | hh |
+------------+------------+----+----+
| 1 | 1 | 01 | 01 |
| 3 | 3 | 01 | 01 |
| 2 | 2 | 01 | 01 |
+------------+------------+----+----+
--使用insert into向mf_dt表目標分區中追加數據。
insert into table mf_dt partition(dd='01', hh='01')
values (3, 30), (4, 4), (5, 5);
select * from mf_dt where dd='01' and hh='01';
--返回結果
+------------+------------+----+----+
| pk | val | dd | hh |
+------------+------------+----+----+
| 1 | 1 | 01 | 01 |
| 3 | 30 | 01 | 01 |
| 4 | 4 | 01 | 01 |
| 5 | 5 | 01 | 01 |
| 2 | 2 | 01 | 01 |
+------------+------------+----+----+
--使用insert overwrite向mf_dt表目標分區的覆蓋寫入數據。
insert overwrite table mf_dt partition (dd='01', hh='01')
values (1, 1), (2, 2), (3, 3);
select * from mf_dt where dd='01' and hh='02';
--返回結果。
+------------+------------+----+----+
| pk | val | dd | hh |
+------------+------------+----+----+
| 1 | 1 | 01 | 01 |
| 3 | 3 | 01 | 01 |
| 2 | 2 | 01 | 01 |
+------------+------------+----+----+
--使用insert into向mf_dt表dd='01'和hh='02'的分區寫入數據。
insert overwrite table mf_dt partition (dd='01', hh='02')
values (1, 11), (2, 22), (3, 32);
select * from mf_dt where dd='01' and hh='02';
--返回結果。
+------------+------------+----+----+
| pk | val | dd | hh |
+------------+------------+----+----+
| 1 | 11 | 01 | 02 |
| 3 | 32 | 01 | 02 |
| 2 | 22 | 01 | 02 |
+------------+------------+----+----+
--開啟全表掃描,僅此Session有效。執行select語句查看表mf_dt中的數據。
set odps.sql.allow.fullscan=true;
select * from mf_dt;
--返回結果。
+------------+------------+----+----+
| pk | val | dd | hh |
+------------+------------+----+----+
| 1 | 11 | 01 | 02 |
| 3 | 32 | 01 | 02 |
| 2 | 22 | 01 | 02 |
| 1 | 1 | 01 | 01 |
| 3 | 3 | 01 | 01 |
| 2 | 2 | 01 | 01 |
+------------+------------+----+----+
最佳實踐
Z-Order功能并不是適用于所有場景,也沒有統一的規則來指導是否應該用Z-Order及如何使用。很多時候都需要根據具體案例去嘗試改造,綜合評估改造Z-Order后生成數據帶來的額外計算成本,相對于存儲成本的節省和下游消費計算成本的節省,是否有收益。下面提供一些經驗上的建議,同時也需要靠各位用戶在使用過程中一起提煉和總結。
優先考慮Clustered Index而不是Z-Order的場景
如果過濾條件基本都是某個前綴的組合,比如a、a和b、a和b和c,那么使用Clustered Index(即ORDER BY a, b, c)更有效,此時不要使用ZORDER BY。因為ORDER BY對第一個字段有非常好的排序效果,對后面字段影響較少;而ZORDER BY對每個字段給予了相同的權重,僅看某一列的排序是不如ORDER BY的第一個字段的。
如果某些字段經常出現在JOIN KEY上,這些字段使用Hash或Range Clustering更合適。因為MaxCompute Z-Order的實現僅僅在文件內進行了排序,而SQL引擎對Z-Order的數據分布沒有感知;但是SQL引擎是能夠感知Clustered Index的,因此在做查詢計劃階段能夠更好地優化JOIN的性能。
如果某些字段經常需要進行GROUP BY和ORDER BY,那么使用Clustered Index可以獲得更好的性能。
Z-Order使用建議
選取經常出現在過濾條件中的字段,尤其是經常聯合在一起過濾的字段。
ZORDER BY的字段數越多,每個字段的排序性能會越差,因此字段數不宜超過4個。如果只有一個字段,那就應該使用Clustered Index而不是Z-Order。
選取的字段的distinct value不宜太小或太大。太小的極端情況就是性別字段,只有兩個值,排序并沒有多大意義。太大的極端情況就是基本沒有重復的,這樣排序的代價會很高,因為MaxCompute的Z-Order實現需要將字段出現的所有值緩存在內存中來計算ZValue。
表的數據量也不宜太小或太大。如果數據量太小,Z-Order無法看出效果。而數據量太大,按照Z-Order方式產出數據的代價會比較高,尤其是基線任務會明顯影響產出的時間。