使用copy命令的二進制形式向PostgreSQL/Greenplum數據庫批量導入數據

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
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章