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:
@@ -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 EXTENSION:existing 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');
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user