问题:
csharp
db.Insert<PersonEntity>(new Person
{
CreateTime = DateTime.Now,
CreateTimeOffset = DateTimeOffset.Now
}).ToSql();
这个输出应该是: 不带时区(如:"2026-01-01 02:03:04.567") 还是带时区(如: "2026-01-01 02:03:04.567+08:00")
一、观察 mysql 内部行为
mysql 关注的列类型是:timestamp 和 datetime
-
datetime: mysql存数据到datetime时候,将数据先转换成mysql使用的时区(如果指定了时区),取得时候就原封不动取出来(不带时区)。 注意:mysql 8.0.19及以上才允许insert时加时区;
-
timestamp: mysql存数据的时候,将数据先转换成UTC时间(如果指定了时区则换算,如果没指定时区就认为用的是mysql服务器时区),当取出的时候再转换成mysql使用的时区(输出结果中不带时区)。
看测试代码:
sql
-- 设置会话级时区为东九区
SET time_zone = '+09:00';
SELECT @@session.time_zone;
drop table if exists test
create table test(
t_datetime datetime,
t_timestamp timestamp
)
insert into test(t_datetime,t_timestamp) values
('2022-02-11 09:30:00+08:00','2022-02-11 09:30:00+08:00'),
('2022-02-11 09:30:00','2022-02-11 09:30:00')
select * from test
执行的结果:

二、观察 sqlserver 内部行为
在sqlserver中可以匹配 DateTime 和 DateTimeOffset 的列类型有 datetime datetime2 datetimeoffset 三个。
前两个不存储时区,而且在接受类似 '2026-01-04 02:03:04+09:00' 数据时是直接丢掉的,不存在存储时转到服务器所在时区,或统一转成utc查询的时候再转成服务器所在时区的逻辑。可以认为,这两个列类型不考虑时区、直接丢弃时区(如果用户指定时区的话)。
参考ai的回答,未验证
datetimeoffset 最容易理解了,完整存储时区。
三、观察 pgsql 内部行为
pgsql关注的列是 timestamp 和 timestamptz 他们的存储规则:
- timestamp: 不带时区, 写入时如果给了带时区的字符串会直接丢弃,读取的时候直接读,和pgsql服务器的时区没有关系。
- timestamptz: 带时区信息, 将数据先转换成UTC时间(如果指定了时区则换算,如果没指定时区就认为用的是服务器时区),当取出的时候再转换成服务器使用的时区(输出结果中带时区)。
看测试代码:
sql
SET TIME ZONE '-09:00'; -- 设置为东9区, pgsql 用的是 POSIX 规则 ,它和 ISO 正好是反的
select now() - (now() AT TIME ZONE 'UTC') AS utc_offset; -- 得到: 09:00:00
-- SET TIME ZONE 'Asia/Shanghai';
-- SHOW TIME ZONE;
drop table test
create table test(t_timestamp timestamp,t_timestamptz timestamptz)
insert into test(t_timestamp,t_timestamptz) values
('2022-02-11 09:30:00+08:00','2022-02-11 09:30:00+08:00'),
('2022-02-11 09:30:00','2022-02-11 09:30:00')
select * from test
效果:

四、观察主流 orm 库的处理原则
先说结论:
- dapper:原生不提供生成sql能力,无对比性;
- efcore:强制要求数据参数化,无对比性;
- freesql:
- mysql下:DateTime转成sql不带时区,直接不支持DateTimeOffset
- sqlserver下:DateTime转成sql不带时区,DateTimeOffset转成sql带时区
- sqlsugar:
- mysql下:DateTime转成sql不带时区,DateTimeOffset转成sql不带时区
- sqlserver下:DateTime转成sql不带时区,DateTimeOffset转成sql不带时区
测试的代码如下:
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FreeSql.Provider.MySqlConnector" Version="3.5.305" />
<PackageReference Include="FreeSql.Provider.SqlServer" Version="3.5.305" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.211" />
</ItemGroup>
</Project>
csharp
using SqlSugar;
namespace ConsoleApp1
{
public class Person
{
public DateTime CreateTime { get; set; }
public DateTimeOffset CreateTimeOffset { get; set; }
}
internal class Program
{
private static DateTime local = DateTime.Now; // Kind = Local
private static DateTime utc = DateTime.UtcNow; // Kind = Utc
private static DateTime unspecified = DateTime.Parse("2026-01-04 02:03:04"); // Unspecified
private static DateTimeOffset local_offset = DateTimeOffset.Now;
private static DateTimeOffset utc_offset = DateTimeOffset.UtcNow;
private static DateTimeOffset other_offset = DateTimeOffset.Parse("2026-01-04 02:03:04+09:00");
public static void Main()
{
FreeSqlMySql();
FreeSqlSqlServer();
SqlSugarMySql();
SqlSugarSqlServer();
}
private static void SqlSugarSqlServer()
{
var db = new SqlSugarClient(new ConnectionConfig
{
ConnectionString = "Data Source=.;Initial Catalog=test;User ID=sa;Password=123456;",
DbType = DbType.Sqlite,
});
var sql = db.Insertable(new[]
{
new Person { CreateTime = local, CreateTimeOffset = local_offset },
new Person { CreateTime = utc , CreateTimeOffset = utc_offset },
new Person { CreateTime = unspecified , CreateTimeOffset = other_offset }
})
.ToSqlString();
Console.WriteLine("--------------SqlSugar-SqlSever------------------");
Console.WriteLine(sql);
Console.WriteLine("=================================");
Console.WriteLine();
}
private static void SqlSugarMySql()
{
var db = new SqlSugarClient(new ConnectionConfig
{
ConnectionString = "Server=127.0.0.1;Database=test;Uid=root;Pwd=123456;",
DbType = DbType.MySql,
});
var sql = db.Insertable(new[]
{
new Person { CreateTime = local, CreateTimeOffset = local_offset },
new Person { CreateTime = utc , CreateTimeOffset = utc_offset },
new Person { CreateTime = unspecified , CreateTimeOffset = other_offset }
})
.ToSqlString();
Console.WriteLine("--------------SqlSugar-MySql------------------");
Console.WriteLine(sql);
Console.WriteLine("=================================");
Console.WriteLine();
}
private static void FreeSqlSqlServer()
{
var orm = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.SqlServer, "Data Source=.;Initial Catalog=test;User ID=sa;Password=123456;")
.UseNoneCommandParameter(true)
.Build();
var sql = orm.Insert<Person>()
.AppendData(new Person { CreateTime = local, CreateTimeOffset = local_offset })
.AppendData(new Person { CreateTime = utc, CreateTimeOffset = utc_offset })
.AppendData(new Person { CreateTime = unspecified, CreateTimeOffset = other_offset })
.ToSql();
Console.WriteLine("--------------freesql-SqlServer------------------");
Console.WriteLine(sql);
Console.WriteLine("=================================");
Console.WriteLine();
}
private static void FreeSqlMySql()
{
var orm = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql, "Server=127.0.0.1;Database=test;Uid=root;Pwd=123456;")
.UseNoneCommandParameter(true)
.Build();
var sql = orm.Insert<Person>()
.InsertColumns(i => new { i.CreateTime })//只能插入 DateTime
.AppendData(new Person { CreateTime = local, CreateTimeOffset = local_offset })
.AppendData(new Person { CreateTime = utc, CreateTimeOffset = utc_offset })
.AppendData(new Person { CreateTime = unspecified, CreateTimeOffset = other_offset })
.ToSql();
Console.WriteLine("--------------freesql-MySql------------------");
Console.WriteLine(sql);
Console.WriteLine("=================================");
Console.WriteLine();
}
}
}
测试效果:

五、最后总结:
C# 中的 DateTime 转 sql 不应该输出时区信息, 直接舍弃掉即可
C# 中的 DateTimeOffset 转 sql 应该始终输出时区信息
给出整理的表格:
text
SessionTimeZone(+09:00)
MySql ===========================================================================================================================================================================================================================================================================================================
| | | | | | | |
mysql datetime | User-provided has timezone | | Convert To SessionTimeZone | trim timezone | Internal Storage Format | read direct | final return |
(no timezone) | '2022-02-11 09:30:00+08:00' | | +01:00 (09:00 - 08:00) | | '2022-02-11 10:30:00' | | '2022-02-11 10:30:00' |
| | | | | | | |
| | | | | | | |
mysql datetime | User-provided no timezone | | | | Internal Storage Format | read direct | final return |
(no timezone) | '2022-02-11 09:30:00' | | | | '2022-02-11 09:30:00' | | '2022-02-11 09:30:00' |
| | | | | | | |
-------------------------------|-----------------------------------------|-----------------------------------------|--------------------------------------|------------------------------|-------------------------------------|-------------------------------------|------------------------------------------|
| | | | | | | |
mysql timestamp | User-provided has timezone | | Convert To UTC TimeZone zero | trim timezone | Internal Storage Format | Read-Convert To SessionTimeZone | final return |
(Internal Storage:utc zero) | '2022-02-11 09:30:00+08:00' | | -08:00 | | '2022-02-11 01:30:00Z' | +09:00 | '2022-02-11 10:30:00' |
| | | | | | | |
mysql timestamp | User-provided no timezone | Regard as SessionTimeZone | Convert To UTC TimeZone zero | trim timezone | Internal Storage Format | Read-Convert To SessionTimeZone | final return |
(Internal Storage:utc zero) | '2022-02-11 09:30:00' | '2022-02-11 09:30:00+09:00' | -09:00 | | '2022-02-11 00:30:00Z' | +09:00 | '2022-02-11 09:30:00' |
| | | | | | | |
SqlServer =====================|=========================================|=========================================|======================================|==============================|=====================================|=====================================|==========================================|
| | | | | | | |
sqlserver datetime/datetime2 | User-provided has timezone | | | trim timezone | Internal Storage Format | read direct | final return |
(no timezone) | '2022-02-11 09:30:00+08:00' | | | | '2022-02-11 09:30:00' | | '2022-02-11 09:30:00' |
| | | | | | | |
sqlserver datetime/datetime2 | User-provided no timezone | | | | Internal Storage Format | read direct | final return |
(no timezone) | '2022-02-11 09:30:00' | | | | '2022-02-11 09:30:00' | | '2022-02-11 09:30:00' |
| | | | | | | |
-------------------------------|-----------------------------------------|-----------------------------------------|--------------------------------------|------------------------------|-------------------------------------|-------------------------------------|------------------------------------------|
| | | | | | | |
sqlserver datetimeoffset | User-provided has timezone | | | | Internal Storage Format | read direct | final return |
(store timezone) | '2022-02-11 09:30:00+08:00' | | | | '2022-02-11 09:30:00+08:00' | | '2022-02-11 09:30:00+08:00' |
| | | | | | | |
sqlserver datetimeoffset | User-provided no timezone | Regard as ServerTimeZone | | | Internal Storage Format | read direct | final return |
(store timezone) | '2022-02-11 09:30:00' | '2022-02-11 09:30:00+09:00' | | | '2022-02-11 09:30:00+09:00' | | '2022-02-11 09:30:00+09:00' |
| | | | | | | |
PgSql =========================|=========================================|=========================================|======================================|==============================|=====================================|=====================================|==========================================|
| | | | | | | |
pgsql timestamp | User-provided has timezone | | | trim timezone | Internal Storage Format | read direct | final return |
(no timezone) | '2022-02-11 09:30:00+08:00' | | | | '2022-02-11 09:30:00' | | '2022-02-11 09:30:00' |
| | | | | | | |
pgsql timestamp | User-provided no timezone | | | | Internal Storage Format | read direct | final return |
(no timezone) | '2022-02-11 09:30:00' | | | | '2022-02-11 09:30:00' | | '2022-02-11 09:30:00' |
| | | | | | | |
-------------------------------|-----------------------------------------|-----------------------------------------|--------------------------------------|------------------------------|-------------------------------------|-------------------------------------|------------------------------------------|
| | | | | | | |
pgsql timestamptz | User-provided has timezone | | Convert To UTC TimeZone zero | trim timezone | Internal Storage Format | Read-Convert To SessionTimeZone | final return |
(Internal Storage:utc zero) | '2022-02-11 09:30:00+08:00' | | -08:00 | | '2022-02-11 01:30:00Z' | +09:00 | '2022-02-11 10:30:00+09:00' |
| | | | | | | |
pgsql timestamptz | User-provided no timezone | Regard as SessionTimeZone | Convert To UTC TimeZone zero | trim timezone | Internal Storage Format | Read-Convert To SessionTimeZone | final return |
(Internal Storage:utc zero) | '2022-02-11 09:30:00' | '2022-02-11 09:30:00+09:00' | -09:00 | | '2022-02-11 00:30:00Z' | +09:00 | '2022-02-11 09:30:00+09:00' |
| | | | | | | |
=========================================================================|=======================================================================================================================================================================================================================================
