海量資料切分抽取的實踐場景(r11筆記第43天)
一、問題背景
今天開發的同學找到我,他們需要做一個數據統計分析,需要我提供一些支援,把一個統計庫中的大表資料匯出成文字提供給他們。
這個表有多大呢,資料量有4億+,而且使用了分庫分表的策略,所以看起來這不是一個簡單的問題。
簡單來說就是下面的架構方式,在右側的目標端存在的都是物化檢視,存在12個子集,也就意味著有12個物化檢視存在。
如何抽取出這12個物化檢視的資料呢,一邊和BI的同學確認格式,而另一邊需要對抽取的檔案進行切分,意味著一個物化檢視如果資料量太大,匯出的csv檔案會很大,希望提供給BI同學的是一些大小均勻的csv檔案。
這裡就有兩個重要的內容需要說一下:
1)大表如何平均切分,而不單單考慮是否為分割槽表。
2)如何規範化,標準化的抽取資料。
二、大表如何切分
大表的切分一直以來是資料遷移中的重頭戲,我在以前的時間積累中也為此困擾。一個表如果不是分割槽表,存在1000萬的資料,如果我們希望以資料條數為基準進行切分,能否實現。
比如1000萬資料的表,100萬為單位,那就生成10個csv檔案,每個檔案包含100萬資料。
當然這個工作是可以做成的,實現的基礎就是ROWID切分。直接上指令碼。
#### $1 dba conn details #### $2 table owner #### $3 table_name #### $4 subobject_name #### $5 parallel_no function normal_split { sqlplus -s $1 <<EOF set linesize 200 set pages 0 set feedback off spool rowid_range_$3_x.lst select rownum || ', ' ||' rowid between '|| chr(39)||dbms_rowid.rowid_create( 1, DOI, lo_fno, lo_block, 0 ) ||chr(39)|| ' and ' || chr(39)||dbms_rowid.rowid_create( 1, DOI, hi_fno, hi_block, 1000000 )||chr(39) data from ( SELECT DISTINCT DOI, grp, first_value(relative_fno) over (partition BY DOI,grp order by relative_fno, block_id rows BETWEEN unbounded preceding AND unbounded following) lo_fno, first_value(block_id ) over (partition BY DOI,grp order by relative_fno, block_id rows BETWEEN unbounded preceding AND unbounded following) lo_block, last_value(relative_fno) over (partition BY DOI,grp order by relative_fno, block_id rows BETWEEN unbounded preceding AND unbounded following) hi_fno, last_value(block_id+blocks-1) over (partition BY DOI,grp order by relative_fno, block_id rows BETWEEN unbounded preceding AND unbounded following) hi_block, SUM(blocks) over (partition BY DOI,grp) sum_blocks,SUBOBJECT_NAME FROM( SELECT obj.OBJECT_ID, obj.SUBOBJECT_NAME, obj.DATA_OBJECT_ID as DOI, ext.relative_fno, ext.block_id, ( SUM(blocks) over () ) SUM, (SUM(blocks) over (ORDER BY DATA_OBJECT_ID,relative_fno, block_id)-0.01 ) sum_fno , TRUNC( (SUM(blocks) over (ORDER BY DATA_OBJECT_ID,relative_fno, block_id)-0.01) / (SUM(blocks) over ()/ $5 ) ) grp, ext.blocks FROM dba_extents ext, dba_objects obj WHERE ext.segment_name = UPPER('$3') AND ext.owner = UPPER('$2') AND obj.owner = ext.owner AND obj.object_name = ext.segment_name AND obj.DATA_OBJECT_ID IS NOT NULL ORDER BY DATA_OBJECT_ID, relative_fno, block_id ) order by DOI,grp ); spool off; EOF } sub_partition_name=$4 if [[ $sub_partition_name = 'x' ]] then normal_split $1 $2 $3 x $5 fi 說實話,這段指令碼值得你好好體會一番,而不是看過就看過了,很多產品工具的核心就是一些很細小的東西,點到為止。
指令碼的執行結果如下,我們期望是切分為20份。輸出結果會直接打印出邊界的ROWID,執行結果如下:
$ksh gen_rowid.sh test_dba/xxx accstat ACC00_USER_SOCIETY_INFO x 20 1, rowid between 'AAFO0gAIFAAPhoJAAA' and 'AAFO0gAMhAAPUj/EJA' 2, rowid between 'AAFO0gAMhAAPUkAAAA' and 'AAFO0gAMhAAPYj/EJA' 3, rowid between 'AAFO0gAMhAAPYkAAAA' and 'AAFO0gANvAAD21/EJA' 4, rowid between 'AAFO0gANvAAD22AAAA' and 'AAFO0gANvAAD5h/EJA'
有了第一步的輔助,那麼第二步就順手推舟了,不過還得再加把勁兒。
三、如何規範化匯出海量資料?
這個部分可能存在一些爭議,怎樣算規範化,怎麼樣的算海量資料,我們先不拘束於這些,我們先說說匯出資料為csv有哪幾種方式,除了圖形工具外,Oracle命令列的方式匯出有SQL, PL/SQL,其它程式語言的方式。
SQL匯出的要點就是設定分隔符,假設分隔符為逗號,SQL*Plus中設定屬性colsep " ," (以逗號分隔),這種方式的輸出實在不敢恭維,還有一種就是手工設定風格符,比如通過chr(44)的方式來設定。毫無疑問,還是太繁瑣。
PL/SQL匯出的方式也有標準版,高配版兩種方式,標準版我留使用utl_file來完成,通過設定目錄的方式。
比如我們建立了一個目錄為TMP_DATA,則可以使用如下的方式來完成。
create directory TMP_DATA as '/U01/app/tmp_data';
grant read,write on directory tmp_data to test_dba;
使用如下的指令碼來完成基本的資料抽取,生成檔案為output.txt
這裡我們就使用ROWID的方式來抽取資料。
declare
v_filehandle UTL_FILE.FILE_TYPE;
begin
v_filehandle:=utl_file.fopen('TMP_DATA','output.txt','w');
UTL_FILE.PUTF (v_filehandle,'---export data from table ACC00_USER_SOCIETY_INFO:', SYSTIMESTAMP);
UTL_FILE.NEW_LINE (v_filehandle);
for i in(select
*
FROM accstat.ACC00_USER_SOCIETY_INFO where rowid between 'AAFO0gAIFAAPhoJAAA' and 'AAFO0gAMhAAPUj/EJA' ) loop
UTL_FILE.PUTF (v_filehandle, '%s,%sn',i.uin,i.age);
end loop;
UTL_FILE.FCLOSE (v_filehandle);
end;
/
這種方式相對來說可控一些,但是一個比較繁瑣的部分就是欄位都得一一對映,這可不大好。
有沒有更好的方式呢,有的,我們得想起了Thomas Kyte大師的指令碼了,之前他提供過,建立一個FUNCTION即可。
CREATE function dump_csv( p_query in varchar2,
p_separator in varchar2
default ',',
p_dir in varchar2 ,
p_filename in varchar2 )
return number
AUTHID CURRENT_USER
is
l_output utl_file.file_type;
l_theCursor integer default dbms_sql.open_cursor;
l_columnValue varchar2(2000);
l_status integer;
l_colCnt number default 0;
l_separator varchar2(10) default '';
l_cnt number default 0;
begin
l_output := utl_file.fopen( p_dir, p_filename, 'w' );
dbms_sql.parse( l_theCursor, p_query, dbms_sql.native );
for i in 1 .. 255 loop
begin
dbms_sql.define_column( l_theCursor, i,
l_columnValue, 2000 );
l_colCnt := i;
exception
when others then
if ( sqlcode = -1007 ) then exit;
else
raise;
end if;
end;
end loop;
dbms_sql.define_column( l_theCursor, 1, l_columnValue,
2000 );
l_status := dbms_sql.execute(l_theCursor);
loop
exit when ( dbms_sql.fetch_rows(l_theCursor) <= 0 );
l_separator := '';
for i in 1 .. l_colCnt loop
dbms_sql.column_value( l_theCursor, i,
l_columnValue );
utl_file.put( l_output, l_separator ||
l_columnValue );
l_separator := p_separator;
end loop;
utl_file.new_line( l_output );
l_cnt := l_cnt+1;
end loop;
dbms_sql.close_cursor(l_theCursor);
utl_file.fclose( l_output );
return l_cnt;
end dump_csv;
/如果需要匯出一個表裡的資料,這樣使用就可以了,還是根據ROWID來切分資料。
select dump_csv('select * from accstat.ACC00_USER_SOCIETY_INFO where rowid between ''AAFO0gAIFAAPhoJAAA'' and ''AAFO0gAMhAAPUj/EJA'' and rownum<1000',',','TMP_DATA','data.csv') from dual;
當然要點就是這些,我們可以寫幾個SQL即可生成資料。
整個過程其實涉及到一些技術細節,還是需要大家多加揣摩,掌握好了之後,在資料遷移的場景中就能夠大展拳腳。
我也給自己的公眾號設定了一個簡單的封面,看起來還行吧。純手工PS摳圖補字完成。