Chapter 09 · Index Design, FULLTEXT, Spatial, Vector, and Specialized Access Paths

Functional/Expression and Generated-Column Indexing Patterns

Index deterministic expressions in MariaDB with generated columns, exact type/collation semantics, and 11.8+ virtual-column optimizer substitution—without importing MySQL functional-index syntax.

Intermediate → Advanced90–110 minutesGenerated-column expression index labMariaDB Community 12.3.2 baselineInnoDB + free local serverLast reviewed: August 2026

Learning outcomes

ServiceHub stores user email exactly as entered, but login lookups must be case-normalized. Another table stores a small JSON document whose severity field is frequently filtered. A developer coming from MySQL 8 tries CREATE INDEX idx_lower_email ON users ((LOWER(email))) and assumes MariaDB accepts the same functional-index DDL. On the current MariaDB line, that is the wrong portability assumption.

MariaDB’s practical pattern is to define a generated column whose value is a deterministic expression and index that column. From MariaDB 11.8, the optimizer can also recognize the exact indexed virtual-column expression in a WHERE condition and substitute the virtual column; from MariaDB 12.1, related recognition was improved for ORDER BY/GROUP BY. MariaDB still requires the generated-column object rather than MySQL-style direct expression-index syntax.

01

Distinguish direct expression-index syntax from MariaDB generated-column indexing.

02

Choose VIRTUAL versus PERSISTENT/STORED generated columns from evaluation, storage and maintenance needs.

03

Build deterministic indexed expressions with explicit data types and collations.

04

Use MariaDB 11.8+ virtual-column substitution without assuming semantically equivalent expressions match.

05

Diagnose expression/type/collation mismatch and measure the write/read tradeoff of maintaining derived keys.

Version discipline

Mandatory examples target MariaDB 12.3.2. The optimizer feature that recognizes indexed virtual-column expressions in WHERE is available from 11.8. MariaDB documentation explicitly notes that direct CREATE INDEX ON expression syntax is not the MariaDB mechanism; create a virtual/generated column and index it.

1. Prove the MySQL-style assumption wrong first

sql · intentionally non-portable direct expression-index attempt
USE servicehub_index_lab;CREATE TABLE service_users (  user_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,  email VARCHAR(254) NOT NULL,  profile JSON NOT NULL) ENGINE=InnoDB;-- Intentionally use syntax seen in another product/version family:CREATE INDEX idx_lower_email_directON service_users ((LOWER(email)));

Do not memorize a specific parser error number here; exact diagnostics can evolve. The learning point is the contract: current MariaDB documentation says to create a generated/virtual column and index that column. Compatibility claims must be tested against the exact product and version rather than inferred from shared SQL heritage.

2. Generated columns turn an expression into an indexable schema object

sql · case-normalized and JSON-derived keys
ALTER TABLE service_users  ADD COLUMN email_norm VARCHAR(254)    COLLATE utf8mb4_uca1400_ai_ci    AS (LOWER(email)) VIRTUAL,  ADD COLUMN severity INT    AS (CAST(JSON_VALUE(profile,'$.severity') AS INTEGER)) VIRTUAL,  ADD INDEX idx_email_norm (email_norm),  ADD INDEX idx_severity (severity);INSERT INTO service_users(email,profile) VALUES ('Alice.Example@Example.COM','{"severity":2,"team":"network"}'), ('bob@example.com','{"severity":5,"team":"field"}'), ('carol@example.com','{"severity":2,"team":"network"}');SHOW CREATE TABLE service_users\GSHOW INDEX FROM service_users;

A VIRTUAL generated column computes its value when needed rather than storing the derived value as a normal field. A PERSISTENT (also accepted as STORED) generated column stores the generated result. MariaDB can index both virtual and persistent generated columns when the storage engine supports them. The index itself still consumes storage and must be maintained on writes even if the generated column is virtual.

3. On 11.8+, the optimizer can substitute the exact virtual expression in WHERE

sql · compare explicit virtual-column and expression predicates
EXPLAINSELECT user_id,emailFROM service_usersWHERE severity=2;EXPLAINSELECT user_id,emailFROM service_usersWHERE CAST(JSON_VALUE(profile,'$.severity') AS INTEGER)=2;ANALYZE FORMAT=JSONSELECT user_id,emailFROM service_usersWHERE CAST(JSON_VALUE(profile,'$.severity') AS INTEGER)=2;

On MariaDB 11.8 and later, the optimizer can recognize the exact expression used by an indexed virtual column and rewrite the condition to use that virtual column for range/ref access. This is a targeted optimizer transformation, not a general theorem prover. The expression must match the generated-column definition exactly enough for substitution.

Observable evidence

Use EXPLAIN to confirm the selected key and ANALYZE FORMAT=JSON to compare estimated rows with observed rows. If optimizer trace is enabled in a disposable diagnostic session, virtual-column substitution can also appear in the trace. Do not enable heavyweight tracing globally on production merely to prove a classroom point.

4. Deliberately wrong: change the expression or collation and expect the same index

sql · expressions that look similar to a human
-- The indexed definition casts to INTEGER.EXPLAIN SELECT user_idFROM service_usersWHERE JSON_VALUE(profile,'$.severity') = '2';-- The indexed definition uses LOWER(email) with a declared collation.EXPLAIN SELECT user_idFROM service_usersWHERE UCASE(email) = UCASE('alice.example@example.com');

These predicates can be semantically related to the generated values but are not the same indexed expression. Type conversion and collation are part of the expression contract. The safe repair is either to query the generated column directly—WHERE severity=2, WHERE email_norm=LOWER(?)—or to use the exact supported expression and verify the plan on the target MariaDB version.

For string extraction from JSON, MariaDB documentation specifically warns that JSON_VALUE inherits a binary-style collation context from JSON text. If your application expects a Unicode case-insensitive collation, declare that collation on the generated column and keep query comparison semantics aligned with it.

5. Determinism is a correctness requirement, not only an optimizer preference

Indexed or persistent generated values must remain consistent when the row is written and later searched. Expressions depending on nondeterministic state, external data, session modes or mutable configuration can make that promise unsafe. MariaDB permits many generated expressions but restricts nondeterministic functions for indexed virtual or persistent generated columns.

sql · inspect generated-column metadata
SELECT COLUMN_NAME,DATA_TYPE,COLLATION_NAME,IS_GENERATED,GENERATION_EXPRESSION,EXTRAFROM information_schema.COLUMNSWHERE TABLE_SCHEMA='servicehub_index_lab'  AND TABLE_NAME='service_users'ORDER BY ORDINAL_POSITION;

Treat GENERATION_EXPRESSION, data type and collation as deployable schema. A migration that silently changes the expression, SQL mode or collation can change lookup semantics even when application SQL is unchanged. Keep generated-index definitions under schema migration control and validate them during upgrades.

6. VIRTUAL versus PERSISTENT is a workload decision

Choice Benefit Cost / risk
VIRTUAL + index Avoids storing an additional table column value while providing an index key Expression is evaluated for index maintenance and reads that need the column; index still consumes space.
PERSISTENT + index Stores the derived value and can reduce repeated computation for selected reads Adds row storage plus index storage and write maintenance.
Application-computed real column Maximum control and cross-database portability Application must keep the derived value correct; duplication can drift without constraints/process discipline.
No derived index Simplest writes/schema Expression predicates may require scans or other access paths.

Do not select PERSISTENT because it sounds “faster,” or VIRTUAL because it sounds “free.” Build a representative dataset, measure write rate and query rate, and compare execution evidence. The same expression can have very different economics on a read-heavy catalog versus a write-heavy event stream.

7. Reproducible lab, verification, and production judgment

  1. Create service_users and observe the intentionally unsupported direct-expression-index attempt.
  2. Add virtual generated columns and indexes with explicit type/collation.
  3. Use EXPLAIN on direct generated-column predicates.
  4. On MariaDB 11.8+, test the exact expression and verify whether the optimizer chooses the generated-column index.
  5. Change the expression or collation and observe the plan difference.
  6. Record schema metadata so the generated expression is part of migration review.

Check your understanding

  1. What is the MariaDB pattern for indexing LOWER(email) on the current baseline?
  2. Why can a VIRTUAL generated column still increase write cost when indexed?
  3. What changed in MariaDB 11.8 for indexed virtual-column expressions?
  4. Why can a different cast or collation prevent expected index use?
  5. When might PERSISTENT be reasonable instead of VIRTUAL?
Review the answers

Define a generated column such as email_norm AS (LOWER(email)) and index that column; MariaDB does not use MySQL-style direct expression-index DDL for this case. An index over a virtual column still stores and maintains index entries on writes. From 11.8, the optimizer can recognize the exact indexed virtual-column expression in WHERE and substitute the column for range/ref access. Type/collation differences change the expression and comparison contract. PERSISTENT can be reasonable when storing the derived result is worth its row-space/write cost for the measured workload.

The next lesson applies indexing to tokenized text search. FULLTEXT is not a B-tree over strings; it has its own token, stopword, relevance and rebuild semantics.

8. Treat expression indexes as versioned schema contracts

A generated-column index is more than a convenience for one query. It becomes part of the physical schema contract that application code, migration scripts, collations, SQL mode and optimizer behavior depend on. Before deploying one, capture SHOW CREATE TABLE, the exact generation expression, data type, collation and representative EXPLAIN output. Repeat those checks after a MariaDB upgrade because optimizer capabilities can improve between releases even when the table definition is unchanged.

This matters especially for JSON-derived keys. MariaDB stores JSON using its own semantics, and JSON_VALUE() returns a scalar whose SQL type/collation must be made explicit when the indexed business key is numeric or case-insensitive text. A generated expression that casts a value to INTEGER is not interchangeable with a string comparison simply because today’s sample data contains only digits. Likewise, a case-folded email key must use a collation that matches the login identity rule, not whatever server default happens to be active during deployment.

For zero-downtime schema change, treat creation of the generated column and its index like any other potentially expensive DDL. Estimate table size, test DDL algorithm/locking behavior on the exact release, monitor replicas or Galera if present, and have a rollback plan. On a busy table, the operational cost of building an index can dominate the eventual per-query benefit. The correct acceptance criterion therefore has two parts: the new access path must improve the target workload, and the deployment must fit the system’s change window and recovery model.

Production judgment

Prefer the simplest derived key that expresses a stable business predicate. If application code can query a real normalized column directly with clearer semantics, that may be better than hiding complex logic in a generated expression. Use generated-column indexing when it improves correctness and access-path clarity—not merely to avoid changing SQL.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.