跳到主要内容

CSV 自动检测

使用 read_csv 时,系统会借助 CSV sniffer 自动推断应如何读取 CSV 文件。 这一步是必要的,因为 CSV 文件并非自描述格式,而且存在许多不同“方言(dialect)”。自动检测大致包含以下步骤:

  • 检测 CSV 文件方言(分隔符、引号规则、转义字符)。
  • 检测每一列的数据类型。
  • 检测文件是否包含表头行。

默认情况下,系统会尝试自动检测全部选项。不过,用户也可以逐项覆盖这些选项,这在系统判断错误时很有用。例如,如果分隔符识别错了,我们可以在 read_csv 中显式指定分隔符来覆盖(如 read_csv('file.csv', delim = '|'))。

样本大小

类型检测是基于文件样本进行的。 你可以通过 sample_size 参数修改样本大小。 默认样本大小是 20,480 行。 将 sample_size 设为 -1 表示对整个文件采样:

SELECT * FROM read_csv('my_csv_file.csv', sample_size = -1);

采样方式取决于文件类型。若读取的是磁盘上的普通文件,系统会跳转到文件不同位置进行采样。 若读取的是无法随机跳转的文件(如 .gz 压缩 CSV 或 stdin),则只能从文件开头采样。

sniff_csv 函数

你可以通过 sniff_csv(filename) 单独运行 CSV sniffer。该函数会以单行表的形式返回检测到的 CSV 属性。 sniff_csv 还支持可选参数 sample_size,用于配置采样行数。

FROM sniff_csv('my_file.csv');
FROM sniff_csv('my_file.csv', sample_size = 1000);
列名说明示例
Delimiter分隔符,
Quote引号字符"
Escape转义字符\
NewLineDelimiter换行分隔符\r\n
Comment注释字符#
SkipRows跳过的行数1
HasHeaderCSV 是否有表头true
ColumnsSTRUCT 列表(LIST)编码的列类型({'name': 'VARCHAR', 'age': 'BIGINT'})
DateFormat日期格式%d/%m/%Y
TimestampFormat时间戳格式%Y-%m-%dT%H:%M:%S.%f
UserArguments调用 sniff_csv 时使用的参数sample_size = 1000
Prompt可直接用于读取 CSV 的提示命令FROM read_csv('my_file.csv', auto_detect=false, delim=',', ...)

Prompt

Prompt 列包含了一条 SQL 命令,内含 sniffer 检测到的配置。

-- use line mode in CLI to get the full command
.mode line
SELECT Prompt FROM sniff_csv('my_file.csv');
Prompt = FROM read_csv('my_file.csv', auto_detect=false, delim=',', quote='"', escape='"', new_line='\n', skip=0, header=true, columns={...});

检测步骤

方言检测

方言检测会尝试使用候选值集合去解析样本。最终选出的方言应同时满足:

  1. 每行列数一致;
  2. 每行列数尽可能多。

自动方言检测会考虑以下参数组合。

参数候选值
delim, `
quote" ' (empty)
escape" ' \ (empty)

考虑示例文件 flights.csv

FlightDate|UniqueCarrier|OriginCityName|DestCityName
1988-01-01|AA|New York, NY|Los Angeles, CA
1988-01-02|AA|New York, NY|Los Angeles, CA
1988-01-03|AA|New York, NY|Los Angeles, CA

在这个文件中,方言检测过程如下:

  • 若以 | 分割,每行都会被分成 4 列。
  • 若以 , 分割,第 2-4 行是 3 列,而第一行是 1 列。
  • 若以 ; 分割,每行都是 1 列。
  • 若以 \t 分割,每行都是 1 列。

在本例中,系统会选择 | 作为分隔符。因为所有行列数一致,且每行列数大于 1,说明该分隔符确实存在于 CSV 文件中。

类型检测

完成方言检测后,系统会尝试推断每一列的类型。注意,这一步只在调用 read_csv 时执行;对于 COPY 语句,则会直接使用目标表的列类型。

类型检测通过将每列值尝试转换到候选类型来完成。若转换失败,该候选类型会从该列的候选集合中移除。处理完全部样本后,会选择剩余候选类型中优先级最高的一个。默认候选类型(按优先级从高到低)如下:

Types
NULL
BOOLEAN
TIME
DATE
TIMESTAMP
TIMESTAMPTZ
BIGINT
DOUBLE
VARCHAR

所有值都可以转换为 VARCHAR,因此它优先级最低:当列无法转换为其他类型时,会回退到 VARCHAR。 在 flights.csv 中,FlightDate 会被识别为 DATE,其他列会被识别为 VARCHAR

你可以通过 auto_type_candidates 显式指定 CSV 读取器应考虑的候选类型集合。作为回退类型的 VARCHAR 无论是否显式指定都会被纳入候选。

下面是可通过 auto_type_candidates 指定的额外候选类型(按优先级):

Types
TINYINT
SMALLINT
INTEGER
DECIMAL
FLOAT

虽然可自动检测的数据类型看起来有限,但你可以使用下一节介绍的 types 选项将 CSV 读取器配置为读取任意复杂类型。

你可以通过 all_varchar 选项完全禁用类型检测。启用后,所有列都会保持为 VARCHAR(即 CSV 原始读入形式)。

注意,字段是否带引号(如 "42"42)不会影响类型检测。 带引号字段并不会被强制转成 VARCHAR;sniffer 仍会尝试选择优先级最高的可行类型。

覆盖类型检测结果

可通过 types 选项逐列覆盖检测出的类型。该选项支持两种形式:

  • 类型定义列表(如 types = ['INTEGER', 'VARCHAR', 'DATE']):按列在 CSV 中出现顺序覆盖类型。
  • nametype 映射:按列名覆盖指定列类型(如 types = {'quarter': 'INTEGER'})。

types 可指定的列类型并不局限于 auto_type_candidates 支持的集合:任何合法类型定义都可用于 types。(可通过 typeof() 获取合法类型定义,或查看 DESCRIBE 结果中的 column_type 列。)

sniff_csv()Column 字段会返回列名与类型组成的结构体,可作为覆盖类型时的参考。

表头检测

表头检测会判断候选表头行在类型上是否与其他数据行明显不同。例如在 flights.csv 中,表头行全是 VARCHAR,而数据行中的 FlightDate 列是 DATE。因此系统会将第一行识别为表头,并从中提取列名。

对于无表头文件,列名会自动生成为 column0column1 等。

注意:若所有列均为 VARCHAR,系统可能无法正确检测表头,因为无法区分表头行与数据行。此时系统会默认文件有表头。你可以通过将 header 设为 false 来覆盖该行为。

日期与时间戳

Goose 默认使用 ISO 8601 格式来解析时间戳、日期和时间。但现实中并非所有数据都遵循该标准。因此 CSV 读取器还支持 dateformattimestampformat 选项,允许用户通过格式字符串指定解析方式。

在自动检测过程中,系统会尝试判断日期和时间是否采用了其他表示方式。但这并不总是可行,因为格式可能存在歧义。例如 01-02-2000 既可能表示 1 月 2 日,也可能表示 2 月 1 日。很多歧义可以通过更多数据消除,比如若后续出现 21-02-2000,就能确定格式应为 DD-MM-YYYY,因为 MM-DD-YYYY 不存在第 21 月。

若数据本身无法消除歧义,系统会按预设优先级选择日期格式。若系统选错,用户可手动设置 dateformattimestampformat

系统会对日期(dateformat)考虑以下格式。出现歧义时,上方格式优先级高于下方(即 ISO 8601 优先于 MM-DD-YYYY)。

dateformat
ISO 8601
%y-%m-%d
%Y-%m-%d
%d-%m-%y
%d-%m-%Y
%m-%d-%y
%m-%d-%Y

系统会对时间戳(timestampformat)考虑以下格式。出现歧义时,上方格式优先级高于下方。

timestampformat
ISO 8601
%y-%m-%d %H:%M:%S
%Y-%m-%d %H:%M:%S
%d-%m-%y %H:%M:%S
%d-%m-%Y %H:%M:%S
%m-%d-%y %I:%M:%S %p
%m-%d-%Y %I:%M:%S %p
%Y-%m-%d %H:%M:%S.%f