VIRTUAL в Oracle SQL. Введение
Есть слова в Oracle, которые сначала кажутся скромными, а потом внезапно открывают очень красивую архитектуру.
VIRTUAL — именно такое. Чаще всего его используют с виртуальными столбцами:
значение не хранится физически как обычные данные, а вычисляется из выражения.
Это значит меньше дублирования, чище модель и приятнее сопровождение.
Если любишь SQL не только за мощь, но и за элегантность, то виртуальные столбцы — почти всегда любовь с первого запроса.
Синтаксис
CREATE TABLE table_name (
col1 datatype,
col2 datatype,
calc_col GENERATED ALWAYS AS (expression) VIRTUAL
)На практике VIRTUAL чаще всего встречается в
CREATE TABLE и
ALTER TABLE.
Ты задаёшь выражение, а Oracle сам вычисляет результат на лету.
Это особенно удобно для производных значений: суммы, даты, статусы, форматированные строки, признаки, хэши и технические ключи.
Где используют
- производные значения без физического хранения
- нормализация данных без дублирования логики
- выражения для индексации и ускорения поиска
- подготовка витрин и технических признаков
- упрощение отчётов и фильтров
- вычисляемые флаги, категории, slug, периодические ключи
100 примеров
1. Полная цена как сумма net и tax
CREATE TABLE invoice_lines (
net_amt NUMBER(10,2),
tax_amt NUMBER(10,2),
gross_amt GENERATED ALWAYS AS (net_amt + tax_amt) VIRTUAL
)2. НДС как 20 процентов от суммы
CREATE TABLE vat_demo (
amount NUMBER(10,2),
vat_amt GENERATED ALWAYS AS (ROUND(amount * 0.2,2)) VIRTUAL
)3. Год из даты продажи через EXTRACT
CREATE TABLE sales_y (
sale_date DATE,
sale_year GENERATED ALWAYS AS (EXTRACT(YEAR FROM sale_date)) VIRTUAL
)4. Месяц продажи через TO_CHAR
CREATE TABLE sales_m (
sale_date DATE,
sale_month GENERATED ALWAYS AS (TO_CHAR(sale_date,'YYYY-MM')) VIRTUAL
)5. Длина имени клиента
CREATE TABLE customer_name_len (
customer_name VARCHAR2(100),
name_len GENERATED ALWAYS AS (LENGTH(customer_name)) VIRTUAL
)6. Email в нижнем регистре для унификации
CREATE TABLE norm_email (
email VARCHAR2(200),
email_lc GENERATED ALWAYS AS (LOWER(email)) VIRTUAL
)7. Имя в верхнем регистре для поиска
CREATE TABLE search_name (
full_name VARCHAR2(200),
full_name_uc GENERATED ALWAYS AS (UPPER(full_name)) VIRTUAL
)8. Код без пробелов через TRIM
CREATE TABLE sku_trim (
sku_raw VARCHAR2(50),
sku_norm GENERATED ALWAYS AS (TRIM(sku_raw)) VIRTUAL
)9. Префикс артикула через SUBSTR
CREATE TABLE sku_prefix (
sku VARCHAR2(50),
sku_pref GENERATED ALWAYS AS (SUBSTR(sku,1,3)) VIRTUAL
)10. Домен email после символа @
CREATE TABLE email_domain_demo (
email VARCHAR2(200),
domain_name GENERATED ALWAYS AS (SUBSTR(email,INSTR(email,'@') + 1)) VIRTUAL
)11. Остаток от деления как технический bucket
CREATE TABLE hash_bucket_demo (
user_id NUMBER,
bucket_no GENERATED ALWAYS AS (MOD(user_id,16)) VIRTUAL
)12. Возраст в годах от даты рождения
CREATE TABLE person_age_demo (
birth_date DATE,
age_years GENERATED ALWAYS AS (TRUNC(MONTHS_BETWEEN(SYSDATE,birth_date)/12)) VIRTUAL
)13. Первый день месяца по дате
CREATE TABLE month_start_demo (
txn_date DATE,
month_start GENERATED ALWAYS AS (TRUNC(txn_date,'MM')) VIRTUAL
)14. Последний день месяца по дате
CREATE TABLE month_end_demo (
txn_date DATE,
month_end GENERATED ALWAYS AS (LAST_DAY(txn_date)) VIRTUAL
)15. Квартал из даты
CREATE TABLE quarter_demo (
txn_date DATE,
quarter_no GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(txn_date,'Q'))) VIRTUAL
)16. Год и месяц в формате YYYYMM
CREATE TABLE ym_demo (
txn_date DATE,
ym_key GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(txn_date,'YYYYMM'))) VIRTUAL
)17. День недели как текст
CREATE TABLE weekday_demo (
txn_date DATE,
weekday_name GENERATED ALWAYS AS (TO_CHAR(txn_date,'DY','NLS_DATE_LANGUAGE=ENGLISH')) VIRTUAL
)18. Флаг выходного дня через CASE
CREATE TABLE weekend_flag_demo (
txn_date DATE,
is_weekend GENERATED ALWAYS AS (
CASE
WHEN TO_CHAR(txn_date,'DY','NLS_DATE_LANGUAGE=ENGLISH') IN ('SAT','SUN') THEN 1
ELSE 0
END
) VIRTUAL
)19. Флаг высокой суммы заказа
CREATE TABLE high_value_demo (
amount NUMBER(12,2),
high_value_flag GENERATED ALWAYS AS (CASE WHEN amount >= 10000 THEN 1 ELSE 0 END) VIRTUAL
)20. Категория клиента по количеству заказов
CREATE TABLE customer_band_demo (
orders_cnt NUMBER,
customer_band GENERATED ALWAYS AS (
CASE
WHEN orders_cnt >= 100 THEN 'VIP'
WHEN orders_cnt >= 20 THEN 'REGULAR'
ELSE 'NEW'
END
) VIRTUAL
)Еще 20 примеров
21. Нормализованный телефон без лишних символов
CREATE TABLE phone_norm_demo (
phone_raw VARCHAR2(50),
phone_digits GENERATED ALWAYS AS (REGEXP_REPLACE(phone_raw,'\D')) VIRTUAL
)22. Проверка email по regexp с флагом
CREATE TABLE email_check_demo (
email VARCHAR2(200),
email_valid GENERATED ALWAYS AS (
CASE WHEN REGEXP_LIKE(email,'^[^@]+@[^@]+\.[^@]+$') THEN 1 ELSE 0 END
) VIRTUAL
)23. Slug из заголовка
CREATE TABLE slug_demo (
title VARCHAR2(200),
slug_val GENERATED ALWAYS AS (LOWER(REPLACE(TRIM(title),' ','-'))) VIRTUAL
)24. Количество слов в строке
CREATE TABLE word_count_demo (
phrase VARCHAR2(300),
word_cnt GENERATED ALWAYS AS (
REGEXP_COUNT(TRIM(phrase),'\S+')
) VIRTUAL
)25. Префикс кода страны из телефона
CREATE TABLE phone_prefix_demo (
phone_raw VARCHAR2(50),
cc_prefix GENERATED ALWAYS AS (SUBSTR(REGEXP_REPLACE(phone_raw,'\D'),1,3)) VIRTUAL
)26. Инициалы из имени и фамилии
CREATE TABLE initials_demo (
first_name VARCHAR2(50),
last_name VARCHAR2(50),
initials GENERATED ALWAYS AS (UPPER(SUBSTR(first_name,1,1) || SUBSTR(last_name,1,1))) VIRTUAL
)27. Полное имя через конкатенацию
CREATE TABLE full_name_demo (
first_name VARCHAR2(50),
last_name VARCHAR2(50),
full_name GENERATED ALWAYS AS (TRIM(first_name || ' ' || last_name)) VIRTUAL
)28. Маска email для безопасного вывода
CREATE TABLE email_mask_demo (
email VARCHAR2(200),
email_masked GENERATED ALWAYS AS (REGEXP_REPLACE(email,'(^.).+(@.*$)','\1***\2')) VIRTUAL
)29. Вычисление скидочной цены
CREATE TABLE discount_demo (
price NUMBER(10,2),
discount_pct NUMBER(5,2),
price_after_discount GENERATED ALWAYS AS (ROUND(price * (1 - discount_pct / 100),2)) VIRTUAL
)30. Наценка как разница между ценой продажи и себестоимостью
CREATE TABLE margin_demo (
cost_amt NUMBER(10,2),
sale_amt NUMBER(10,2),
margin_amt GENERATED ALWAYS AS (sale_amt - cost_amt) VIRTUAL
)31. Маржа в процентах
CREATE TABLE margin_pct_demo (
cost_amt NUMBER(10,2),
sale_amt NUMBER(10,2),
margin_pct GENERATED ALWAYS AS (
CASE WHEN sale_amt = 0 THEN 0 ELSE ROUND((sale_amt - cost_amt) / sale_amt * 100,2) END
) VIRTUAL
)32. Налоговый период в формате YYYY-Q
CREATE TABLE tax_period_demo (
doc_date DATE,
tax_period GENERATED ALWAYS AS (TO_CHAR(doc_date,'YYYY') || '-Q' || TO_CHAR(doc_date,'Q')) VIRTUAL
)33. ISO-неделя даты
CREATE TABLE iso_week_demo (
dt DATE,
iso_week GENERATED ALWAYS AS (TO_CHAR(dt,'IYYY-IW')) VIRTUAL
)34. Час события из timestamp
CREATE TABLE event_hour_demo (
event_ts TIMESTAMP,
event_hour GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(event_ts,'HH24'))) VIRTUAL
)35. День месяца как число
CREATE TABLE day_no_demo (
dt DATE,
day_no GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(dt,'DD'))) VIRTUAL
)36. Признак конца месяца
CREATE TABLE month_edge_demo (
dt DATE,
is_month_end GENERATED ALWAYS AS (CASE WHEN dt = LAST_DAY(dt) THEN 1 ELSE 0 END) VIRTUAL
)37. Признак начала месяца
CREATE TABLE month_begin_demo (
dt DATE,
is_month_begin GENERATED ALWAYS AS (CASE WHEN dt = TRUNC(dt,'MM') THEN 1 ELSE 0 END) VIRTUAL
)38. Числовой sign-код по сумме
CREATE TABLE sign_demo (
amount NUMBER(10,2),
sign_code GENERATED ALWAYS AS (SIGN(amount)) VIRTUAL
)39. Флаг нулевого остатка
CREATE TABLE zero_stock_demo (
qty NUMBER,
zero_flag GENERATED ALWAYS AS (CASE WHEN qty = 0 THEN 1 ELSE 0 END) VIRTUAL
)40. Группа по возрасту склада в днях
CREATE TABLE aging_demo (
load_date DATE,
aging_band GENERATED ALWAYS AS (
CASE
WHEN SYSDATE - load_date < 30 THEN 'NEW'
WHEN SYSDATE - load_date < 90 THEN 'MID'
ELSE 'OLD'
END
) VIRTUAL
)Еще 20 примеров
41. Хэш строки через STANDARD_HASH
CREATE TABLE hash_demo (
payload VARCHAR2(4000),
payload_hash GENERATED ALWAYS AS (STANDARD_HASH(payload,'SHA256')) VIRTUAL
)42. Контрольная сумма через ORA_HASH
CREATE TABLE ora_hash_demo (
payload VARCHAR2(4000),
hash_num GENERATED ALWAYS AS (ORA_HASH(payload)) VIRTUAL
)43. JSON-значение из payload
CREATE TABLE json_value_demo (
payload CLOB CHECK (payload IS JSON),
order_id GENERATED ALWAYS AS (JSON_VALUE(payload,'$.orderId' RETURNING NUMBER)) VIRTUAL
)44. JSON-флаг активности
CREATE TABLE json_flag_demo (
payload CLOB CHECK (payload IS JSON),
active_flag GENERATED ALWAYS AS (JSON_VALUE(payload,'$.active' RETURNING NUMBER)) VIRTUAL
)45. XML длина документа
CREATE TABLE xml_len_demo (
xml_txt CLOB,
xml_len GENERATED ALWAYS AS (DBMS_LOB.GETLENGTH(xml_txt)) VIRTUAL
)46. Виртуальный столбец для индекса по UPPER(email)
CREATE TABLE email_idx_demo (
email VARCHAR2(200),
email_upper GENERATED ALWAYS AS (UPPER(email)) VIRTUAL
)47. Индекс по виртуальному столбцу
CREATE TABLE email_idx_ready (
email VARCHAR2(200),
email_upper GENERATED ALWAYS AS (UPPER(email)) VIRTUAL
)48. Индекс CREATE INDEX на виртуальное поле
CREATE INDEX idx_email_upper
ON email_idx_ready(email_upper)49. Уникальность по нормализованному логину
CREATE TABLE login_norm_demo (
login_raw VARCHAR2(100),
login_norm GENERATED ALWAYS AS (LOWER(TRIM(login_raw))) VIRTUAL,
CONSTRAINT uq_login_norm UNIQUE (login_norm)
)50. Партиционный ключ как YYYYMM из даты
CREATE TABLE partition_key_demo (
doc_date DATE,
ym_key GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(doc_date,'YYYYMM'))) VIRTUAL
)51. Префикс карты для BIN-аналитики
CREATE TABLE card_bin_demo (
card_no VARCHAR2(20),
bin_code GENERATED ALWAYS AS (SUBSTR(card_no,1,6)) VIRTUAL
)52. Последние 4 цифры карты
CREATE TABLE card_tail_demo (
card_no VARCHAR2(20),
card_tail GENERATED ALWAYS AS (SUBSTR(card_no,-4)) VIRTUAL
)53. Маска карты для UI
CREATE TABLE card_mask_demo (
card_no VARCHAR2(20),
card_mask GENERATED ALWAYS AS ('**** **** **** ' || SUBSTR(card_no,-4)) VIRTUAL
)54. Страна из адреса по префиксу zipcode
CREATE TABLE zip_prefix_demo (
zip_code VARCHAR2(20),
zip_prefix GENERATED ALWAYS AS (SUBSTR(zip_code,1,3)) VIRTUAL
)55. Размер текста по диапазонам
CREATE TABLE text_size_demo (
body_txt VARCHAR2(4000),
size_band GENERATED ALWAYS AS (
CASE
WHEN LENGTH(body_txt) < 100 THEN 'SHORT'
WHEN LENGTH(body_txt) < 500 THEN 'MID'
ELSE 'LONG'
END
) VIRTUAL
)56. Признак латиницы в имени
CREATE TABLE latin_flag_demo (
person_name VARCHAR2(200),
latin_flag GENERATED ALWAYS AS (
CASE WHEN REGEXP_LIKE(person_name,'^[A-Za-z ]+$') THEN 1 ELSE 0 END
) VIRTUAL
)57. Номер недели месяца
CREATE TABLE week_of_month_demo (
dt DATE,
wom_no GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(dt,'W'))) VIRTUAL
)58. Амортизационный период в месяцах
CREATE TABLE asset_age_demo (
purchase_date DATE,
age_months GENERATED ALWAYS AS (TRUNC(MONTHS_BETWEEN(SYSDATE,purchase_date))) VIRTUAL
)59. Признак истечения срока
CREATE TABLE expiry_flag_demo (
expiry_date DATE,
expired_flag GENERATED ALWAYS AS (CASE WHEN expiry_date < SYSDATE THEN 1 ELSE 0 END) VIRTUAL
)60. Чистый URL без query string
CREATE TABLE url_clean_demo (
full_url VARCHAR2(1000),
url_path GENERATED ALWAYS AS (REGEXP_REPLACE(full_url,'\?.*$')) VIRTUAL
)Еще 20 примеров
61. Хост из URL через regexp
CREATE TABLE url_host_demo (
full_url VARCHAR2(1000),
host_name GENERATED ALWAYS AS (REGEXP_SUBSTR(full_url,'https?://([^/]+)',1,1,NULL,1)) VIRTUAL
)62. Признак HTTPS-ссылки
CREATE TABLE https_flag_demo (
full_url VARCHAR2(1000),
https_flag GENERATED ALWAYS AS (CASE WHEN full_url LIKE 'https://%' THEN 1 ELSE 0 END) VIRTUAL
)63. Категория оценки NPS
CREATE TABLE nps_demo (
score NUMBER,
nps_group GENERATED ALWAYS AS (
CASE
WHEN score >= 9 THEN 'PROMOTER'
WHEN score >= 7 THEN 'PASSIVE'
ELSE 'DETRACTOR'
END
) VIRTUAL
)64. Категория риска по скорингу
CREATE TABLE risk_band_demo (
risk_score NUMBER,
risk_band GENERATED ALWAYS AS (
CASE
WHEN risk_score >= 800 THEN 'LOW'
WHEN risk_score >= 600 THEN 'MID'
ELSE 'HIGH'
END
) VIRTUAL
)65. Идентификатор года и недели
CREATE TABLE year_week_demo (
dt DATE,
yw_key GENERATED ALWAYS AS (TO_CHAR(dt,'IYYY') || TO_CHAR(dt,'IW')) VIRTUAL
)66. Год и квартал как ключ отчёта
CREATE TABLE yq_key_demo (
dt DATE,
yq_key GENERATED ALWAYS AS (TO_CHAR(dt,'YYYY') || 'Q' || TO_CHAR(dt,'Q')) VIRTUAL
)67. Признак пустого текста
CREATE TABLE blank_text_demo (
body_txt VARCHAR2(500),
blank_flag GENERATED ALWAYS AS (CASE WHEN TRIM(body_txt) IS NULL THEN 1 ELSE 0 END) VIRTUAL
)68. Безопасная длина с NVL
CREATE TABLE safe_len_demo (
body_txt VARCHAR2(500),
body_len GENERATED ALWAYS AS (NVL(LENGTH(body_txt),0)) VIRTUAL
)69. Числовой день недели
CREATE TABLE dow_num_demo (
dt DATE,
dow_num GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(dt,'D'))) VIRTUAL
)70. Часовой bucket 0-3-6-…-21
CREATE TABLE hour_bucket_demo (
event_ts TIMESTAMP,
hour_bucket GENERATED ALWAYS AS (TRUNC(TO_NUMBER(TO_CHAR(event_ts,'HH24'))/3)*3) VIRTUAL
)71. Валюта и сумма в одной строке
CREATE TABLE money_label_demo (
ccy CHAR(3),
amount NUMBER(12,2),
money_label GENERATED ALWAYS AS (ccy || ' ' || TO_CHAR(amount,'999G999D00')) VIRTUAL
)72. Признак положительной суммы
CREATE TABLE positive_flag_demo (
amount NUMBER,
positive_flag GENERATED ALWAYS AS (CASE WHEN amount > 0 THEN 1 ELSE 0 END) VIRTUAL
)73. Флаг отрицательной суммы
CREATE TABLE negative_flag_demo (
amount NUMBER,
negative_flag GENERATED ALWAYS AS (CASE WHEN amount < 0 THEN 1 ELSE 0 END) VIRTUAL
)74. Признак нулевой суммы
CREATE TABLE zero_amount_demo (
amount NUMBER,
zero_flag GENERATED ALWAYS AS (CASE WHEN amount = 0 THEN 1 ELSE 0 END) VIRTUAL
)75. Разница в днях между двумя датами
CREATE TABLE date_diff_demo (
start_dt DATE,
end_dt DATE,
diff_days GENERATED ALWAYS AS (end_dt - start_dt) VIRTUAL
)76. Разница в часах между timestamp-ами
CREATE TABLE ts_diff_demo (
started_at TIMESTAMP,
finished_at TIMESTAMP,
diff_hours GENERATED ALWAYS AS (
EXTRACT(DAY FROM (finished_at - started_at)) * 24 +
EXTRACT(HOUR FROM (finished_at - started_at))
) VIRTUAL
)77. Признак нового клиента по дате регистрации
CREATE TABLE newcomer_demo (
reg_date DATE,
newcomer_flag GENERATED ALWAYS AS (CASE WHEN reg_date >= ADD_MONTHS(TRUNC(SYSDATE),-1) THEN 1 ELSE 0 END) VIRTUAL
)78. Категория по количеству символов SKU
CREATE TABLE sku_size_demo (
sku VARCHAR2(50),
sku_type GENERATED ALWAYS AS (CASE WHEN LENGTH(sku)=8 THEN 'SHORT' ELSE 'LONG' END) VIRTUAL
)79. Удаление HTML-тегов из текста
CREATE TABLE html_strip_demo (
body_html VARCHAR2(4000),
body_plain GENERATED ALWAYS AS (REGEXP_REPLACE(body_html,'<[^>]+>')) VIRTUAL
)80. Чистый поиск без пробелов и регистра
CREATE TABLE search_key_demo (
query_txt VARCHAR2(400),
query_key GENERATED ALWAYS AS (LOWER(REPLACE(TRIM(query_txt),' ',''))) VIRTUAL
)Еще 20 примеров
81. Добавление виртуального столбца через ALTER TABLE
ALTER TABLE employees
ADD (
full_name GENERATED ALWAYS AS (TRIM(first_name || ' ' || last_name)) VIRTUAL
)82. Добавление виртуального признака активности
ALTER TABLE subscriptions
ADD (
active_flag GENERATED ALWAYS AS (CASE WHEN end_date >= SYSDATE THEN 1 ELSE 0 END) VIRTUAL
)83. Добавление виртуального года документа
ALTER TABLE invoices
ADD (
invoice_year GENERATED ALWAYS AS (EXTRACT(YEAR FROM invoice_date)) VIRTUAL
)84. Добавление виртуального tax key
ALTER TABLE tax_doc
ADD (
tax_key GENERATED ALWAYS AS (country_code || '-' || TO_CHAR(doc_date,'YYYYMM')) VIRTUAL
)85. Добавление виртуального хэша email
ALTER TABLE users
ADD (
email_hash GENERATED ALWAYS AS (STANDARD_HASH(LOWER(email),'SHA256')) VIRTUAL
)86. Добавление индекса по новому виртуальному полю
CREATE INDEX idx_users_email_hash
ON users(email_hash)87. Виртуальный столбец в CTAS как обычное выражение результата
CREATE TABLE sales_ctas AS
SELECT amount,
ROUND(amount * 0.2,2) AS vat_amt
FROM sales_source88. Таблица с несколькими виртуальными столбцами сразу
CREATE TABLE multi_virtual_demo (
net_amt NUMBER(10,2),
tax_amt NUMBER(10,2),
gross_amt GENERATED ALWAYS AS (net_amt + tax_amt) VIRTUAL,
margin_pct GENERATED ALWAYS AS (CASE WHEN net_amt = 0 THEN 0 ELSE ROUND(tax_amt / net_amt * 100,2) END) VIRTUAL
)89. Признак premium-пользователя по LTV
CREATE TABLE premium_demo (
ltv_amt NUMBER(12,2),
premium_flag GENERATED ALWAYS AS (CASE WHEN ltv_amt >= 50000 THEN 1 ELSE 0 END) VIRTUAL
)90. Bucket выручки по диапазонам
CREATE TABLE revenue_band_demo (
revenue_amt NUMBER(12,2),
revenue_band GENERATED ALWAYS AS (
CASE
WHEN revenue_amt < 1000 THEN 'SMALL'
WHEN revenue_amt < 10000 THEN 'MEDIUM'
ELSE 'LARGE'
END
) VIRTUAL
)91. Последние 2 символа кода
CREATE TABLE code_tail_demo (
code_raw VARCHAR2(50),
code_tail GENERATED ALWAYS AS (SUBSTR(code_raw,-2)) VIRTUAL
)92. Признак локального номера телефона
CREATE TABLE local_phone_demo (
phone_raw VARCHAR2(50),
local_flag GENERATED ALWAYS AS (CASE WHEN LENGTH(REGEXP_REPLACE(phone_raw,'\D')) = 10 THEN 1 ELSE 0 END) VIRTUAL
)93. Bucket цены по FLOOR
CREATE TABLE price_bucket_demo (
price NUMBER(10,2),
price_bucket GENERATED ALWAYS AS (FLOOR(price / 100)) VIRTUAL
)94. URL slug с заменой лишних дефисов
CREATE TABLE smart_slug_demo (
title VARCHAR2(300),
slug_val GENERATED ALWAYS AS (
REGEXP_REPLACE(LOWER(REPLACE(TRIM(title),' ','-')),'-+','-')
) VIRTUAL
)95. Флаг наличия пробелов в значении
CREATE TABLE spaces_flag_demo (
code_raw VARCHAR2(100),
has_spaces GENERATED ALWAYS AS (CASE WHEN INSTR(code_raw,' ') > 0 THEN 1 ELSE 0 END) VIRTUAL
)96. Ключ страны и города в одном поле
CREATE TABLE geo_key_demo (
country_code CHAR(2),
city_code VARCHAR2(20),
geo_key GENERATED ALWAYS AS (country_code || ':' || city_code) VIRTUAL
)97. Признак пустого email после trim
CREATE TABLE email_blank_demo (
email VARCHAR2(200),
blank_flag GENERATED ALWAYS AS (CASE WHEN TRIM(email) IS NULL THEN 1 ELSE 0 END) VIRTUAL
)98. Календарный ключ YYYYMMDD
CREATE TABLE date_key_demo (
dt DATE,
date_key GENERATED ALWAYS AS (TO_NUMBER(TO_CHAR(dt,'YYYYMMDD'))) VIRTUAL
)99. Технический merge-ключ из нескольких атрибутов
CREATE TABLE merge_key_demo (
source_code VARCHAR2(20),
ext_id VARCHAR2(50),
merge_key GENERATED ALWAYS AS (LOWER(source_code || ':' || ext_id)) VIRTUAL
)100. Финальный пример: витринный набор с годом, месяцем, флагом и хэшем
CREATE TABLE final_virtual_demo (
event_ts TIMESTAMP,
payload VARCHAR2(4000),
event_year GENERATED ALWAYS AS (EXTRACT(YEAR FROM event_ts)) VIRTUAL,
event_month GENERATED ALWAYS AS (TO_CHAR(event_ts,'YYYY-MM')) VIRTUAL,
payload_len GENERATED ALWAYS AS (NVL(LENGTH(payload),0)) VIRTUAL,
payload_hash GENERATED ALWAYS AS (STANDARD_HASH(payload,'SHA256')) VIRTUAL
)Заключение
VIRTUAL в Oracle — это очень взрослая история про чистую модель данных.
Ты не хранишь то, что можно надёжно вычислить. Ты не дублируешь логику там, где её можно централизовать.
И именно поэтому виртуальные столбцы так хороши: они делают схему аккуратнее, запросы понятнее, а сопровождение приятнее.
Oracle в этом месте особенно красив — даёт выразительность SQL без лишнего мусора в таблицах.
Официальная документация Oracle:
https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/CREATE-TABLE.html
🔜 Следующая статья:
VIEW в Oracle SQL — как создавать представления для запросов