<think>我们首先需要解决两个问题:
1. 详细介绍Microsoft SQL Server 2014中的BULK INSERT语句的参数和用法。
2. 使用Delphi 11将UTF-8格式的二维数组通过BULK INSERT插入到SQL Server数据库表中。
由于BULK INSERT要求数据源是文件,因此我们需要先将二维数组写入一个临时的CSV文件(UTF-8编码),然后使用BULK INSERT从该文件中导入数据。
步骤:
1. 在Delphi中,将二维数组转换为CSV格式的字符串,并保存为UTF-8编码的文本文件。
2. 使用ADO或FDConnection等组件执行BULK INSERT语句,指定该文件路径。
注意:SQL Server的BULK INSERT对文件路径有访问权限要求,需要确保SQL Server服务账户有权限读取该文件。
下面详细说明:
### 一、BULK INSERT 参数详解
语法:
```sql
BULK INSERT [ [ database_name . ] schema_name . ] table_name
FROM 'data_file'
[ WITH
(
[ [ , ] BATCHSIZE = batch_size ]
[ [ , ] CHECK_CONSTRAINTS ]
[ [ , ] CODEPAGE = { 'ACP' | 'OEM' | 'RAW' | 'code_page' } ]
[ [ , ] DATAFILETYPE =
{ 'char' | 'native' | 'widechar' | 'widenative' } ]
[ [ , ] FIELDTERMINATOR = 'field_terminator' ]
[ [ , ] FIRSTROW = first_row ]
[ [ , ] FIRE_TRIGGERS ]
[ [ , ] FORMATFILE = 'format_file_path' ]
[ [ , ] KEEPIDENTITY ]
[ [ , ] KEEPNULLS ]
[ [ , ] KILOBYTES_PER_BATCH = kilobytes_per_batch ]
[ [ , ] LASTROW = last_row ]
[ [ , ] MAXERRORS = max_errors ]
[ [ , ] ORDER ( { column [ ASC | DESC ] } [ ,...n ] ) ]
[ [ , ] ROWS_PER_BATCH = rows_per_batch ]
[ [ , ] ROWTERMINATOR = 'row_terminator' ]
[ [ , ] TABLOCK ]
[ [ , ] ERRORFILE = 'file_name' ]
)
]
```
常用参数说明:
1. **BATCHSIZE**:指定每批导入的行数。默认是整个文件。
2. **CHECK_CONSTRAINTS**:指定在导入期间检查表约束。默认情况下,忽略约束。
3. **CODEPAGE**:指定文件中数据的代码页。对于UTF-8文件,使用`CODEPAGE='65001'`。
4. **DATAFILETYPE**:指定文件类型。对于UTF-8文本文件,使用`DATAFILETYPE='char'`;对于Unicode(UTF-16)文件,使用`'widechar'`。注意:UTF-8文件应使用`char`并配合`CODEPAGE='65001'`。
5. **FIELDTERMINATOR**:指定列分隔符,默认为制表符`\t`。对于CSV文件,通常为逗号`','`。
6. **FIRSTROW**:指定开始导入的第一行的行号(从1开始计数)。如果文件有标题行,可以设置为2跳过标题。
7. **FIRE_TRIGGERS**:指定是否触发插入触发器。默认不触发。
8. **FORMATFILE**:指定格式文件的路径,用于非标准格式的文件(如列顺序不一致)。
9. **KEEPNULLS**:指定在导入过程中,空列应保留NULL值,而不是使用列的默认值。
10. **LASTROW**:指定导入的最后一行行号。
11. **MAXERRORS**:指定允许的最大错误数,超过则终止导入。默认为10。
12. **ROWTERMINATOR**:指定行终止符。在Windows中通常为`'\r\n'`。
13. **TABLOCK**:在导入期间获取表级锁,可以提高性能。
14. **ERRORFILE**:指定错误文件的路径,用于记录格式错误无法导入的行。
### 二、Delphi 11实现步骤
假设我们有一个二维数组`Data: TArray<TArray<string>>`,其中每个内层数组代表一行,每行有两个元素(对应两列)。
步骤:
1. 将二维数组转换为CSV格式的字符串(使用逗号分隔列,换行符分隔行)。
2. 将字符串保存为UTF-8编码的临时文件(注意:Delphi默认的字符串是Unicode,所以保存为UTF-8需要处理)。
3. 构造BULK INSERT语句,指定临时文件路径和参数(特别是CODEPAGE='65001')。
4. 使用ADOQuery或FDQuery执行该SQL语句。
示例代码(Delphi 11):
```delphi
uses
System.Classes, System.SysUtils, Data.DB, FireDAC.Comp.Client, System.IOUtils;
procedure InsertDataViaBulkInsert(Connection: TFDConnection; const Data: TArray<TArray<string>>);
var
CSV: TStringList;
I: Integer;
TempFileName: string;
SQL: string;
begin
// 创建CSV内容
CSV := TStringList.Create;
try
for I := 0 to High(Data) do
begin
// 每行两个元素,用逗号分隔
if Length(Data[I]) >= 2 then
CSV.Add(Format('"%s","%s"', [Data[I][0], Data[I][1]]));
end;
// 创建临时文件(使用系统临时目录)
TempFileName := TPath.GetTempFileName;
// 将CSV内容保存为UTF-8文件(带BOM)
CSV.SaveToFile(TempFileName, TEncoding.UTF8);
// 构造BULK INSERT语句
SQL :=
'BULK INSERT JR..time_old ' + // 数据库名..表名,如果JR是数据库名,则写成[JR].dbo.time_old
'FROM ''' + TempFileName + ''' ' +
'WITH ( ' +
'CODEPAGE = ''65001'', ' + // UTF-8编码
'DATAFILETYPE = ''char'', ' + // 文本文件
'FIELDTERMINATOR = '','', ' + // 列分隔符为逗号,注意:如果字段中包含逗号,需要用双引号括起来
'ROWTERMINATOR = ''\n'', ' + // 行终止符,这里使用\n(因为TStringList默认使用当前系统的行结束符,但保存为文本时是系统默认,这里我们指定为\n,实际在Windows中可能是\r\n,但BULK INSERT也支持\n)
'KEEPNULLS, ' +
'FIRSTROW = 1 ' + // 从第一行开始(没有标题行)
')';
// 执行SQL
Connection.ExecSQL(SQL);
finally
CSV.Free;
// 删除临时文件
if FileExists(TempFileName) then
DeleteFile(TempFileName);
end;
end;
```
注意:
- 如果字段内容包含逗号或换行符,需要将字段用双引号括起来,并且双引号内的双引号用两个双引号表示(CSV标准)。上面的Format函数中已经用双引号将每个字段括起来了。
- 行终止符:在Windows中通常为`\r\n`,但BULK INSERT也支持`\n`。上面我们使用了`\n`,因为TStringList在保存时默认使用当前系统的行结束符(在Windows中是`\r\n`),但我们在ROWTERMINATOR中指定为`'\n'`可能不匹配。因此,我们可以改为使用`ROWTERMINATOR = ''0x0a''`(十六进制表示换行)或者使用`ROWTERMINATOR = ''' + #10 + ''''`(在Delphi字符串中表示为`#13#10`,但实际保存的文件是`\r\n`,所以应该指定为`'\r\n'`)。因此,修改如下:
修正行终止符部分:
由于TStringList在保存到文件时,在Windows平台下默认使用`\r\n`作为行结束符,所以ROWTERMINATOR应该设置为`'\r\n'`。但是,在SQL语句中表示回车换行需要写成`'\r\n'`,并且在字符串中需要转义单引号,所以:
```delphi
SQL :=
'BULK INSERT JR..time_old ' +
'FROM ''' + TempFileName + ''' ' +
'WITH ( ' +
'CODEPAGE = ''65001'', ' +
'DATAFILETYPE = ''char'', ' +
'FIELDTERMINATOR = '','', ' +
'ROWTERMINATOR = ''' + #13#10 + ''', ' + // 注意:这里用两个单引号表示字符串中的单引号,然后中间是转义后的行终止符
'KEEPNULLS, ' +
'FIRSTROW = 1 ' +
')';
```
但是,这样写会使得SQL语句中的行终止符部分变成`ROWTERMINATOR = '回车换行',`,这显然不对。正确做法是使用转义字符表示`\r\n`,即:
'ROWTERMINATOR = ''' + '\r\n' + ''', '
但是,在SQL语句中,我们传递的是字符串,所以应该用两个单引号表示一个单引号,而`\r\n`是普通字符,所以直接写即可。但是,在BULK INSERT语句中,字符串内的`\r\n`会被解释为回车换行。因此,我们可以这样写:
'ROWTERMINATOR = ''\r\n'', '
所以,完整的SQL字符串构造:
```delphi
SQL :=
'BULK INSERT JR..time_old ' +
'FROM ''' + TempFileName + ''' ' +
'WITH ( ' +
'CODEPAGE = ''65001'', ' + // UTF-8编码
'DATAFILETYPE = ''char'', ' + // 文本文件
'FIELDTERMINATOR = '','', ' + // 列分隔符为逗号
'ROWTERMINATOR = ''\r\n'', ' + // 行终止符
'KEEPNULLS, ' +
'FIRSTROW = 1 ' + // 从第一行开始(没有标题行)
')';
```
另外,如果表在特定的架构下(如dbo),则应该写为`[JR].[dbo].[time_old]`。
权限问题:临时文件可能位于临时目录,需要确保SQL Server服务账户有权限读取该目录。如果权限不足,可以考虑将临时文件保存到SQL Server有权限访问的目录(如SQL Server数据目录或共享目录)。
### 三、示例调用
假设我们有一个二维数组:
```delphi
var
Data: TArray<TArray<string>>;
begin
SetLength(Data, 2);
Data[0] := ['值1', '值2'];
Data[1] := ['值3', '值4'];
InsertDataViaBulkInsert(FDConnection1, Data);
end;
```
注意:如果数据量很大,生成CSV字符串可能会占用较多内存,可以考虑使用流式写入文件。
如果遇到权限问题,参考引用[2]中的方法,将临时文件保存到SQL Server的Data目录(通常有权限)或者调整权限。
### 相关问题
1. **如何跳过BULK INSERT导入过程中的错误?**
2. **Delphi中如何将二维数组导出为CSV文件(UTF-8编码)?**
3. **SQL Server BULK INSERT导入时出现代码页转换错误如何解决?**
4. **如何提高BULK INSERT导入大量数据的性能?**
引用说明:
- 引用[1]中提到了使用存储过程循环插入,但这里我们使用BULK INSERT更高效。
- 引用[2]提到了附加数据库权限问题,类似地,BULK INSERT也需要文件访问权限。
- 引用[3]提醒了SQL注入问题,但我们的方法中,SQL语句是拼接文件路径,而文件路径是临时生成的,相对安全(但也要注意文件名中不能有特殊字符,这里使用系统生成的临时文件名是安全的)。
注意:在构造SQL语句时,我们使用了临时文件名,该文件名是系统生成的,不包含用户输入,因此不会存在SQL注入问题。
如果遇到问题,可以检查SQL Server错误日志,或者使用ERRORFILE参数记录错误行。</think>### Microsoft SQL Server 2014 BULK INSERT 参数详解
#### 核心语法
```sql
BULK INSERT [database].[schema].[table]
FROM 'data_file_path'
WITH (
[parameter = value],
...
);
```
#### 完整参数列表及解释
1. **BATCHSIZE**
- 每批插入行数:`BATCHSIZE = 1000`
- 优化大文件导入性能
2. **CHECK_CONSTRAINTS**
- 导入时检查约束:`CHECK_CONSTRAINTS`
- 默认忽略约束验证
3. **CODEPAGE**
- **UTF-8文件必须设置**:`CODEPAGE = '65001'`
- 其他选项:`ACP`(ANSI), `OEM`(系统默认)
4. **DATAFILETYPE**
- 文件类型:`char`(文本), `widechar`(Unicode)
- UTF-8推荐:`DATAFILETYPE = 'char'`
5. **FIELDTERMINATOR**
- 列分隔符:`FIELDTERMINATOR = ','`
- 二维数组常用逗号分隔
6. **FIRSTROW**
- 起始行号:`FIRSTROW = 2`
- 跳过标题行
7. **FIRE_TRIGGERS**
- 触发插入触发器:`FIRE_TRIGGERS`
- 默认不触发
8. **FORMATFILE**
- 格式文件路径:`FORMATFILE = 'C:\format.xml'`
- 定义列映射规则
9. **KEEPIDENTITY**
- 保留标识列值:`KEEPIDENTITY`
10. **KEEPNULLS**
- 保留空值:`KEEPNULLS`
11. **ROWTERMINATOR**
- 行终止符:`ROWTERMINATOR = '\r\n'`
- Windows系统推荐
12. **TABLOCK**
- 表级锁:`TABLOCK`
- 提升导入性能
13. **ERRORFILE**
- 错误日志:`ERRORFILE = 'C:\errors.log'`
14. **MAXERRORS**
- 最大容错数:`MAXERRORS = 10`
- 超过则终止导入
---
### Delphi 11 实现 UTF-8 二维数组导入
#### 实现步骤
1. **准备数据文件**
将二维数组转换为CSV格式并保存为UTF-8文件:
```delphi
var
CSV: TStringList;
I: Integer;
begin
CSV := TStringList.Create;
try
CSV.Text := 'c_tr1,c_tr2' + sLineBreak; // 列标题
for I := 0 to High(ctime) do
CSV.Add(Format('%s,%s', [ctime[I][0], ctime[I][1]]));
CSV.SaveToFile('C:\temp\data.csv', TEncoding.UTF8);
finally
CSV.Free;
end;
end;
```
2. **执行BULK INSERT**
使用TFDConnection执行SQL命令:
```delphi
procedure BulkInsertData(Connection: TFDConnection);
var
SQL: string;
begin
SQL :=
'BULK INSERT JR.dbo.time_old ' +
'FROM ''C:\temp\data.csv'' ' +
'WITH ( ' +
'CODEPAGE = ''65001'', ' +
'DATAFILETYPE = ''char'', ' +
'FIELDTERMINATOR = '','', ' +
'ROWTERMINATOR = ''\r\n'', ' +
'FIRSTROW = 2, ' + // 跳过标题行
'KEEPNULLS, ' +
'TABLOCK ' +
')';
Connection.ExecSQL(SQL);
end;
```
3. **权限处理**
参考引用[2]解决权限问题:
- 将文件放在SQL Server的`DATA`目录
- 授予SQL Server服务账户文件读写权限
- 修改服务登录身份为`Local Service`
#### 关键注意事项
1. **UTF-8编码处理**
- 文件必须包含BOM头(Delphi自动添加)
- 必须设置`CODEPAGE=65001`
2. **特殊字符处理**
若数据包含逗号,需用引号包裹字段:
```delphi
CSV.Add(Format('"%s","%s"', [ctime[I][0], ctime[I][1]]));
```
3. **性能优化**
- 使用`TABLOCK`减少锁竞争
- 设置`BATCHSIZE=5000`分批提交
- 导入前禁用索引
4. **错误处理**
```sql
ERRORFILE = 'C:\errors.log',
MAXERRORS = 100
```
---
### 替代方案:参数化插入
若数据量小,推荐参数化插入防止SQL注入(参考引用[3]):
```delphi
procedure SafeInsert(Connection: TFDConnection);
var
Query: TFDQuery;
I: Integer;
begin
Query := TFDQuery.Create(nil);
try
Query.Connection := Connection;
Query.SQL.Text := 'INSERT INTO time_old (c_tr1, c_tr2) VALUES (:val1, :val2)';
for I := 0 to High(ctime) do begin
Query.ParamByName('val1').AsString := ctime[I][0];
Query.ParamByName('val2').AsString := ctime[I][1];
Query.ExecSQL;
end;
finally
Query.Free;
end;
end;
```
---
### 相关问题
1. **如何解决BULK INSERT导入时的权限拒绝错误?**
2. **Delphi中如何验证UTF-8文件的BOM头?**
3. **大批量数据导入时如何优化Delphi内存使用?**
4. **SQL Server 2014是否支持直接导入JSON格式数据?**
[^1]: 存储过程循环插入示例
[^2]: SQL Server文件权限解决方案
[^3]: 参数化插入防SQL注入