add timescaledb extension migration and orders_daily_by_created continuous aggregate

P22.1 of Stage 22 materialization layer infrastructure. Adds explicit
CREATE EXTENSION migration so clean environments can run `migrate` end-to-end
without ops manual setup, then creates the first continuous aggregate
orders_daily_by_created (day-bucketed, grouped by company/platform/store)
with WITH NO DATA. Five composite indexes cover the high-frequency query
patterns documented in docs/data_query.md. P22.2/P22.3 will add the
by-paid view, refresh policy and historical backfill on top.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 12:07:17 +08:00
parent efc5cabfbb
commit 785726caac
2 changed files with 72 additions and 0 deletions
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Hyperf\DbConnection\Db;
use Hyperf\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
Db::statement('CREATE EXTENSION IF NOT EXISTS timescaledb');
}
public function down(): void
{
// 不主动 DROP EXTENSIONexisting hypertables 依赖该扩展,drop 会破坏数据。
// 完全清空数据库时手动执行 `DROP EXTENSION timescaledb CASCADE`。
}
};
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
use Hyperf\DbConnection\Db;
use Hyperf\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
// 连续聚合视图:按订单创建日期日聚合(含未付订单)。
// WITH NO DATA:视图创建时不立即物化,由 P22.3 的回填命令一次性填充历史数据。
Db::statement(<<<'SQL'
CREATE MATERIALIZED VIEW orders_daily_by_created
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', created_date) AS day,
company_id,
platform_id,
store_id,
COUNT(*) AS total_orders,
COUNT(*) FILTER (WHERE paid_date IS NOT NULL) AS paid_orders,
COUNT(*) FILTER (WHERE paid_date IS NULL) AS unpaid_orders,
SUM(total_amount) AS sum_total_amount,
SUM(total_paid) AS sum_total_paid,
SUM(total_received) AS sum_total_received,
AVG(total_amount) AS avg_total_amount,
AVG(total_paid) FILTER (WHERE paid_date IS NOT NULL) AS avg_paid_amount,
SUM(freight_fee) AS sum_freight_fee,
SUM(tax_fee) AS sum_tax_fee,
SUM(commission_fee) AS sum_commission_fee,
SUM(discount_fee) AS sum_discount_fee
FROM orders
GROUP BY day, company_id, platform_id, store_id
WITH NO DATA;
SQL);
// 复合索引:覆盖"最近 N 日按 X 维度"与"指定 X 全历史时间序列"两类高频查询。
Db::statement('CREATE INDEX IF NOT EXISTS idx_orders_daily_by_created_day_company ON orders_daily_by_created (day DESC, company_id)');
Db::statement('CREATE INDEX IF NOT EXISTS idx_orders_daily_by_created_day_platform ON orders_daily_by_created (day DESC, platform_id)');
Db::statement('CREATE INDEX IF NOT EXISTS idx_orders_daily_by_created_day_store ON orders_daily_by_created (day DESC, store_id)');
Db::statement('CREATE INDEX IF NOT EXISTS idx_orders_daily_by_created_company_day ON orders_daily_by_created (company_id, day DESC)');
Db::statement('CREATE INDEX IF NOT EXISTS idx_orders_daily_by_created_store_day ON orders_daily_by_created (store_id, day DESC)');
}
public function down(): void
{
// CASCADE 一并 drop 索引以及后续阶段附加的刷新策略。
Db::statement('DROP MATERIALIZED VIEW IF EXISTS orders_daily_by_created CASCADE');
}
};