VIRTUAL в Oracle SQL — как создавать вычисляемые столбцы

🟢 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_source

88. Таблица с несколькими виртуальными столбцами сразу

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 — как создавать представления для запросов


 

Понравилась статья? Поделиться с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии