如果我了解您的设计,这确实是一个架构设计问题。代替:
CREATE TABLE sometable (
date1 date,
time1 time,
date2 date,
time2 time
);
你通常想要:
CREATE TABLE sometable (
timestamp1 timestamp with time zone,
timestamp2 timestamp with time zone
);
如果您希望将时间戳自动转换为 UTC 并返回到客户端TimeZone
,或者timestamp without time zone
如果您想存储原始时间戳而不进行时区转换。
如果包容性测试没问题,你可以写:
SELECT ...
FROM sometable
WHERE '2012-01-01 11:15 +0800' BETWEEN timestamp1 AND timestamp2;
如果你不能修改你的架构,你最好的选择是这样的:
SELECT ...
FROM sometable
WHERE '2012-01-01 11:15 +0800' BETWEEN (date1 + time1) AND (date2 + time2);
当涉及到多个时区的客户时,这可能会有一些意想不到的怪癖;您可能需要查看AT TIME ZONE
操作员。
如果您需要在一侧和/或另一侧进行排他性测试,则不能使用,BETWEEN
因为它是a <= x <= b
运算符。而是写:
SELECT ...
FROM sometable
WHERE '2012-01-01 11:15 +0800' > (date1 + time1)
AND '2012-01-01 11:15 +0800' < (date2 + time2);
自动化架构更改
自动化模式更改是可能的。
您想查询具有INFORMATION_SCHEMA
和列对的表,pg_catalog.pg_class
然后生成一组命令来统一它们。pg_catalog.pg_attribute
date
time
ALTER TABLE
确定什么是“对”是特定于应用程序的;如果您使用了一致的命名方案,则应该很容易使用LIKE
or~
运算符和/或regexp_matches
. 您想生成一组(tablename, datecolumnname, timecolumnname)
元组。
一旦你有了它,你可以为每个(tablename, datecolumnname, timecolumnname)
元组生成以下ALTER TABLE
语句,这些语句必须在事务中运行以确保安全,并且应该在使用你关心的任何数据之前进行测试,并且其中的条目[brackets]
是替换:
BEGIN;
ALTER TABLE [tablename] ADD COLUMN [timestampcolumnname] TIMESTAMP WITH TIME ZONE;
--
-- WARNING: This part can lose data; if one of the columns is null and the other one isn't
-- the result is null. You should've had a CHECK constraint preventing that, but probably
-- didn't. You might need to special case that; the `coalesce` and `nullif` functions and
-- the `CASE` clause might be useful if so.
--
UPDATE [tablename] SET [timestampcolumnname] = ([datecolumnname] + [timecolumnname]);
ALTER TABLE [tablename] DROP COLUMN [datecolumnname];
ALTER TABLE [tablename] DROP COLUMN [timecolumnname];
-- Finally, if the originals were NOT NULL:
ALTER TABLE [tablename] ALTER COLUMN [timestampcolumnname] SET NOT NULL;
然后检查结果,COMMIT
如果满意。请注意,从一开始就对表进行排他锁,ALTER
因此在您COMMIT
或ROLLBACK
.
如果您使用的是现代 PostgreSQL,您可以使用函数format
生成SQL ;在旧版本上,您可以使用字符串连接 ( ||
) 和quote_literal
函数。例子:
给定样本数据:
CREATE TABLE sometable(date1 date not null, time1 time not null, date2 date not null, time2 time not null);
INSERT INTO sometable(date1,time1,date2,time2) VALUES
('2012-01-01','11:15','2012-02-03','04:00');
CREATE TABLE othertable(somedate date, sometime time);
INSERT INTO othertable(somedate, sometime) VALUES
(NULL, NULL),
(NULL, '11:15'),
('2012-03-08',NULL),
('2014-09-18','23:12');
这是一个生成输入数据集的查询。请注意,它依赖于命名约定,即一旦从列中删除任何date
或单词,匹配的列对总是具有通用名称。time
您可以改为通过测试来使用邻接c1.attnum + 1 = c2.attnum
。
BEGIN;
WITH
-- Create set of each date/time column along with its table name, oids, and not null flag
cols AS (
select attrelid, relname, attname, typname, atttypid, attnotnull
from pg_attribute
inner join pg_class on pg_attribute.attrelid = pg_class.oid
inner join pg_type on pg_attribute.atttypid = pg_type.oid
where NOT attisdropped AND atttypid IN ('date'::regtype, 'time'::regtype)
),
-- Self join the time and date column set, filtering the left side for only dates and
-- the right side for only times, producing two distinct sets. Then filter for entries
-- where the names are the same after replacing any appearance of the word `date` or
-- `time`.
tableinfo (tablename, datecolumnname, timecolumnname, nonnull, hastimezone) AS (
SELECT
c1.relname, c1.attname, c2.attname,
c1.attnotnull AND c2.attnotnull AS nonnull,
't'::boolean AS withtimezone
FROM cols c1
INNER JOIN cols c2 ON (
c1.atttypid = 'date'::regtype
AND c2.atttypid = 'time'::regtype
AND c1.attrelid = c2.attrelid
-- Match column pairs; I used name matching, you might use adjancency:
AND replace(c1.attname,'date','') = replace(c2.attname,'time','')
)
)
-- Finally, format the results into a series of ALTER TABLE statements.
SELECT format($$
ALTER TABLE %1$I ADD COLUMN %4$I TIMESTAMP %5$s;
UPDATE %1$I SET %4$I = (%2$I + %3$I);
ALTER TABLE %1$I DROP COLUMN %2$I;
ALTER TABLE %1$I DROP COLUMN %3$I;
$$ ||
-- Append a clause to make the column NOT NULL now that it's populated, only
-- if the original date or time were NOT NULL:
CASE
WHEN nonnull
THEN ' ALTER TABLE %1$I ALTER COLUMN %4$I SET NOT NULL;'
ELSE ''
END,
-- Now the format arguments
tablename, -- 1
datecolumnname, -- 2
timecolumnname, -- 3
-- You'd use a better column name generator than this simple example:
datecolumnname||'_'||timecolumnname, -- 4
CASE
WHEN hastimezone THEN 'WITH TIME ZONE'
ELSE 'WITHOUT TIME ZONE'
END -- 5
)
FROM tableinfo;
您可以在第二个会话中读取结果并将它们作为 SQL 命令发送,或者如果您想变得花哨,您可以编写一个相当简单的 PL/PgSQL 函数,该函数LOOP
覆盖结果并EXECUTE
逐个处理。该查询产生如下输出:
ALTER TABLE sometable ADD COLUMN date1_time1 TIMESTAMP WITH TIME ZONE;
UPDATE sometable SET date1_time1 = (date1 + time1);
ALTER TABLE sometable DROP COLUMN date1;
ALTER TABLE sometable DROP COLUMN time1;
ALTER TABLE sometable ALTER COLUMN date1_time1 SET NOT NULL;
ALTER TABLE sometable ADD COLUMN date2_time2 TIMESTAMP WITH TIME ZONE;
UPDATE sometable SET date2_time2 = (date2 + time2);
ALTER TABLE sometable DROP COLUMN date2;
ALTER TABLE sometable DROP COLUMN time2;
ALTER TABLE sometable ALTER COLUMN date2_time2 SET NOT NULL;
ALTER TABLE othertable ADD COLUMN somedate_sometime TIMESTAMP WITHOUT TIME ZONE;
UPDATE othertable SET somedate_sometime = (somedate + sometime);
ALTER TABLE othertable DROP COLUMN somedate;
ALTER TABLE othertable DROP COLUMN sometime;
我不知道是否有任何有用的方法可以在每列的基础上进行计算,无论你想要WITH TIME ZONE
还是WITHOUT TIME ZONE
. 很可能你只是硬编码就完成了,在这种情况下,你可以删除该列。我把它放在那里,以防在你的应用程序中有一个很好的方法来解决它。
如果您遇到时间可以为空但日期不为空或反之亦然的情况,您需要将日期和时间包装在一个表达式中,以决定在为空时返回什么结果。nullif
和函数对此coalesce
很有用,就像CASE
. 请记住,添加 null 和非 null 值会产生 null 结果,因此您可能不需要做任何特殊的事情。
如果您使用模式,您可能需要进一步细化查询以使用模式名称前缀的 %I 替换来消除歧义。如果您不使用模式(如果您不知道模式是什么,则不使用),那么这无关紧要。
完成此操作后,请考虑添加小于或等于在应用程序中有意义的位置CHECK
强制执行的约束。另请查看文档中的排除约束。time1
time2