139 lines
4.8 KiB
PL/PgSQL
139 lines
4.8 KiB
PL/PgSQL
-- /admin/groups 分组用量日汇总。
|
|||
|
|
-- 迁移创建结构与源表失效触发器,历史数据由后台聚合作业按持久水位回填。
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS usage_group_daily_rollups (
|
||
|
|
bucket_date DATE NOT NULL,
|
||
|
|
group_id BIGINT NOT NULL,
|
||
|
|
actual_cost DECIMAL(20, 10) NOT NULL DEFAULT 0,
|
||
|
|
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
|
|
PRIMARY KEY (bucket_date, group_id)
|
||
|
|
);
|
||
|
|
|
||
|
|
COMMENT ON TABLE usage_group_daily_rollups IS '按北京时间自然日聚合的分组实际费用。';
|
||
|
|
COMMENT ON COLUMN usage_group_daily_rollups.bucket_date IS '北京时间自然日。';
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS usage_group_rollup_state (
|
||
|
|
id SMALLINT PRIMARY KEY CHECK (id = 1),
|
||
|
|
closed_before DATE NOT NULL DEFAULT DATE '1970-01-01',
|
||
|
|
retained_from TIMESTAMPTZ NOT NULL DEFAULT TIMESTAMPTZ '1970-01-01 00:00:00+00',
|
||
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
COMMENT ON TABLE usage_group_rollup_state IS '分组日汇总的单行发布水位。';
|
||
|
|
COMMENT ON COLUMN usage_group_rollup_state.closed_before IS '已完整发布日桶的北京时间日期排他上界。';
|
||
|
|
|
||
|
|
INSERT INTO usage_group_rollup_state (id, closed_before, retained_from)
|
||
|
|
VALUES (1, DATE '1970-01-01', TIMESTAMPTZ '1970-01-01 00:00:00+00')
|
||
|
|
ON CONFLICT (id) DO NOTHING;
|
||
|
|
|
||
|
|
-- 已发布范围的源记录发生变化时,必须在同一事务内后退发布水位。
|
||
|
|
-- DELETE/UPDATE 使用行级触发器,以覆盖外键级联、分区表和直接分区写入。
|
||
|
|
CREATE OR REPLACE FUNCTION invalidate_group_usage_rollup_state()
|
||
|
|
RETURNS TRIGGER
|
||
|
|
LANGUAGE plpgsql
|
||
|
|
AS $$
|
||
|
|
DECLARE
|
||
|
|
affected_date DATE;
|
||
|
|
published_before DATE;
|
||
|
|
BEGIN
|
||
|
|
IF TG_OP = 'DELETE' THEN
|
||
|
|
affected_date := (OLD.created_at AT TIME ZONE 'Asia/Shanghai')::date;
|
||
|
|
ELSE
|
||
|
|
IF OLD.group_id IS NULL THEN
|
||
|
|
affected_date := (NEW.created_at AT TIME ZONE 'Asia/Shanghai')::date;
|
||
|
|
ELSIF NEW.group_id IS NULL THEN
|
||
|
|
affected_date := (OLD.created_at AT TIME ZONE 'Asia/Shanghai')::date;
|
||
|
|
ELSE
|
||
|
|
affected_date := LEAST(
|
||
|
|
(OLD.created_at AT TIME ZONE 'Asia/Shanghai')::date,
|
||
|
|
(NEW.created_at AT TIME ZONE 'Asia/Shanghai')::date
|
||
|
|
);
|
||
|
|
END IF;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
-- 即使当前已发布水位尚未越过受影响日期,也必须先锁行。
|
||
|
|
-- 否则并发关闭作业可能在本事务之后把水位推进,覆盖本次失效。
|
||
|
|
SELECT closed_before
|
||
|
|
INTO published_before
|
||
|
|
FROM usage_group_rollup_state
|
||
|
|
WHERE id = 1
|
||
|
|
FOR UPDATE;
|
||
|
|
|
||
|
|
IF published_before > affected_date THEN
|
||
|
|
UPDATE usage_group_rollup_state
|
||
|
|
SET closed_before = LEAST(closed_before, affected_date),
|
||
|
|
updated_at = NOW()
|
||
|
|
WHERE id = 1;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
IF TG_OP = 'DELETE' THEN
|
||
|
|
RETURN OLD;
|
||
|
|
END IF;
|
||
|
|
RETURN NEW;
|
||
|
|
END;
|
||
|
|
$$;
|
||
|
|
|
||
|
|
-- INSERT 是网关高频路径。transition table 让每个批量 INSERT 只锁一次状态行;
|
||
|
|
-- KEY SHARE 在普通写入之间兼容,但会与关闭作业的 FOR UPDATE 串行化。
|
||
|
|
CREATE OR REPLACE FUNCTION invalidate_group_usage_rollup_state_after_insert()
|
||
|
|
RETURNS TRIGGER
|
||
|
|
LANGUAGE plpgsql
|
||
|
|
AS $$
|
||
|
|
DECLARE
|
||
|
|
affected_date DATE;
|
||
|
|
published_before DATE;
|
||
|
|
BEGIN
|
||
|
|
SELECT MIN((created_at AT TIME ZONE 'Asia/Shanghai')::date)
|
||
|
|
INTO affected_date
|
||
|
|
FROM inserted_usage_logs
|
||
|
|
WHERE group_id IS NOT NULL;
|
||
|
|
|
||
|
|
IF affected_date IS NULL THEN
|
||
|
|
RETURN NULL;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
SELECT closed_before
|
||
|
|
INTO published_before
|
||
|
|
FROM usage_group_rollup_state
|
||
|
|
WHERE id = 1
|
||
|
|
FOR KEY SHARE;
|
||
|
|
|
||
|
|
IF published_before > affected_date THEN
|
||
|
|
UPDATE usage_group_rollup_state
|
||
|
|
SET closed_before = LEAST(closed_before, affected_date),
|
||
|
|
updated_at = NOW()
|
||
|
|
WHERE id = 1;
|
||
|
|
END IF;
|
||
|
|
|
||
|
|
RETURN NULL;
|
||
|
|
END;
|
||
|
|
$$;
|
||
|
|
|
||
|
|
DROP TRIGGER IF EXISTS usage_logs_group_rollup_invalidate_insert ON usage_logs;
|
||
|
|
CREATE TRIGGER usage_logs_group_rollup_invalidate_insert
|
||
|
|
AFTER INSERT ON usage_logs
|
||
|
|
REFERENCING NEW TABLE AS inserted_usage_logs
|
||
|
|
FOR EACH STATEMENT
|
||
|
|
EXECUTE FUNCTION invalidate_group_usage_rollup_state_after_insert();
|
||
|
|
|
||
|
|
DROP TRIGGER IF EXISTS usage_logs_group_rollup_invalidate_delete ON usage_logs;
|
||
|
|
CREATE TRIGGER usage_logs_group_rollup_invalidate_delete
|
||
|
|
AFTER DELETE ON usage_logs
|
||
|
|
FOR EACH ROW
|
||
|
|
WHEN (OLD.group_id IS NOT NULL)
|
||
|
|
EXECUTE FUNCTION invalidate_group_usage_rollup_state();
|
||
|
|
|
||
|
|
DROP TRIGGER IF EXISTS usage_logs_group_rollup_invalidate_update ON usage_logs;
|
||
|
|
CREATE TRIGGER usage_logs_group_rollup_invalidate_update
|
||
|
|
AFTER UPDATE OF created_at, group_id, actual_cost ON usage_logs
|
||
|
|
FOR EACH ROW
|
||
|
|
WHEN (
|
||
|
|
(
|
||
|
|
OLD.created_at IS DISTINCT FROM NEW.created_at
|
||
|
|
OR OLD.group_id IS DISTINCT FROM NEW.group_id
|
||
|
|
OR OLD.actual_cost IS DISTINCT FROM NEW.actual_cost
|
||
|
|
)
|
||
|
|
AND (OLD.group_id IS NOT NULL OR NEW.group_id IS NOT NULL)
|
||
|
|
)
|
||
|
|
EXECUTE FUNCTION invalidate_group_usage_rollup_state();
|