یک فرمول مشترک در چند مسیر برنامه

وقتی مبلغ یک ردیف سفارش از تعداد و قیمت واحد به دست می‌آید، تکرار فرمول در گزارش، API و پنل مدیریت احتمال ناسازگاری را بالا می‌برد. ستون generated می‌تواند رابطه مشتق‌شده را در مدل داده نگه دارد. PostgreSQL 18 ستون مجازی را نیز پشتیبانی می‌کند؛ مقداری که هنگام خواندن محاسبه می‌شود. این مقاله به بررسی قابلیت نسخه ۱۸ می‌پردازد و به معنی توصیه ارتقای فوری یا خبر انتشار امروز نیست.

در نوع STORED نتیجه محاسبه ذخیره می‌شود و هنگام نوشتن به‌روز می‌شود. در نوع VIRTUAL مقدار در زمان خواندن محاسبه می‌شود. انتخاب بین آن‌ها به الگوی بار بستگی دارد: محاسبه ارزان روی ردیفی که کم خوانده می‌شود با فرمول گران در گزارش پرتکرار یکی نیست. در PostgreSQL 18 نوع مجازی پیش‌فرض است، ولی در مثال آن را صریح می‌نویسیم تا خواننده از روی کد تصمیم معماری را ببیند.

انتخاب محل هزینه محاسبه

هر عبارتی برای generated مجاز نیست. فرمول باید محدودیت توابع immutable را رعایت کند و نمی‌توان آن را به جست‌وجوی دلخواه در جدول دیگر تبدیل کرد. محدودیت نوع مجازی درباره نوع و تابع کاربرساخته نیز باید در مستندات نسخه مقصد بررسی شود. اگر نرخ مالیات از جدول سیاست روزانه می‌آید، این مسئله احتمالاً فقط با افزودن یک ستون محاسباتی حل نمی‌شود.

مثال با عدد صحیح تعداد و numeric برای قیمت، یک مبلغ ساده می‌سازد. نتیجه این قرارداد تاریخچه قیمت نیست: اگر داده پایه تغییر کند، مبلغ مشتق‌شده هم تغییر می‌کند. برای فاکتور قطعی شاید نیاز داشته باشید قیمت، مالیات و تخفیف همان لحظه را به‌عنوان داده تجاری مستقل ثبت کنید. تفاوت «مشتق قابل محاسبه» و «واقعیت ثبت‌شده در زمان معامله» پیش از انتخاب نوع ستون باید روشن شود.

قبل از مهاجرت، پرس‌وجوهای پرمصرف را با اندازه داده واقعی آزمایش کنید و هزینه خواندن و نوشتن را جدا ثبت کنید. صرفه‌جویی در ذخیره‌سازی به‌تنهایی تضمین بهبود نیست. ابزار ORM، export و replication نیز ممکن است درباره ستون‌های generated فرض متفاوت داشته باشند؛ سازگاری آن‌ها را با نسخه‌های دقیق پروژه بسنجید. نمونه جدول موقت است و فقط در همان نشست باقی می‌ماند.

نمونه کد و روش بررسی

پیش‌نیاز و خروجی مورد انتظار در توضیح مثال آمده است. ابتدا نمونه را در محیط آزمایشی اجرا کنید و سپس آن را با داده و محدودیت‌های پروژه خود تطبیق دهید.

1. SQL برای PostgreSQL 18؛ جدول موقت آزمایشی

BEGIN;
CREATE TEMP TABLE demo_line_items (
    quantity integer NOT NULL CHECK (quantity > 0),
    unit_price numeric(12,2) NOT NULL CHECK (unit_price >= 0),
    total numeric GENERATED ALWAYS AS
        (quantity * unit_price) VIRTUAL
);
INSERT INTO demo_line_items (quantity, unit_price)
VALUES (3, 12.50);
SELECT quantity, unit_price, total FROM demo_line_items;
ROLLBACK;

روی PostgreSQL 18 انتظار total برابر 37.50 داریم. ROLLBACK همه تغییرهای این مثال را برمی‌گرداند. روی نسخه قدیمی‌تر نحو VIRTUAL ممکن است پذیرفته نشود. مثال تخفیف، مالیات و واحد پول را مدل نمی‌کند.

2. مقایسه صریح VIRTUAL و STORED پس از تغییر داده

در این نشست آزمایشی دو نوع ستون از یک فرمول و داده یکسان ساخته می‌شوند. مقدار generated را در INSERT نمی‌نویسیم؛ فقط داده پایه را می‌دهیم تا رابطه در مدل داده حفظ شود.

-- PostgreSQL 18; disposable session.
BEGIN;
CREATE TEMP TABLE generated_price_demo (
    id integer PRIMARY KEY,
    quantity integer NOT NULL CHECK(quantity > 0),
    unit_price numeric(12,2) NOT NULL CHECK(unit_price >= 0),
    virtual_total numeric GENERATED ALWAYS AS
        (quantity * unit_price) VIRTUAL,
    stored_total numeric GENERATED ALWAYS AS
        (quantity * unit_price) STORED
);
INSERT INTO generated_price_demo(id,quantity,unit_price)
VALUES(1,3,12.50);
SELECT id,virtual_total,stored_total FROM generated_price_demo;
UPDATE generated_price_demo SET quantity=4 WHERE id=1;
SELECT id,virtual_total,stored_total FROM generated_price_demo;
ROLLBACK;

خروجی اول برای هر دو 37.50 و خروجی دوم 50.00 است. یکسانی مقدار به معنی یکسانی محل هزینه محاسبه نیست. ROLLBACK نمونه را پاک می‌کند و جدول تجاری شما تغییری نمی‌کند.

3. بررسی متادیتای ستون و مرز ورودی نامعتبر

متادیتا را بخوانید تا ابزار مهاجرت یا ORM بداند total ستون معمولی قابل نوشتن نیست. سپس ورودی مرزی نامعتبر را داخل یک بلوک کنترل‌شده می‌فرستیم؛ شکست مورد انتظار نباید کل نشست آزمایش را بی‌توضیح متوقف کند.

-- PostgreSQL 18.
BEGIN;
CREATE TEMP TABLE generated_validation_demo (
    quantity integer NOT NULL CHECK(quantity > 0),
    price numeric(12,2) NOT NULL CHECK(price >= 0),
    total numeric GENERATED ALWAYS AS (quantity * price) VIRTUAL
);
SELECT column_name,is_generated,generation_expression
FROM information_schema.columns
WHERE table_name='generated_validation_demo'
  AND table_schema LIKE 'pg_temp_%'
ORDER BY ordinal_position;

DO $$
BEGIN
    BEGIN
        INSERT INTO generated_validation_demo(quantity,price) VALUES(0,10);
        RAISE EXCEPTION 'Expected a check violation';
    EXCEPTION WHEN check_violation THEN
        RAISE NOTICE 'Invalid quantity rejected as expected';
    END;
END $$;
SELECT count(*) AS rows_after_rejected_insert
FROM generated_validation_demo;
ROLLBACK;

باید NOTICE مربوط به ردشدن تعداد صفر و شمارش نهایی صفر را ببینید. اگر محدودیت حذف شود، RAISE EXCEPTION آزمون را ناموفق می‌کند. کنترل قاعده تجاری در CHECK مستقل از نوع ذخیره یا مجازی ستون است.

4. مقایسه مسیر خواندن با EXPLAIN در داده کنترل‌شده

هدف این آزمایش دیدن پلان و تأیید برابری نتیجه است. چون هر دو ستون در یک جدول‌اند، از آن نتیجه قطعی درباره صرفه‌جویی فضای دو طراحی جدا نگیرید. برای تصمیم تولید، دو طرح مستقل با الگوی واقعی نوشتن و خواندن بسازید.

-- PostgreSQL 18; 10,000 synthetic rows, no performance promise.
BEGIN;
CREATE TEMP TABLE generated_read_demo (
    quantity integer NOT NULL,
    price numeric NOT NULL,
    virtual_total numeric GENERATED ALWAYS AS (quantity * price) VIRTUAL,
    stored_total numeric GENERATED ALWAYS AS (quantity * price) STORED
);
INSERT INTO generated_read_demo(quantity,price)
SELECT (n % 5) + 1, (n % 100) + 0.50
FROM generate_series(1,10000) AS numbers(n);
ANALYZE generated_read_demo;
EXPLAIN (ANALYZE, BUFFERS)
SELECT sum(virtual_total) FROM generated_read_demo;
EXPLAIN (ANALYZE, BUFFERS)
SELECT sum(stored_total) FROM generated_read_demo;
SELECT sum(virtual_total)=sum(stored_total) AS equal_results
FROM generated_read_demo;
ROLLBACK;

equal_results باید true باشد. اجرای EXPLAIN ANALYZE واقعاً پرس‌وجو را اجرا می‌کند؛ آن را با دستور تغییردهنده ناشناخته روی داده اصلی به کار نبرید. ترتیب اجرا و کش می‌تواند زمان را تغییر دهد؛ یک بار سریع‌ترشدن، معیار انتخاب معماری نیست.

مهاجرت بر پایه الگوی واقعی پرس‌وجو

برای شروع، یک فرمول ساده و پرتکرار را انتخاب کنید و خروجی آن را با محاسبه موجود تطبیق دهید. سپس هدف عملکرد و روش بازگشت را مشخص کنید. در پروژه آموزشی، مقایسه دو نوع ستون با داده یکسان تمرین خوبی است، به شرط اینکه نتیجه یک لپ‌تاپ به همه محیط‌ها تعمیم داده نشود. تصمیم درست از قرارداد داده و اندازه‌گیری می‌آید، نه از تازه‌بودن قابلیت.

چک‌لیست اجرای عملی

  • نسخه سرور و محدودیت generated بررسی شود.
  • VIRTUAL یا STORED در طرح صریح انتخاب شود.
  • مقدار مشتق‌شده با سند مالی تاریخی اشتباه نشود.
  • ORM و export در محیط آزمایش بررسی شوند.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.