Doris 提供多种数据导入方案,可以针对不同的数据源进行选择不同的数据导入方式。
数据源 | 导入方式 |
---|---|
对象存储(s3),HDFS | 使用Broker导入数据 |
本地文件 | 导入本地数据 |
Kafka | 订阅Kafka数据 |
Mysql、PostgreSQL,Oracle,SQLServer | 通过外部表同步数据 |
通过JDBC导入 | 使用JDBC同步数据 |
导入JSON格式数据 | JSON格式数据导入 |
导入方式名称 | 使用方式 |
---|---|
Spark Load | 通过Spark导入外部数据 |
Broker Load | 通过Broker导入外部存储数据 |
Stream Load | 流式导入数据(本地文件及内存数据) |
Routine Load | 导入Kafka数据 |
Insert Into | 外部表通过INSERT方式导入数据 |
S3 Load | S3协议的对象存储数据导入 |
MySQL Load | MySQL客户端导入本地数据 |
不同的导入方式支持的数据格式略有不同。
导入方式 | 支持的格式 |
---|---|
Broker Load | parquet、orc、csv、gzip |
Stream Load | csv、json、parquet、orc |
Routine Load | csv、json |
MySQL Load | csv |
Apache Doris 的数据导入实现有以下共性特征,这里分别介绍,以帮助大家更好的使用数据导入功能
Doris 的每一个导入作业,不论是使用 Broker Load 进行批量导入,还是使用 INSERT 语句进行单条导入,都是一个完整的事务操作。导入事务可以保证一批次内的数据原子生效,不会出现部分数据写入的情况。
同时,一个导入作业都会有一个 Label。这个 Label 是在一个数据库(Database)下唯一的,用于唯一标识一个导入作业。Label 可以由用户指定,部分导入功能也会由系统自动生成。
Label 是用于保证对应的导入作业,仅能成功导入一次。一个被成功导入的 Label,再次使用时,会被拒绝并报错 Label already used
。通过这个机制,可以在 Doris 侧做到 At-Most-Once
语义。如果结合上游系统的 At-Least-Once
语义,则可以实现导入数据的 Exactly-Once
语义。
关于原子性保证的最佳实践,可以参阅 导入事务和原子性。
导入方式分为同步和异步。对于同步导入方式,返回结果即表示导入成功还是失败。而对于异步导入方式,返回成功仅代表作业提交成功,不代表数据导入成功,需要使用对应的命令查看导入作业的运行状态。
向量化场景才能支持array函数,非向量化场景不支持。
如果想要应用array函数导入数据,则应先启用向量化功能;然后需要根据array函数的参数类型将输入参数列转换为array类型;最后,就可以继续使用array函数了。
例如以下导入,需要先将列b14和列a13先cast成array
类型,再运用array_union
函数。
LOAD LABEL label_03_14_49_34_898986_19090452100 (
DATA INFILE("hdfs://test.hdfs.com:9000/user/test/data/sys/load/array_test.data")
INTO TABLE `test_array_table`
COLUMNS TERMINATED BY "|" (`k1`, `a1`, `a2`, `a3`, `a4`, `a5`, `a6`, `a7`, `a8`, `a9`, `a10`, `a11`, `a12`, `a13`, `b14`)
SET(a14=array_union(cast(b14 as array), cast(a13 as array))) WHERE size(a2) > 270)
WITH BROKER "hdfs" ("username"="test_array", "password"="")
PROPERTIES( "max_filter_ratio"="0.8" );
本文档主要介绍如何从客户端导入本地的数据。
目前Doris支持两种从本地导入数据的模式:
Stream Load 用于将本地文件导入到 Doris 中。
不同于其他命令的提交方式,Stream Load 是通过 HTTP 协议与 Doris 进行连接交互的。
该方式中涉及 HOST:PORT 应为 HTTP 协议端口。
本文文档我们以 curl 命令为例演示如何进行数据导入。
文档最后,我们给出一个使用 Java 导入数据的代码示例
Stream Load 的请求体如下:
PUT /api/{db}/{table}/_stream_load
创建一张表
通过 CREATE TABLE
命令在demo
创建一张表用于存储待导入的数据。具体的导入方式请查阅 CREATE TABLE 命令手册。示例如下:
CREATE TABLE IF NOT EXISTS load_local_file_test
(
id INT,
age TINYINT,
name VARCHAR(50)
)
unique key(id)
DISTRIBUTED BY HASH(id) BUCKETS 3;
导入数据
执行以下 curl 命令导入本地文件:
curl -u user:passwd -H "label:load_local_file_test" -T /path/to/local/demo.txt http://host:port/api/demo/load_local_file_test/_stream_load
关于 Stream Load 命令的更多高级操作,请参阅 Stream Load 命令文档。
等待导入结果
Stream Load 命令是同步命令,返回成功即表示导入成功。如果导入数据较大,可能需要较长的等待时间。示例如下:
{
"TxnId": 1003,
"Label": "load_local_file_test",
"Status": "Success",
"Message": "OK",
"NumberTotalRows": 1000000,
"NumberLoadedRows": 1000000,
"NumberFilteredRows": 1,
"NumberUnselectedRows": 0,
"LoadBytes": 40888898,
"LoadTimeMs": 2144,
"BeginTxnTimeMs": 1,
"StreamLoadPutTimeMs": 2,
"ReadDataTimeMs": 325,
"WriteDataTimeMs": 1933,
"CommitAndPublishTimeMs": 106,
"ErrorURL": "http://192.168.1.1:8042/api/_load_error_log?file=__shard_0/error_log_insert_stmt_db18266d4d9b4ee5-abb00ddd64bdf005_db18266d4d9b4ee5_abb00ddd64bdf005"
}
Status
字段状态为 Success
即表示导入成功。这里通过一个简单的 JAVA 示例来执行 Stream Load:
package demo.doris;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/*
这是一个 Doris Stream Load 示例,需要依赖
org.apache.httpcomponents
httpclient
4.5.13
*/
public class DorisStreamLoader {
//可以选择填写 FE 地址以及 FE 的 http_port,但须保证客户端和 BE 节点的连通性。
private final static String HOST = "your_host";
private final static int PORT = 8040;
private final static String DATABASE = "db1"; // 要导入的数据库
private final static String TABLE = "tbl1"; // 要导入的表
private final static String USER = "root"; // Doris 用户名
private final static String PASSWD = ""; // Doris 密码
private final static String LOAD_FILE_NAME = "/path/to/1.txt"; // 要导入的本地文件路径
private final static String loadUrl = String.format("http://%s:%s/api/%s/%s/_stream_load",
HOST, PORT, DATABASE, TABLE);
private final static HttpClientBuilder httpClientBuilder = HttpClients
.custom()
.setRedirectStrategy(new DefaultRedirectStrategy() {
@Override
protected boolean isRedirectable(String method) {
// 如果连接目标是 FE,则需要处理 307 redirect。
return true;
}
});
public void load(File file) throws Exception {
try (CloseableHttpClient client = httpClientBuilder.build()) {
HttpPut put = new HttpPut(loadUrl);
put.setHeader(HttpHeaders.EXPECT, "100-continue");
put.setHeader(HttpHeaders.AUTHORIZATION, basicAuthHeader(USER, PASSWD));
// 可以在 Header 中设置 stream load 相关属性,这里我们设置 label 和 column_separator。
put.setHeader("label","label1");
put.setHeader("column_separator",",");
// 设置导入文件。
// 这里也可以使用 StringEntity 来传输任意数据。
FileEntity entity = new FileEntity(file);
put.setEntity(entity);
try (CloseableHttpResponse response = client.execute(put)) {
String loadResult = "";
if (response.getEntity() != null) {
loadResult = EntityUtils.toString(response.getEntity());
}
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
throw new IOException(
String.format("Stream load failed. status: %s load result: %s", statusCode, loadResult));
}
System.out.println("Get load result: " + loadResult);
}
}
}
private String basicAuthHeader(String username, String password) {
final String tobeEncode = username + ":" + password;
byte[] encoded = Base64.encodeBase64(tobeEncode.getBytes(StandardCharsets.UTF_8));
return "Basic " + new String(encoded);
}
public static void main(String[] args) throws Exception{
DorisStreamLoader loader = new DorisStreamLoader();
File file = new File(LOAD_FILE_NAME);
loader.load(file);
}
}
注意:这里 http client 的版本要是4.5.13
org.apache.httpcomponents httpclient 4.5.13
SinceVersion devMySql LOAD样例
创建一张表
通过 CREATE TABLE
命令在demo
创建一张表用于存储待导入的数据
CREATE TABLE IF NOT EXISTS load_local_file_test
(
id INT,
age TINYINT,
name VARCHAR(50)
)
unique key(id)
DISTRIBUTED BY HASH(id) BUCKETS 3;
导入数据 在MySql客户端下执行以下 SQL 命令导入本地文件:
LOAD DATA
LOCAL
INFILE '/path/to/local/demo.txt'
INTO TABLE demo.load_local_file_test
关于 MySQL Load 命令的更多高级操作,请参阅 MySQL Load 命令文档。
等待导入结果
MySql Load 命令是同步命令,返回成功即表示导入成功。如果导入数据较大,可能需要较长的等待时间。示例如下:
Query OK, 1 row affected (0.17 sec)
Records: 1 Deleted: 0 Skipped: 0 Warnings: 0
本文档主要介绍如何导入外部系统中存储的数据。例如(HDFS,所有支持S3协议的对象存储)
上传需要导入的文件到HDFS上,具体命令可参阅HDFS上传命令
Hdfs load 创建导入语句,导入方式和Broker Load 基本相同,只需要将 WITH BROKER broker_name ()
语句替换成如下部分
LOAD LABEL db_name.label_name
(data_desc, ...)
WITH HDFS
[PROPERTIES (key1=value1, ... )]
创建一张表
通过 CREATE TABLE
命令在demo
创建一张表用于存储待导入的数据。具体的导入方式请查阅 CREATE TABLE 命令手册。示例如下:
CREATE TABLE IF NOT EXISTS load_hdfs_file_test
(
id INT,
age TINYINT,
name VARCHAR(50)
)
unique key(id)
DISTRIBUTED BY HASH(id) BUCKETS 3;
导入数据执行以下命令导入HDFS文件:
LOAD LABEL demo.label_20220402
(
DATA INFILE("hdfs://host:port/tmp/test_hdfs.txt")
INTO TABLE `load_hdfs_file_test`
COLUMNS TERMINATED BY "\t"
(id,age,name)
)
with HDFS (
"fs.defaultFS"="hdfs://testFs",
"hdfs_user"="user"
)
PROPERTIES
(
"timeout"="1200",
"max_filter_ratio"="0.1"
);
关于参数介绍,请参阅Broker Load,HA集群的创建语法,通过HELP BROKER LOAD
查看
查看导入状态
Broker load 是一个异步的导入方式,具体导入结果可以通过SHOW LOAD命令查看
mysql> show load order by createtime desc limit 1\G;
*************************** 1. row ***************************
JobId: 41326624
Label: broker_load_2022_04_15
State: FINISHED
Progress: ETL:100%; LOAD:100%
Type: BROKER
EtlInfo: unselected.rows=0; dpp.abnorm.ALL=0; dpp.norm.ALL=27
TaskInfo: cluster:N/A; timeout(s):1200; max_filter_ratio:0.1
ErrorMsg: NULL
CreateTime: 2022-04-01 18:59:06
EtlStartTime: 2022-04-01 18:59:11
EtlFinishTime: 2022-04-01 18:59:11
LoadStartTime: 2022-04-01 18:59:11
LoadFinishTime: 2022-04-01 18:59:11
URL: NULL
JobDetails: {"Unfinished backends":{"5072bde59b74b65-8d2c0ee5b029adc0":[]},"ScannedRows":27,"TaskNumber":1,"All backends":{"5072bde59b74b65-8d2c0ee5b029adc0":[36728051]},"FileNumber":1,"FileSize":5540}
1 row in set (0.01 sec)
从0.14 版本开始,Doris 支持通过S3协议直接从支持S3协议的在线存储系统导入数据。
下面主要介绍如何导入 AWS S3 中存储的数据。也支持导入其他支持S3协议的对象存储系统导入。
Access keys
,可以在 AWS console 的 My Security Credentials
找到生成方式, 如下图所示: AK_SK 选择 Create New Access Key
注意保存生成 AK和SK.其他云存储系统可以相应的文档找到与S3兼容的相关信息
导入方式和 Broker Load 基本相同,只需要将 WITH BROKER broker_name ()
语句替换成如下部分
WITH S3
(
"AWS_ENDPOINT" = "AWS_ENDPOINT",
"AWS_ACCESS_KEY" = "AWS_ACCESS_KEY",
"AWS_SECRET_KEY"="AWS_SECRET_KEY",
"AWS_REGION" = "AWS_REGION"
)
完整示例如下
LOAD LABEL example_db.exmpale_label_1
(
DATA INFILE("s3://your_bucket_name/your_file.txt")
INTO TABLE load_test
COLUMNS TERMINATED BY ","
)
WITH S3
(
"AWS_ENDPOINT" = "AWS_ENDPOINT",
"AWS_ACCESS_KEY" = "AWS_ACCESS_KEY",
"AWS_SECRET_KEY"="AWS_SECRET_KEY",
"AWS_REGION" = "AWS_REGION"
)
PROPERTIES
(
"timeout" = "3600"
);
virtual-hosted style
方式。但某些对象存储系统可能没开启或没支持 virtual-hosted style
方式的访问,此时我们可以添加 use_path_style
参数来强制使用 path style
方式: WITH S3
(
"AWS_ENDPOINT" = "AWS_ENDPOINT",
"AWS_ACCESS_KEY" = "AWS_ACCESS_KEY",
"AWS_SECRET_KEY"="AWS_SECRET_KEY",
"AWS_REGION" = "AWS_REGION",
"use_path_style" = "true"
)
SinceVersion 1.2
WITH S3
(
"AWS_ENDPOINT" = "AWS_ENDPOINT",
"AWS_ACCESS_KEY" = "AWS_TEMP_ACCESS_KEY",
"AWS_SECRET_KEY" = "AWS_TEMP_SECRET_KEY",
"AWS_TOKEN" = "AWS_TEMP_TOKEN",
"AWS_REGION" = "AWS_REGION"
)
用户可以通过提交例行导入作业,直接订阅Kafka中的消息数据,以近实时的方式进行数据同步。
Doris 自身能够保证不丢不重的订阅 Kafka 中的消息,即 Exactly-Once
消费语义。
订阅 Kafka 消息使用了 Doris 中的例行导入(Routine Load)功能。
用户首先需要创建一个例行导入作业。作业会通过例行调度,不断地发送一系列的任务,每个任务会消费一定数量 Kafka 中的消息。
请注意以下使用限制:
例行导入功能支持无认证的 Kafka 集群,以及通过 SSL 认证的 Kafka 集群。
访问 SSL 认证的 Kafka 集群需要用户提供用于认证 Kafka Broker 公钥的证书文件(ca.pem)。如果 Kafka 集群同时开启了客户端认证,则还需提供客户端的公钥(client.pem)、密钥文件(client.key),以及密钥密码。这里所需的文件需要先通过 CREAE FILE
命令上传到 Doris 中,并且 catalog 名称为 kafka
。CREATE FILE
命令的具体帮助可以参见 CREATE FILE 命令手册。这里给出示例:
上传文件
CREATE FILE "ca.pem" PROPERTIES("url" = "https://example_url/kafka-key/ca.pem", "catalog" = "kafka");
CREATE FILE "client.key" PROPERTIES("url" = "https://example_urlkafka-key/client.key", "catalog" = "kafka");
CREATE FILE "client.pem" PROPERTIES("url" = "https://example_url/kafka-key/client.pem", "catalog" = "kafka");
上传完成后,可以通过 SHOW FILES 命令查看已上传的文件。
创建例行导入任务的具体命令,请参阅 ROUTINE LOAD 命令手册。这里给出示例:
访问无认证的 Kafka 集群
CREATE ROUTINE LOAD demo.my_first_routine_load_job ON test_1
COLUMNS TERMINATED BY ","
PROPERTIES
(
"max_batch_interval" = "20",
"max_batch_rows" = "300000",
"max_batch_size" = "209715200",
)
FROM KAFKA
(
"kafka_broker_list" = "broker1:9092,broker2:9092,broker3:9092",
"kafka_topic" = "my_topic",
"property.group.id" = "xxx",
"property.client.id" = "xxx",
"property.kafka_default_offsets" = "OFFSET_BEGINNING"
);
max_batch_interval/max_batch_rows/max_batch_size
用于控制一个子任务的运行周期。一个子任务的运行周期由最长运行时间、最多消费行数和最大消费数据量共同决定。访问 SSL 认证的 Kafka 集群
CREATE ROUTINE LOAD demo.my_first_routine_load_job ON test_1
COLUMNS TERMINATED BY ",",
PROPERTIES
(
"max_batch_interval" = "20",
"max_batch_rows" = "300000",
"max_batch_size" = "209715200",
)
FROM KAFKA
(
"kafka_broker_list"= "broker1:9091,broker2:9091",
"kafka_topic" = "my_topic",
"property.security.protocol" = "ssl",
"property.ssl.ca.location" = "FILE:ca.pem",
"property.ssl.certificate.location" = "FILE:client.pem",
"property.ssl.key.location" = "FILE:client.key",
"property.ssl.key.password" = "abcdefg"
);
查看作业状态的具体命令和示例请参阅 SHOW ROUTINE LOAD 命令文档。
查看某个作业的任务运行状态的具体命令和示例请参阅 SHOW ROUTINE LOAD TASK 命令文档。
只能查看当前正在运行中的任务,已结束和未开始的任务无法查看。
用户可以修改已经创建的作业的部分属性。具体说明请参阅 ALTER ROUTINE LOAD 命令手册。
用户可以通过 STOP/PAUSE/RESUME
三个命令来控制作业的停止,暂停和重启。
具体命令请参阅 STOP ROUTINE LOAD,PAUSE ROUTINE LOAD,RESUME ROUTINE LOAD 命令文档。
关于 ROUTINE LOAD 的更多详细语法和最佳实践,请参阅 ROUTINE LOAD 命令手册。
Doris 可以创建外部表。创建完成后,可以通过 SELECT 语句直接查询外部表的数据,也可以通过 INSERT INTO SELECT
的方式导入外部表的数据。
Doris 外部表目前支持的数据源包括:
本文档主要介绍如何创建通过 ODBC 协议访问的外部表,以及如何导入这些外部表的数据。
创建 ODBC 外部表的详细介绍请参阅 CREATE EXTERNAL TABLE 语法帮助手册。
这里仅通过示例说明使用方式。
创建 ODBC Resource
ODBC Resource 的目的是用于统一管理外部表的连接信息。
CREATE EXTERNAL RESOURCE `oracle_test_odbc`
PROPERTIES (
"type" = "odbc_catalog",
"host" = "192.168.0.10",
"port" = "8086",
"user" = "oracle",
"password" = "oracle",
"database" = "oracle",
"odbc_type" = "oracle",
"driver" = "Oracle"
);
这里我们创建了一个名为 oracle_test_odbc
的 Resource,其类型为 odbc_catalog
,表示这是一个用于存储 ODBC 信息的 Resource。odbc_type
为 oracle
,表示这个 OBDC Resource 是用于连接 Oracle 数据库的。关于其他类型的资源,具体可参阅 资源管理 文档。
CREATE EXTERNAL TABLE `ext_oracle_demo` (
`k1` decimal(9, 3) NOT NULL COMMENT "",
`k2` char(10) NOT NULL COMMENT "",
`k3` datetime NOT NULL COMMENT "",
`k5` varchar(20) NOT NULL COMMENT "",
`k6` double NOT NULL COMMENT ""
) ENGINE=ODBC
COMMENT "ODBC"
PROPERTIES (
"odbc_catalog_resource" = "oracle_test_odbc",
"database" = "oracle",
"table" = "baseall"
);
这里我们创建一个 ext_oracle_demo
外部表,并引用了之前创建的 oracle_test_odbc
Resource
创建 Doris 表
这里我们创建一张 Doris 的表,列信息和上一步创建的外部表 ext_oracle_demo
一样:
CREATE TABLE `doris_oralce_tbl` (
`k1` decimal(9, 3) NOT NULL COMMENT "",
`k2` char(10) NOT NULL COMMENT "",
`k3` datetime NOT NULL COMMENT "",
`k5` varchar(20) NOT NULL COMMENT "",
`k6` double NOT NULL COMMENT ""
)
COMMENT "Doris Table"
DISTRIBUTED BY HASH(k1) BUCKETS 2
PROPERTIES (
"replication_num" = "1"
);
关于创建 Doris 表的详细说明,请参阅 CREATE-TABLE 语法帮助。
导入数据 (从 ext_oracle_demo
表 导入到 doris_oralce_tbl
表)
INSERT INTO doris_oralce_tbl SELECT k1,k2,k3 FROM ext_oracle_demo limit 100;
INSERT 命令是同步命令,返回成功,即表示导入成功。
关于 CREATE EXTERNAL TABLE 的更多详细语法和最佳实践,请参阅 CREATE EXTERNAL TABLE 命令手册
用户可以通过 MySQL 协议,使用 INSERT 语句进行数据导入。
INSERT 语句的使用方式和 MySQL 等数据库中 INSERT 语句的使用方式类似。 INSERT 语句支持以下两种语法:
* INSERT INTO table SELECT ...
* INSERT INTO table VALUES(...)
这里我们仅介绍第二种方式。关于 INSERT 命令的详细说明,请参阅 INSERT 命令文档。
单次写入是指用户直接执行一个 INSERT 命令。示例如下:
INSERT INTO example_tbl (col1, col2, col3) VALUES (1000, "test", 3.25);
对于 Doris 来说,一个 INSERT 命令就是一个完整的导入事务。
因此不论是导入一条数据,还是多条数据,我们都不建议在生产环境使用这种方式进行数据导入。高频次的 INSERT 操作会导致在存储层产生大量的小文件,会严重影响系统性能。
该方式仅用于线下简单测试或低频少量的操作。
或者可以使用以下方式进行批量的插入操作:
INSERT INTO example_tbl VALUES
(1000, "baidu1", 3.25)
(2000, "baidu2", 4.25)
(3000, "baidu3", 5.25);
我们建议一批次插入条数在尽量大,比如几千甚至一万条一次。或者可以通过下面的程序的方式,使用 PreparedStatement 来进行批量插入。
这里我们给出一个简单的 JDBC 批量 INSERT 代码示例:
package demo.doris;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class DorisJDBCDemo {
private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
private static final String DB_URL_PATTERN = "jdbc:mysql://%s:%d/%s?rewriteBatchedStatements=true";
private static final String HOST = "127.0.0.1"; // Leader Node host
private static final int PORT = 9030; // query_port of Leader Node
private static final String DB = "demo";
private static final String TBL = "test_1";
private static final String USER = "admin";
private static final String PASSWD = "my_pass";
private static final int INSERT_BATCH_SIZE = 10000;
public static void main(String[] args) {
insert();
}
private static void insert() {
// 注意末尾不要加 分号 ";"
String query = "insert into " + TBL + " values(?, ?)";
// 设置 Label 以做到幂等。
// String query = "insert into " + TBL + " WITH LABEL my_label values(?, ?)";
Connection conn = null;
PreparedStatement stmt = null;
String dbUrl = String.format(DB_URL_PATTERN, HOST, PORT, DB);
try {
Class.forName(JDBC_DRIVER);
conn = DriverManager.getConnection(dbUrl, USER, PASSWD);
stmt = conn.prepareStatement(query);
for (int i =0; i < INSERT_BATCH_SIZE; i++) {
stmt.setInt(1, i);
stmt.setInt(2, i * 100);
stmt.addBatch();
}
int[] res = stmt.executeBatch();
System.out.println(res);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (stmt != null) {
stmt.close();
}
} catch (SQLException se2) {
se2.printStackTrace();
}
try {
if (conn != null) conn.close();
} catch (SQLException se) {
se.printStackTrace();
}
}
}
}
请注意以下几点:
JDBC 连接串需添加 rewriteBatchedStatements=true
参数,并使用 PreparedStatement
方式。
目前 Doris 暂不支持服务器端的 PrepareStatemnt,所以 JDBC Driver 会在客户端进行批量 Prepare。
rewriteBatchedStatements=true
会确保 Driver 执行批处理。并最终形成如下形式的 INSERT 语句发往 Doris:
INSERT INTO example_tbl VALUES
(1000, "baidu1", 3.25)
(2000, "baidu2", 4.25)
(3000, "baidu3", 5.25);
批次大小
因为是在客户端进行批量处理,所以一批次过大的话,会占用客户端的内存资源,需关注。
Doris 后续会支持服务端的 PrepareStatemnt,敬请期待。
导入原子性
和其他到导入方式一样,INSERT 操作本身也支持原子性。每一个 INSERT 操作都是一个导入事务,能够保证一个 INSERT 中的所有数据原子性的写入。
前面提到,我们建议在使用 INSERT 导入数据时,采用 ”批“ 的方式进行导入,而不是单条插入。
同时,我们可以为每次 INSERT 操作设置一个 Label。通过 Label 机制 可以保证操作的幂等性和原子性,最终做到数据的不丢不重。关于 INSERT 中 Label 的具体用法,可以参阅 INSERT 文档。
Doris 中的所有导入操作都有原子性保证,即一个导入作业中的数据要么全部成功,要么全部失败。不会出现仅部分数据导入成功的情况。
在 BROKER LOAD 中我们也可以实现多表的原子性导入。
对于表所附属的 物化视图,也同时保证和基表的原子性和一致性。
Doris 的导入作业都可以设置一个 Label。这个 Label 通常是用户自定义的、具有一定业务逻辑属性的字符串。
Label 的主要作用是唯一标识一个导入任务,并且能够保证相同的 Label 仅会被成功导入一次。
Label 机制可以保证导入数据的不丢不重。如果上游数据源能够保证 At-Least-Once 语义,则配合 Doris 的 Label 机制,能够保证 Exactly-Once 语义。
Label 在一个数据库下具有唯一性。Label 的保留期限默认是 3 天。即 3 天后,已完成的 Label 会被自动清理,之后 Label 可以被重复使用。
Label 通常被设置为 业务逻辑+时间
的格式。如 my_business1_20220330_125000
。
这个 Label 通常用于表示:业务 my_business1
这个业务在 2022-03-30 12:50:00
产生的一批数据。通过这种 Label 设定,业务上可以通过 Label 查询导入任务状态,来明确的获知该时间点批次的数据是否已经导入成功。如果没有成功,则可以使用这个 Label 继续重试导入。
BROKER LOAD
LOAD LABEL example_db.label1
(
DATA INFILE("bos://bucket/input/file")
INTO TABLE `my_table`
(k1, k2, tmpk3)
PRECEDING FILTER k1 = 1
SET (
k3 = tmpk3 + 1
)
WHERE k1 > k2
)
WITH BROKER bos
(
...
);
STREAM LOAD
curl
--location-trusted
-u user:passwd
-H "columns: k1, k2, tmpk3, k3 = tmpk3 + 1"
-H "where: k1 > k2"
-T file.txt
http://host:port/api/testDb/testTbl/_stream_load
ROUTINE LOAD
CREATE ROUTINE LOAD example_db.label1 ON my_table
COLUMNS(k1, k2, tmpk3, k3 = tmpk3 + 1),
PRECEDING FILTER k1 = 1,
WHERE k1 > k2
...
以上导入方式都支持对源数据进行列映射、转换和过滤操作:
前置过滤:对读取到的原始数据进行一次过滤。
PRECEDING FILTER k1 = 1
映射:定义源数据中的列。如果定义的列名和表中的列相同,则直接映射为表中的列。如果不同,则这个被定义的列可以用于之后的转换操作。如上面示例中的:
(k1, k2, tmpk3)
转换:将第一步中经过映射的列进行转换,可以使用内置表达式、函数、自定义函数进行转化,并重新映射到表中对应的列上。如上面示例中的:
k3 = tmpk3 + 1
后置过滤:对经过映射和转换后的列,通过表达式进行过滤。被过滤的数据行不会导入到系统中。如上面示例中的:
WHERE k1 > k2
列映射的目的主要是描述导入文件中各个列的信息,相当于为源数据中的列定义名称。通过描述列映射关系,我们可以将于表中列顺序不同、列数量不同的源文件导入到 Doris 中。下面我们通过示例说明:
假设源文件有4列,内容如下(表头列名仅为方便表述,实际并无表头):
列1 | 列2 | 列3 | 列4 |
---|---|---|---|
1 | 100 | beijing | 1.1 |
2 | 200 | shanghai | 1.2 |
3 | 300 | guangzhou | 1.3 |
4 | \N | chongqing | 1.4 |
注:
\N
在源文件中表示 null。
调整映射顺序
假设表中有 k1,k2,k3,k4
4列。我们希望的导入映射关系如下:
列1 -> k1
列2 -> k3
列3 -> k2
列4 -> k4
则列映射的书写顺序应如下:
(k1, k3, k2, k4)
源文件中的列数量多于表中的列
假设表中有 k1,k2,k3
3列。我们希望的导入映射关系如下:
列1 -> k1
列2 -> k3
列3 -> k2
则列映射的书写顺序应如下:
(k1, k3, k2, tmpk4)
其中 tmpk4
为一个自定义的、表中不存在的列名。Doris 会忽略这个不存在的列名。
源文件中的列数量少于表中的列,使用默认值填充
假设表中有 k1,k2,k3,k4,k5
5列。我们希望的导入映射关系如下:
列1 -> k1
列2 -> k3
列3 -> k2
这里我们仅使用源文件中的前3列。k4,k5
两列希望使用默认值填充。
则列映射的书写顺序应如下:
(k1, k3, k2)
如果 k4,k5
列有默认值,则会填充默认值。否则如果是 nullable
的列,则会填充 null
值。否则,导入作业会报错。
前置过滤是对读取到的原始数据进行一次过滤。目前仅支持 BROKER LOAD 和 ROUTINE LOAD。
前置过滤有以下应用场景:
转换前做过滤
希望在列映射和转换前做过滤的场景。能够先行过滤掉部分不需要的数据。
过滤列不存在于表中,仅作为过滤标识
比如源数据中存储了多张表的数据(或者多张表的数据写入了同一个 Kafka 消息队列)。数据中每行有一列表名来标识该行数据属于哪个表。用户可以通过前置过滤条件来筛选对应的表数据进行导入。
列转换功能允许用户对源文件中列值进行变换。目前 Doris 支持使用绝大部分内置函数、用户自定义函数进行转换。
注:自定义函数隶属于某一数据库下,在使用自定义函数进行转换时,需要用户对这个数据库有读权限。
转换操作通常是和列映射一起定义的。即先对列进行映射,再进行转换。下面我们通过示例说明:
假设源文件有4列,内容如下(表头列名仅为方便表述,实际并无表头):
列1 | 列2 | 列3 | 列4 |
---|---|---|---|
1 | 100 | beijing | 1.1 |
2 | 200 | shanghai | 1.2 |
3 | 300 | guangzhou | 1.3 |
\N | 400 | chongqing | 1.4 |
将源文件中的列值经转换后导入表中
假设表中有 k1,k2,k3,k4
4列。我们希望的导入映射和转换关系如下:
列1 -> k1
列2 * 100 -> k3
列3 -> k2
列4 -> k4
则列映射的书写顺序应如下:
(k1, tmpk3, k2, k4, k3 = tmpk3 * 100)
这里相当于我们将源文件中的第2列命名为 tmpk3
,同时指定表中 k3
列的值为 tmpk3 * 100
。最终表中的数据如下:
k1 | k2 | k3 | k4 |
---|---|---|---|
1 | beijing | 10000 | 1.1 |
2 | shanghai | 20000 | 1.2 |
3 | guangzhou | 30000 | 1.3 |
null | chongqing | 40000 | 1.4 |
通过 case when 函数,有条件的进行列转换。
假设表中有 k1,k2,k3,k4
4列。我们希望对于源数据中的 beijing, shanghai, guangzhou, chongqing
分别转换为对应的地区id后导入:
列1 -> k1
列2 -> k2
列3 进行地区id转换后 -> k3
列4 -> k4
则列映射的书写顺序应如下:
(k1, k2, tmpk3, k4, k3 = case tmpk3 when "beijing" then 1 when "shanghai" then 2 when "guangzhou" then 3 when "chongqing" then 4 else null end)
最终表中的数据如下:
k1 | k2 | k3 | k4 |
---|---|---|---|
1 | 100 | 1 | 1.1 |
2 | 200 | 2 | 1.2 |
3 | 300 | 3 | 1.3 |
null | 400 | 4 | 1.4 |
将源文件中的 null 值转换成 0 导入。同时也进行示例2中的地区id转换。
假设表中有 k1,k2,k3,k4
4列。在对地区id转换的同时,我们也希望对于源数据中 k1 列的 null 值转换成 0 导入:
列1 如果为null 则转换成0 -> k1
列2 -> k2
列3 -> k3
列4 -> k4
则列映射的书写顺序应如下:
(tmpk1, k2, tmpk3, k4, k1 = ifnull(tmpk1, 0), k3 = case tmpk3 when "beijing" then 1 when "shanghai" then 2 when "guangzhou" then 3 when "chongqing" then 4 else null end)
最终表中的数据如下:
k1 | k2 | k3 | k4 |
---|---|---|---|
1 | 100 | 1 | 1.1 |
2 | 200 | 2 | 1.2 |
3 | 300 | 3 | 1.3 |
0 | 400 | 4 | 1.4 |
经过列映射和转换后,我们可以通过过滤条件将不希望导入到Doris中的数据进行过滤。下面我们通过示例说明:
假设源文件有4列,内容如下(表头列名仅为方便表述,实际并无表头):
列1 | 列2 | 列3 | 列4 |
---|---|---|---|
1 | 100 | beijing | 1.1 |
2 | 200 | shanghai | 1.2 |
3 | 300 | guangzhou | 1.3 |
\N | 400 | chongqing | 1.4 |
在列映射和转换缺省的情况下,直接过滤
假设表中有 k1,k2,k3,k4
4列。我们可以在缺省列映射和转换的情况下,直接定义过滤条件。如我们希望只导入源文件中第4列为大于 1.2 的数据行,则过滤条件如下:
where k4 > 1.2
最终表中的数据如下:
k1 | k2 | k3 | k4 |
---|---|---|---|
3 | 300 | guangzhou | 1.3 |
null | 400 | chongqing | 1.4 |
缺省情况下,Doris 会按照顺序进行列映射,因此源文件中的第4列自动被映射到表中的 k4
列。
对经过列转换的数据进行过滤
假设表中有 k1,k2,k3,k4
4列。在 列转换 示例中,我们将省份名称转换成了id。这里我们想过滤掉 id 为 3 的数据。则转换、过滤条件如下:
(k1, k2, tmpk3, k4, k3 = case tmpk3 when "beijing" then 1 when "shanghai" then 2 when "guangzhou" then 3 when "chongqing" then 4 else null end)
where k3 != 3
最终表中的数据如下:
k1 | k2 | k3 | k4 |
---|---|---|---|
1 | 100 | 1 | 1.1 |
2 | 200 | 2 | 1.2 |
null | 400 | 4 | 1.4 |
这里我们看到,执行过滤时的列值,为经过映射和转换后的最终列值,而不是原始数据。
多条件过滤
假设表中有 k1,k2,k3,k4
4列。我们想过滤掉 k1
列为 null
的数据,同时过滤掉 k4
列小于 1.2 的数据,则过滤条件如下:
where k1 is not null and k4 >= 1.2
最终表中的数据如下:
k1 | k2 | k3 | k4 |
---|---|---|---|
2 | 200 | 2 | 1.2 |
3 | 300 | 3 | 1.3 |
导入作业中被处理的数据行可以分为如下三种:
Filtered Rows
因数据质量不合格而被过滤掉的数据。数据质量不合格包括类型错误、精度错误、字符串长度超长、文件列数不匹配等数据格式问题,以及因没有对应的分区而被过滤掉的数据行。
Unselected Rows
这部分为因 preceding filter
或 where
列过滤条件而被过滤掉的数据行。
Loaded Rows
被正确导入的数据行。
Doris 的导入任务允许用户设置最大错误率(max_filter_ratio
)。如果导入的数据的错误率低于阈值,则这些错误行将被忽略,其他正确的数据将被导入。
错误率的计算方式为:
#Filtered Rows / (#Filtered Rows + #Loaded Rows)
也就是说 Unselected Rows
不会参与错误率的计算。
严格模式(strict_mode)为导入操作中的一个参数配置。该参数会影响某些数值的导入行为和最终导入的数据。
本文档主要说明如何设置严格模式,以及严格模式产生的影响。
严格模式默认情况下都为 False,即关闭状态。
不同的导入方式设置严格模式的方式不尽相同。
BROKER LOAD
LOAD LABEL example_db.label1
(
DATA INFILE("bos://my_bucket/input/file.txt")
INTO TABLE `my_table`
COLUMNS TERMINATED BY ","
)
WITH BROKER bos
(
"bos_endpoint" = "http://bj.bcebos.com",
"bos_accesskey" = "xxxxxxxxxxxxxxxxxxxxxxxxxx",
"bos_secret_accesskey"="yyyyyyyyyyyyyyyyyyyyyyyyyy"
)
PROPERTIES
(
"strict_mode" = "true"
)
STREAM LOAD
curl --location-trusted -u user:passwd \
-H "strict_mode: true" \
-T 1.txt \
http://host:port/api/example_db/my_table/_stream_load
ROUTINE LOAD
CREATE ROUTINE LOAD example_db.test_job ON my_table
PROPERTIES
(
"strict_mode" = "true"
)
FROM KAFKA
(
"kafka_broker_list" = "broker1:9092,broker2:9092,broker3:9092",
"kafka_topic" = "my_topic"
);
INSERT
通过会话变量设置:
SET enable_insert_strict = true;
INSERT INTO my_table ...;
严格模式的意思是,对于导入过程中的列类型转换进行严格过滤。
严格过滤的策略如下:
对于列类型转换来说,如果开启严格模式,则错误的数据将被过滤。这里的错误数据是指:原始数据并不为 null
,而在进行列类型转换后结果为 null
的这一类数据。
这里说指的 列类型转换
,并不包括用函数计算得出的 null
值。
对于导入的某列类型包含范围限制的,如果原始数据能正常通过类型转换,但无法通过范围限制的,严格模式对其也不产生影响。例如:如果类型是 decimal(1,0)
, 原始数据为 10,则属于可以通过类型转换但不在列声明的范围内。这种数据 strict 对其不产生影响。
以列类型为 TinyInt 来举例:
原始数据类型 | 原始数据举例 | 转换为 TinyInt 后的值 | 严格模式 | 结果 |
---|---|---|---|---|
空值 | \N | NULL | 开启或关闭 | NULL |
非空值 | "abc" or 2000 | NULL | 开启 | 非法值(被过滤) |
非空值 | "abc" | NULL | 关闭 | NULL |
非空值 | 1 | 1 | 开启或关闭 | 正确导入 |
说明:
- 表中的列允许导入空值
abc
及2000
在转换为 TinyInt 后,会因类型或精度问题变为 NULL。在严格模式开启的情况下,这类数据将会被过滤。而如果是关闭状态,则会导入null
。
以列类型为 Decimal(1,0) 举例
原始数据类型 | 原始数据举例 | 转换为 Decimal 后的值 | 严格模式 | 结果 |
---|---|---|---|---|
空值 | \N | null | 开启或关闭 | NULL |
非空值 | aaa | NULL | 开启 | 非法值(被过滤) |
非空值 | aaa | NULL | 关闭 | NULL |
非空值 | 1 or 10 | 1 or 10 | 开启或关闭 | 正确导入 |
说明:
- 表中的列允许导入空值
abc
在转换为 Decimal 后,会因类型问题变为 NULL。在严格模式开启的情况下,这类数据将会被过滤。而如果是关闭状态,则会导入null
。10
虽然是一个超过范围的值,但是因为其类型符合 decimal 的要求,所以严格模式对其不产生影响。10
最后会在其他导入处理流程中被过滤。但不会被严格模式过滤。