Greenplum是面向數據倉庫應用的關係型數據庫,基於PostgreSQL開發,跟PostgreSQL的兼容性非常好。通常向PostgreSQL/Greenplum數據庫中大批量寫入數據,可以使用insert語句或者copy命令語句。
但經實際測試經驗發現,對於PostgreSQL數據庫來說,insert與copy的寫入速度基本差不多(參考:PG copy&insert性能對比);但對於Greenplum數據庫來說,copy的寫入速度要比insert快很多。
接下來,我們主要聊下PostgreSQL/Greenplum數據庫的copy命令語句導入數據的用法及其開發。
一、copy命令語法
copy命令可以操作的文件類型有:txt、csv、二進制格式等。
COPY table_name [ ( column_name [, ...] ) ]
FROM { 'filename' | PROGRAM 'command' | STDIN }
[ [ WITH ] ( option [, ...] ) ]
COPY { table_name [ ( column_name [, ...] ) ] | ( query ) }
TO { 'filename' | PROGRAM 'command' | STDOUT }
[ [ WITH ] ( option [, ...] ) ]
where option can be one of:
FORMAT format_name
OIDS [ boolean ]
FREEZE [ boolean ]
DELIMITER 'delimiter_character'
NULL 'null_string'
HEADER [ boolean ]
QUOTE 'quote_character'
ESCAPE 'escape_character'
FORCE_QUOTE { ( column_name [, ...] ) | * }
FORCE_NOT_NULL ( column_name [, ...] )
FORCE_NULL ( column_name [, ...] )
ENCODING 'encoding_name'
其中option的設置的參數如下:
- FORMAT:指複製到文件的文件類型,如:CSV,TEXT。
- OIDS :指複製到文件時帶上oid,但是當某個表沒有oid時就會出錯。
- FREEZE :凍結數據,然後執行VACUUM FREEZE。
- DELIMITER:指在導出文件時的分隔符指定需要用單引號。在TEXT時默認爲tab,CSV文件默認是逗號。不支持binary文件格式。
- HEADER:指在複製到文件時帶上表字段名稱。
- NULL:指定null值,默認爲\N。
- ENCODING:指定文件的編碼,如果沒有指定就默認使用客戶端的字符集。
- STDIN:指的是客戶端程序的輸入流。
- STDOUT: 聲明輸入前往客戶端應用。
- BINARY: 使用二進制格式存儲和讀取,而不是以文本的方式。 在二進制模式下,不能聲明 DELIMITERS,NULL 或者 CSV 選項。
二、copy命令的二進制格式
PostgreSQL的二進制copy文件格式可參考:https://www.postgresql.org/docs/9.4/sql-copy.html
1、使用copy的二進制的原因
當導入的數據中含有二進制類型的數據時,使用csv/text等文本方式的copy將會遇到錯誤。但二進制的copy不存在這種問題,雖然官方強調二進制的copy存在移植性的問題,但是我在用在程序導入數據的場景時,且可不必關心。
2、copy的二進制的文件格式
二進制格式的文件由一個文件頭、數據(零個或多個元組)和文件尾構成。文件頭和數據按網絡字節順序表示。如下:
文件頭
文件頭由15個字節的固定區域和一個變長的擴展區域組成。固定區域包括:
- 簽名:11個字節的序列PGCOPY\nFF\r\n\0。
- 標誌位:32位的整數。位的編號從0(最低位)到31(最高位)。當前只有一個標誌位被使用,它是第16位,如果它的值是1,表示含有OID,如果它的值是0,表示不含OID。其它的標誌位永遠是0。
- 擴展區域:首先是一個32位的整數,表示擴展區域的長度,不包括這個32位的整數自身。當前,它的值是0。第一個元組緊跟在它的後面。
元組
每個元組以一個16位的整數開始,表示元組中域的個數。然後是元組中的每個域。每個域由一個32位的整數開始,它表示域的長度,後面跟着域的數據,-1表示域的值爲空值。如果文件中包含OID,則它的值跟在元組的第一個16位整數的後面,而且就算組的域的個數時,OID不被計算在內,它也由一個32位的整數開始,表示OID的長度,當前,OID的長度固定是4字節,以後可能擴展到8字節。
文件尾
文件尾包含一個16位的整數,它的值是-1。
3、copy的二進制文件格式示例
上圖是copy命令導出的二進制數據文件,圖中用紅色豎線做分隔符,把二進制文件分成不同的段。藍色數字表示各段的編號,其中:
文件頭包括第1段到第3段。第1段是11個字節的簽名序列PGCOPY\nFF\r\n\0;第2段是標誌位,佔4個字節;第3段是擴展區域,至少4個字節,根據情況可能更多。
元組包括第4段到第18段第4、5、6、7、8段是第一條記錄:第4段表示該元祖的列數,佔2個字節;第5段表示第一列的長度,佔4個字節;第6段是第一列的值;第7段是第二列的長度,佔4個字節;第8段是第二列的值。第9、10、11、12、13段是第二條記錄,同上。第14、15、16、17、18段是第三條記錄,同上。
文件尾是第19段,佔2個字節。
三、COPY的二進制導入數據開發
當搞清楚COPY的二進制文件格式後,進行數據導入開發將會變得容易。首先我們準備一個測試導入數據用的表:
CREATE TABLE "tang"."binary_data_table" (
"id" int8 NOT NULL,
"content" varchar(64),
"binary" bytea,
PRIMARY KEY ("id")
)
跟上上述定義的表結構,我們得出的copy命令的語句應爲:
COPY "tang"."binary_data_table" ("id","content","binary") FROM STDIN BINARY
按照上述的二進制文件格式,使用Java語言調用PostgreSQL驅動中的API接口,如下:
import java.io.DataOutputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.List;
import org.postgresql.PGConnection;
import org.postgresql.copy.PGCopyOutputStream;
public class PGCopyOutputStreamTester {
private static String getCopySql(String schemaName, String tableName, List<String> columns) {
StringBuilder sb = new StringBuilder();
sb.append("COPY \"");
sb.append(schemaName);
sb.append("\".\"");
sb.append(tableName);
sb.append("\" (\"");
sb.append(String.join("\",\"", columns));
sb.append("\") FROM STDIN BINARY");
return sb.toString();
}
public static void main(String[] args) {
String schemaName = "tang";
String tableName = "binary_data_table";
List<String> columnList = new ArrayList<String>();
columnList.add("id");
columnList.add("content");
columnList.add("binary");
try {
// 首先與數據庫建立連接
Class.forName("org.postgresql.Driver");
String url = "jdbc:postgresql://172.17.207.151:5432/study";
Connection connection = DriverManager.getConnection(url, "study", "123456");
// 根據schema、table及列拼接出copy語句
String copySqlIn = getCopySql(schemaName, tableName, columnList);
System.out.println(copySqlIn);
// 基於PostgreSQL的驅動API構造向PG寫入數據的輸出流
DataOutputStream os = new DataOutputStream(new PGCopyOutputStream((PGConnection) connection, copySqlIn));
long start = System.currentTimeMillis();
// 11 bytes 文件頭部 簽名字段
byte[] headerSignature = { 'P', 'G', 'C', 'O', 'P', 'Y', '\n', (byte) 0xFF, '\r', '\n', '\0' };
os.write(headerSignature);
// 32 bit integer 文件頭部標誌位,無OID
os.writeInt(0);
// 32 bit header 文件頭部擴展區域
os.writeInt(0);
// 元組列表
for (int i = 0; i < 10000; ++i) {
// 列的總個數,即爲3
os.writeShort(columnList.size());
// 第1個字段id的值
long field1 = 200L + i;
os.writeInt(8);
os.writeLong(field1);
// 第2個字段content的值
String field2 = "hello world two! :" + i;
os.writeInt(field2.getBytes().length);
os.write(field2.getBytes());
// 第3個字段binary的值(模擬的二進制數據)
String filed3 = "this is a test binray content";
os.writeInt(filed3.getBytes().length);
os.write(filed3.getBytes());
}
// 文件尾
os.writeShort(-1);// 0xFFFF
// 將流中的數據寫入到網絡中
os.flush();
os.close();
long stoped = System.currentTimeMillis();
System.out.println("Total copy record 10000, elipse:" + (stoped - start) / 1000.0F);
// 關閉數據庫連接
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
四、結束語
雖然在 SQL 標準裏沒有 COPY 語句,但是當你遇到PostgreSQL/Greenplum等類型的數據庫時,會發現copy有它特別的用途。基於copy方式,本人編寫了一個oracle/SqlServer/mysql/PostgreSQL表結構及數據向Greenplum數據庫的離線同步小工具, 介紹詳見:https://blog.csdn.net/inrgihc/article/details/103739629