前言:这里讨论的内容即是 《DBUtil》 中的时间的实现
一、在c#中相关的基础时间类型有(简称五大时间类型):
- 1.1 DateTime
- 1.2 DateTimeOffset
- 1.3 DateOnly
- 1.4 TimeOnly
- 1.5 TimeSpan
其中,DateTime 和 DateTimeOffset 可以记录时区信息, 其他的都和时区没关系。
DateTime 记录时区的方式,是通过一个 Kind 字段区分三种情况:UTC时区, 本地时区, 未指定时区
DateTimeOffset 记录了具体偏移量(属性:Offset)
它们的默认json序列化示例如:
json
{
"Id": 1,
"DateTime": "2025-11-26T20:11:26.1358717+08:00",
"DateTimeOffset": "2025-11-26T20:11:26.1429332+08:00",
"DateOnly": "2025-11-26",
"TimeOnly": "20:11:26.1437892",
"TimeSpan": "1.02:03:05.6780000"
}
我们可以指定格式化字符串以达到和json序列化相同的目的(注意: 虽然毫秒部分是7位,但db中一般都支持到6位,超出的自动截断):
csharp
var str = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFzzz");
//"2025-11-29T10:51:10.7098014+08:00"
var str2 = DateTimeOffset.Now.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFzzz");
//"2025-11-29T10:51:10.7098967+08:00"
var str3 = DateOnly.FromDateTime(DateTime.Now).ToString("yyyy-MM-dd");
//"2025-11-29"
var str4 = TimeOnly.FromDateTime(DateTime.Now).ToString("HH:mm:ss.FFFFFFF");
//"10:51:10.7099133"
var str5 = (DateTime.Parse("2000-01-01T01:02:03.456789+08:00") - DateTime.Parse("1900-01-01T00:00:00+08:00")).ToString();
//"36524.01:02:03.4567890"
二、明确探讨的存取问题
接下来我们要探讨的是:
- 写入时,如何将 c# 中的五大时间类型翻译成sql 如:我们有一个类
public class Person { prop DateTime CreateTime }那么我们如何将它生成sql - 读取时,如何从 db驱动中读取数据翻译到 c# 中的五大时间类型 如:我们使用驱动(如:pgsql 的 npgsql, mysql 的MySqlConnector)读取到的数据是什么样的,怎么映射到 c# 中 的五大时间类型(或者不需要映射)
三、统一的时间格式存储之字符串
这是一个通用的存储方法(对 sqlite 来说是主要的存储方法)。
这种方法是明确知道 db 中以字符串存储的时间。
怎么才算明确知道呢?
比如:用户的类是这样定义的
csharppublic class Person { [Column(TypeName = "varchar(50)")] public DateTime CreateTime { set; get; } }或者是当前操作的db为
sqlite(sqlite中没有专门的时间类型,只能默认认为存储为字符串)。
3.1 写入时,如何生成sql
考虑到存进去后的时间还可能要参与排序,所以需要一个统一的便于排序的格式,而不能是随便定义的。
比如:
- 不能是一会存储为
2020-01-02 03:04:05一会存储为2020-01-02T03:04:05+08:00 - 也不能毫秒部分是可省略的格式,如:
yyyy-MM-dd HH:mm:ss.FFFzzz下面两个一会有毫秒一会没有毫秒,不能参与排序csharpvar str = DateTime.Parse("2025-02-01 01:02:03+08:00").ToString("yyyy-MM-dd HH:mm:ss.FFFzzz"); //2025-02-01 01:02:03+08:00 var str2 = DateTime.Parse("2025-02-01 01:02:03.456+08:00").ToString("yyyy-MM-dd HH:mm:ss.FFFzzz"); //2025-02-01 01:02:03.456+08:00
先给一个默认的格式,然后再允许用户指定格式应该可以满足要求。
默认格式,可以设定如下:
- DateTime/DateTimeOffset:
yyyy-MM-ddTHH:mm:ss.fffffffzzz - DateOnly:
yyyy-MM-dd - TimeOnly:
HH:mm:ss.fffffff - TimeSpan: 直接用c#默认的,即什么也不传,输出示例:
36524.01:02:03.4567890注意: TimeSpan 的 C# 默认格式,是没办法进行排序的,除非所有的数据都 超不过一天,所以,TimeSpan 的默认db类型不应该是 string。
如果用户指定了格式,使用即可,用户可以通过如下方式指定:
csharp
public class Person
{
//首先声明 db 中使用字符串存储,然后使用指定的格式覆盖默认的格式
[TimeFormat("yyyy-MM-dd")]
[Column(TypeName="varchar(50)")]
public CreateTime DateTime { get; set; }
}
3.2 读取时,如何从字符串反射生成c#五大基本数据类型
so easy。。。
四、统一的时间存储格式之数字
这是一个通用的存储方法(对 sqlite 来说是主要的存储方法)。
这种方法是明确知道 db 中以数字(整型或浮点型)存储的时间(即:Unix 时间戳, UTC时间相对于epoch(1970-01-01)的秒数)。
怎么才算明确知道呢?
比如:用户的类是这样定义的
csharppublic class Person { [Column(TypeName = "decimal(18,7)")] [Column(TypeName = "bigint")] public DateTime CreateTime { set; get; } }
注意:
- 如果声明db中是 int, 那么就到秒数;
- 如果声明db中是 float,那么整数部分是秒数,小数部分是毫秒数;
4.1 写入时,如何生成sql
下面附上c#五大时间类型转数字的方法
csharp
//c# 转换方法
using DotNetCommon.Extensions;
//DateTime => int/float
var p_datetime_int = DateTime.Now.ToEpochSeconds();
var p_datetime_float = DateTime.Now.ToEpochSecondsV2();
//DateTimeOffset => int/float
var p_datetimeoffset_int = DateTimeOffset.Now.LocalDateTime.ToEpochSeconds();
var p_datetimeoffset_float = DateTimeOffset.Now.LocalDateTime.ToEpochSecondsV2();
//DateOnly => int
//DateOnly 转float没意义
var p_dateonly_int = DateOnly.FromDateTime(DateTime.Now).ToDateTime(TimeOnly.MinValue).ToEpochSeconds();
//TimeOnly => int/float
var timeonlyTimeSpan = TimeOnly.FromDateTime(DateTime.Now).ToTimeSpan();
var p_timeonly_int = (long)timeonlyTimeSpan.TotalSeconds;
var p_timeonly_float = timeonlyTimeSpan.TotalSeconds;
//TimeSpan => int/float
var timespan = (DateTime.Parse("2000-01-01T01:02:03.456789+08:00") - DateTime.Parse("1900-01-01T00:00:00+08:00"));
var p_timespan_int = (long)timespan.TotalSeconds;
var p_timespan_float = timespan.TotalSeconds;
4.2 读取时,如何从字符串反射生成c#五大基本数据类型
so easy。。。
五、各个数据库的存储适配
2.1 mysql
a. 存取为普通字符串 (同 《统一的时间格式存储之字符串》)
b. 存取为 int/float(同 《统一的时间存储格式之数字》)
c. 存取为db时间类型
c.1 将 c# 中的 DateTime or DateTimeOffset 存储到db中时
- 当已知db中的列类型为:
date时,使用yyyy-MM-dd格式 - 其他,统一使用
yyyy-MM-ddTHH:mm:ss.fffffffzzz
示例如下:
sql
drop table if exists test
create table test(
id int auto_increment primary key,
p_datetime datetime,
p_datetime6 datetime(6),
p_timestamp timestamp,
p_timestamp6 timestamp(6),
p_date date -- 以为着放弃了时间部分
)
insert into test(p_datetime,p_datetime6,p_timestamp,p_timestamp6,p_date)
values('2025-11-26T20:11:26.1358717+08:00','2025-11-26T20:11:26.1358717+08:00','2025-11-26T20:11:26.1358717+08:00','2025-11-26T20:11:26.1358717+08:00','2025-11-26')
select * from test
执行结果:

读取时用c#解析,easy。
c.2 将 c# 的 DateOnly 存储到db时
直接使用 yyyy-MM-dd 即可
c.3 将 c# 的 TimeOnly or TimeSpan 存储到db时
只能认为db时 time 类型,示例如下:
sql
drop table if exists test
create table test(
id int auto_increment primary key,
p_timeonly time,
p_timeonly6 time(6),
p_timespan time,
p_timespan6 time(6)
)
insert into test(p_timeonly,p_timeonly6,p_timespan,p_timespan6)
values('48:01:02.123456','48:01:02.123456','48:01:02.123456','48:01:02.123456')
执行结果:

注意:
-
从c#生成sql时:
TimeOnly 生成sql使用HH:mm:ss.fffffff, TimeSpan则使用如下:csharpvar timeSpan = new TimeSpan(2, 0, 1, 2, 123, 456); var arr = timeSpan.TotalSeconds.ToString().Split('.'); var precision = arr.Length > 1 ? '.' + arr.Last() : ""; var str = $"{timeSpan.Days * 24 + timeSpan.Hours}:{timeSpan.Minutes.ToString().PadLeft(2, '0')}:{timeSpan.Seconds.ToString().PadLeft(2, '0')}{precision}"; //48:01:02.123456 -
从 db 读取解析时, 驱动读取后为 TimeSpan,如果c#中是 TimeOnly 的话,直接将 TimeSpan 的天数给舍弃转成 TimeOnly 即可。
2.2 sqlite
因为 sqlite 本身没有专门的时间列类型,所以基本上只有两种方案。
a. 存取为普通字符串 (同 《统一的时间格式存储之字符串》)
b. 存取为 int/float(同 《统一的时间存储格式之数字》)
2.3 pgsql
a. 存取为普通字符串 (同 《统一的时间格式存储之字符串》)
b. 存取为 int/float(同 《统一的时间存储格式之数字》)
c. 存取为db时间类型

c.1 将 c# 中的 DateTime or DateTimeOffset 存储到db中时
- 当已知db中的列类型为:
date时,使用yyyy-MM-dd格式 - 其他,统一使用
yyyy-MM-ddTHH:mm:ss.fffffffzzz
示例如下:
sql
drop table if exists test
create table test(
id serial primary key,
p_timestamp timestamp,
p_timestamp6 timestamp(6),
p_date date -- 认为着放弃了时间部分
)
insert into test(p_timestamp,p_timestamp6,p_date)
values('2025-11-26T20:11:26.1358717+08:00','2025-11-26T20:11:26.1358717+08:00','2025-11-26')
select * from test
执行结果:

温馨提示:如果你查询的结果时间的小数部分只有三位(总之不够6位)那么可能时客户端的问题,在 DBEaver中如下配置:
在c#中使用
Npgsql驱动时是没这个问题的。
c.2 将 c# 的 DateOnly 存储到db时
直接使用 yyyy-MM-dd 即可
c.3 将 c# 的 TimeOnly 存储到db时
只能认为db时 time 类型,示例如下:
sql
drop table if exists test
create table test(
id serial primary key,
p_timeonly time,
p_timeonly6 time(6),
p_timespan time,
p_timespan6 time(6)
)
insert into test(p_timeonly,p_timeonly6,p_timespan,p_timespan6)
values('01:01:02.123456','01:01:02.123456','12:01:02.123456','12:01:02.123456')
select * from test
执行结果:

温馨提示:如果发现输出的时间没有小数部分或精度不够参考上面 "c.1 将 c# 中的 DateTime or DateTimeOffset 存储到db中时" 中的提示。
c.4 将 c# 的 TimeSpan 存储到db时
此时只能认为 db 中的类型是 interval 类型,如下所示:
sql
drop table if exists test
create table test(
id serial primary key,
p_timespan INTERVAL DAY TO second,
p_timespan6 INTERVAL DAY TO second(6)
)
insert into test(p_timespan,p_timespan6)
values('2.04:05:06','2.04:05:06.1234567')
select * from test
执行结果:

c.5 存储 pgsql 中的时间范围
此处只讨论读写,下面先附上 chatgpt 的一个解释:

使用示例:
sql
drop table if exists test
create table test(
id serial primary key,
ts tsrange,
tsz tstzrange,
dr daterange
)
insert into test(ts,tsz,dr) values(
'[2025-01-01T10:00:00.1234567+08:00,2025-01-01T12:00:00.1234567+08:00)',
'[2025-01-01T10:00:00.1234567+08:00,2025-01-01T12:00:00.1234567+08:00)',
'[2025-01-01,2025-01-02]'
)
select * from test
执行结果:

那么如何在c#中接收呢?直接使用 npgsql 中定义的数据类型即可,如:
csharp
public class Person
{
public NpgsqlTypes.NpgsqlRange<DateTime> Ts { get; set; }
public NpgsqlTypes.NpgsqlRange<DateTime> Tsz { get; set; }
public NpgsqlTypes.NpgsqlRange<DateTime> Dr { get; set; }
}
npgsql 驱动读取 reader 的时候,就已经是
NpgsqlRange<>类型了,只需要生成sql的时候转换成对应的字符串就行了,还有的问题是?当使用 BullkCopy 时 给 npgsql 的 datatable 的 数据应该是什么?字符串还是NpgsqlRange<>?
2.4 sqlsever
a. 存取为普通字符串 (同 《统一的时间格式存储之字符串》)
b. 存取为 int/float(同 《统一的时间存储格式之数字》)
c. 存取为db时间类型
c.1 将 c# 中的 DateTime 存储到db中时
- 当已知db中的列类型为:
date时,使用yyyy-MM-dd格式 - 其他,统一使用
yyyy-MM-dd HH:mm:ss.fff
示例如下:
sql
-- drop table test
create table test(
id int identity(1,1) primary key,
p_datetime datetime,
p_smalldatetime smalldatetime,
p_datetime2 datetime2,
p_datetime2_7 datetime2(7),
p_datetimeoffset datetimeoffset,
p_datetimeoffset7 datetimeoffset(7),
p_date date -- 认为着放弃了时间部分
)
insert into test(
p_datetime,
p_smalldatetime,
p_datetime2,p_datetime2_7,
p_datetimeoffset,p_datetimeoffset7,
p_date
)
values(
'2025-11-26 20:11:26.123', -- 带时区 或者 多一位小数就会报转换错误 Conversion failed when converting date and/or time from character string.
'2025-11-26 20:11:26.123', -- 带时区 或者 多一位小数就会报转换错误
'2025-11-26T20:11:26.1234567+08:00','2025-11-26T20:11:26.1234567+08:00',
'2025-11-26T20:11:26.1234567+08:00','2025-11-26T20:11:26.1234567+08:00',
'2025-11-26'
)
select * from test
执行结果:


因为对于 db 中的 datetime 和 smalldatetime 极易发生截断异常,所以只能将 DateTime 格式化为: yyyy-MM-dd HH:mm:ss.fff
c.2 将 c# 的 DateTimeOffset 存储到db时
这个就不像存 DateTime 那么小心了, 可以直接格式化为: yyyy-MM-ddTHH:mm:ss.fffffffzzz
如下:
sql
-- drop table test
create table test(
id int identity(1,1) primary key,
p_datetime2 datetime2,
p_datetime2_7 datetime2(7),
p_datetimeoffset datetimeoffset,
p_datetimeoffset7 datetimeoffset(7)
)
insert into test(
p_datetime2,p_datetime2_7,
p_datetimeoffset,p_datetimeoffset7
)
values(
'2025-11-26T20:11:26.1234567+08:00','2025-11-26T20:11:26.1234567+08:00',
'2025-11-26T20:11:26.1234567+08:00','2025-11-26T20:11:26.1234567+08:00'
)
select * from test
执行结果:

c.3 将 c# 的 DateOnly 存储到db时
直接使用 yyyy-MM-dd 即可
c.4 将 c# 的 TimeOnly 存储到db时
只能认为db时 time 类型,示例如下:
sql
-- drop table test
create table test(
id int identity(1,1) primary key,
p_timeonly time,
p_timeonly6 time(6)
)
insert into test(p_timeonly,p_timeonly6)
values('12:01:02.123456','12:01:02.123456')
select * from test
执行结果:

c.5 将 c# 的 TimeSpan 存储到db时
因为 sqlserver 中并没有专门的 时间段 类型,所以只能存储为 int or float
2.5 oracle
a. 存取为普通字符串 (同 《统一的时间格式存储之字符串》)
b. 存取为 int/float(同 《统一的时间存储格式之数字》)
c. 存取为db时间类型
先附上chatgpt关于oracle时间列类型的介绍

c.1 将 c# 的 DateTime or DateTimeOffset 存储到db时
使用格式: yyyy-MM-dd HH:mm:ss.fffffff zzz 且指定 timestamp 前缀,如下:
sql
drop table test
CREATE TABLE test (
p_date DATE,
p_timestamp timestamp,
p_timestamp7 timestamp(7),
p_timestampts TIMESTAMP WITH TIME ZONE,
p_timestampts_7 TIMESTAMP(7) WITH TIME ZONE,
p_timestampts_local TIMESTAMP WITH LOCAL TIME ZONE,
p_timestampts_local_7 TIMESTAMP(7) WITH LOCAL TIME ZONE
);
insert into test(
p_date,
p_timestamp,
p_timestamp7,
p_timestampts,
p_timestampts_7,
p_timestampts_local,
p_timestampts_local_7
) values
(
TIMESTAMP '2025-11-26 20:11:26.1234567 +08:00',-- 可以 且自动截断
TIMESTAMP '2025-11-26 20:11:26.1234567 +08:00',
TIMESTAMP '2025-11-26 20:11:26.1234567 +08:00',
TIMESTAMP '2025-11-26 20:11:26.1234567 +08:00',
TIMESTAMP '2025-11-26 20:11:26.1234567 +08:00',
TIMESTAMP '2025-11-26 20:11:26.1234567 +08:00',
TIMESTAMP '2025-11-26 20:11:26.1234567 +08:00'
)
select * from test
执行结果:

c.2 将 c# 中的 DateOnly 存储到db时
直接使用 yyyy-MM-dd 即可
c.3 将 c# 中的 TimeOnly 存储到db时
因为 oracle 中没有专门的 time 类型,所以存储时,取一个固定的日期,如:'1970-01-01', 示例如下:
sql
drop table test
CREATE TABLE test (
p_time DATE,
p_time2 TIMESTAMP(7)
);
insert into test(p_time,p_time2) values(
TIMESTAMP '1970-01-01 20:11:26.1234567', -- 会自行截断
TIMESTAMP '1970-01-01 20:11:26.1234567'
)
select * from test
执行结果:

c.4 将 c# 的 TimeSpan 存储到db时
认为db中的存储类型是: INTERVAL DAYTO SECOND,示例如下:
sql
drop table test
CREATE TABLE test (
duration INTERVAL DAY(3) TO SECOND(6)
);
INSERT INTO test(duration) VALUES(INTERVAL '5 12:30:15.123456' DAY TO SECOND);
INSERT INTO test(duration) VALUES(INTERVAL '-5 12:30:15.123456' DAY TO SECOND);
-- INSERT INTO test(duration) VALUES(INTERVAL '5.12:30:15.123456' DAY TO SECOND);-- 会报错: ORA-01867: the interval is invalid
select * from test
执行结果:

注意:oracle天数后面用的是空格,而c#默认用的是
.
