MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MariaDB虚拟列的定义与使用场景

2022-03-252.1k 阅读

MariaDB虚拟列的定义

在 MariaDB 数据库中,虚拟列(Virtual Column)是一种特殊类型的列,它的值并非实际存储在数据库表中,而是通过表达式计算得出。虚拟列提供了一种灵活且高效的方式来衍生新的数据,这些数据可以基于表中其他列的值进行计算,从而为数据库设计和查询提供更多的便利。

虚拟列的基本概念

虚拟列在数据库表结构中看起来与普通列无异,但实际上它们并不占用额外的物理存储空间。当查询涉及到虚拟列时,MariaDB 会实时根据定义虚拟列的表达式计算其值。这种特性使得虚拟列在某些场景下能够极大地简化数据处理逻辑,同时避免了冗余数据的存储。

例如,假设有一个存储商品信息的表,其中包含 price(价格)和 quantity(数量)两列。如果我们经常需要获取商品的总价值(即 total_value = price * quantity),传统方式可能需要在应用程序层面每次查询时都进行乘法运算,或者存储一个额外的 total_value 列,这样会造成数据冗余。而使用虚拟列,我们可以在表中定义一个虚拟列 total_value,其值根据 pricequantity 实时计算得出,既不需要在应用程序中重复计算,也不会占用额外的存储空间。

虚拟列的定义语法

在 MariaDB 中,定义虚拟列使用 CREATE TABLE 语句或者 ALTER TABLE 语句。下面是 CREATE TABLE 语句中定义虚拟列的基本语法:

CREATE TABLE table_name (
    column1 datatype,
    column2 datatype,
   ...
    virtual_column_name datatype [GENERATED ALWAYS] AS (expression) [VIRTUAL | STORED]
);
  • virtual_column_name:虚拟列的名称。
  • datatype:虚拟列的数据类型,需要与表达式的返回值类型兼容。
  • GENERATED ALWAYS:这是可选关键字,表明该列是生成列(虚拟列或存储生成列)。如果省略,默认也是生成列。
  • expression:用于计算虚拟列值的表达式,它可以引用同一表中的其他列。
  • VIRTUAL:表明该列是虚拟列,值不存储,查询时实时计算。这是默认选项。
  • STORED:表明该列是存储生成列,值会实际存储在数据库中,查询时直接读取存储的值,而不是实时计算。

以下是一个简单的示例,创建一个包含虚拟列的表:

CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    price DECIMAL(10, 2),
    quantity INT,
    total_value DECIMAL(15, 2) GENERATED ALWAYS AS (price * quantity) VIRTUAL
);

在上述示例中,total_value 是一个虚拟列,它的值通过 pricequantity 的乘积计算得出。

如果要使用 ALTER TABLE 语句添加虚拟列,语法如下:

ALTER TABLE table_name
ADD [COLUMN] virtual_column_name datatype [GENERATED ALWAYS] AS (expression) [VIRTUAL | STORED];

例如,为已有的 products 表添加一个基于 price 计算的折扣后价格虚拟列:

ALTER TABLE products
ADD COLUMN discounted_price DECIMAL(10, 2) GENERATED ALWAYS AS (price * 0.8) VIRTUAL;

虚拟列的数据类型

虚拟列的数据类型必须与表达式的返回值类型兼容。例如,如果表达式返回一个整数类型的值,那么虚拟列的数据类型也应该是整数类型(如 INTBIGINT 等)。如果返回的是字符串类型的值,虚拟列的数据类型可以是 VARCHARTEXT 等字符串类型。

在定义虚拟列时,如果不指定数据类型,MariaDB 会尝试根据表达式推断数据类型,但为了确保准确性和兼容性,最好显式指定数据类型。例如:

CREATE TABLE employees (
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    full_name VARCHAR(101) GENERATED ALWAYS AS (CONCAT(first_name,'', last_name)) VIRTUAL
);

这里 full_name 虚拟列的数据类型 VARCHAR(101) 足够容纳 first_namelast_name 连接后的字符串长度(假设中间有一个空格)。

虚拟列与存储生成列的区别

虽然虚拟列和存储生成列都是通过表达式生成的列,但它们在存储方式和性能上有一些关键区别。

  • 存储方式

    • 虚拟列:值不实际存储在数据库中,每次查询涉及虚拟列时,MariaDB 会实时计算其值。这意味着虚拟列不会占用额外的物理存储空间,适合存储那些计算成本较低且不需要频繁访问的值。
    • 存储生成列:值会实际存储在数据库中,就像普通列一样。这种方式虽然会占用一定的存储空间,但在查询时可以直接读取存储的值,而不需要实时计算,适合计算成本较高或者需要频繁访问的值。
  • 性能影响

    • 虚拟列:由于每次查询都要计算值,如果表达式计算复杂,可能会增加查询的响应时间。但是对于简单的表达式,实时计算的开销通常较小,并且避免了存储冗余数据带来的空间开销和数据一致性维护成本。
    • 存储生成列:查询时直接读取存储的值,性能相对稳定,特别是对于复杂表达式的计算结果。然而,插入和更新操作可能会变慢,因为每次数据变动都需要重新计算并存储生成列的值。

例如,对于一个经常需要根据多个复杂条件计算的统计值,使用存储生成列可能更合适,因为可以减少每次查询的计算开销;而对于简单的基于其他列的数学运算结果,虚拟列则既能满足需求又不占用额外空间。

MariaDB虚拟列的使用场景

数据衍生与计算

  1. 计算统计值:在许多业务场景中,需要根据已有数据计算各种统计值。例如,在销售数据报表中,除了记录每笔销售的单价和数量外,还经常需要计算销售总额、平均价格等。使用虚拟列可以轻松实现这些计算。
CREATE TABLE sales (
    sale_id INT AUTO_INCREMENT PRIMARY KEY,
    product_id INT,
    unit_price DECIMAL(10, 2),
    quantity INT,
    total_amount DECIMAL(15, 2) GENERATED ALWAYS AS (unit_price * quantity) VIRTUAL,
    average_price DECIMAL(10, 2) GENERATED ALWAYS AS (IF(quantity > 0, unit_price, 0)) VIRTUAL
);

在上述 sales 表中,total_amount 虚拟列计算每笔销售的总额,average_price 虚拟列计算平均价格(当数量大于 0 时)。这样在查询销售数据时,可以直接获取这些计算结果,而无需在应用程序中进行额外计算。

  1. 基于日期时间的计算:如果数据库表中包含日期时间列,虚拟列可以用于进行基于日期时间的衍生计算。例如,计算年龄、时间间隔等。
CREATE TABLE customers (
    customer_id INT AUTO_INCREMENT PRIMARY KEY,
    birth_date DATE,
    age INT GENERATED ALWAYS AS (YEAR(CURDATE()) - YEAR(birth_date)) VIRTUAL
);

customers 表中,age 虚拟列根据 birth_date 计算客户的年龄。每次查询客户信息时,都能实时获取准确的年龄值,而不需要在应用程序中进行日期计算。

数据规范化与冗余消除

  1. 避免冗余存储:在数据库设计中,冗余数据的存储可能会导致数据一致性问题。虚拟列提供了一种避免冗余存储的有效方式。例如,在一个包含员工部门信息的表中,部门名称通常存储在 departments 表中,而员工表通过 department_id 关联到 departments 表。如果经常需要在员工表中显示部门名称,可以使用虚拟列来获取。
CREATE TABLE departments (
    department_id INT AUTO_INCREMENT PRIMARY KEY,
    department_name VARCHAR(50)
);

CREATE TABLE employees (
    employee_id INT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    department_id INT,
    department_name VARCHAR(50) GENERATED ALWAYS AS (
        (SELECT department_name FROM departments WHERE departments.department_id = employees.department_id)
    ) VIRTUAL,
    FOREIGN KEY (department_id) REFERENCES departments(department_id)
);

employees 表中,department_name 虚拟列通过子查询从 departments 表获取对应的部门名称。这样既避免了在 employees 表中重复存储部门名称,又能在查询员工信息时方便地获取部门名称,同时保证了数据的一致性。

  1. 数据规范化与视图替代:在某些情况下,传统的视图可以实现类似虚拟列的功能,但虚拟列在数据规范化方面有其独特优势。视图通常是基于多个表的查询结果,而虚拟列直接在表结构层面定义,更紧密地与表数据结合。例如,在一个包含订单和订单详情的数据库中,订单表存储订单的基本信息,订单详情表存储每个订单的具体商品信息。如果需要在订单表中显示订单的总金额,可以使用虚拟列。
CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    order_date DATETIME,
    total_amount DECIMAL(15, 2) GENERATED ALWAYS AS (
        (SELECT SUM(od.quantity * od.unit_price) FROM order_details od WHERE od.order_id = orders.order_id)
    ) VIRTUAL
);

CREATE TABLE order_details (
    order_detail_id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT,
    product_id INT,
    quantity INT,
    unit_price DECIMAL(10, 2),
    FOREIGN KEY (order_id) REFERENCES orders(order_id)
);

通过这种方式,在订单表中定义 total_amount 虚拟列,使得订单表的数据结构更加清晰,同时避免了通过复杂视图来获取订单总金额的操作,提高了数据访问的效率和规范化程度。

数据验证与约束

  1. 基于条件的虚拟列约束:虚拟列可以与 CHECK 约束结合使用,实现更复杂的数据验证。例如,在一个存储员工工资信息的表中,可以定义一个虚拟列来表示工资是否在合理范围内。
CREATE TABLE employees_salary (
    employee_id INT AUTO_INCREMENT PRIMARY KEY,
    salary DECIMAL(10, 2),
    salary_range VARCHAR(20) GENERATED ALWAYS AS (
        CASE 
            WHEN salary >= 1000 AND salary <= 5000 THEN 'Low'
            WHEN salary > 5000 AND salary <= 10000 THEN 'Medium'
            WHEN salary > 10000 THEN 'High'
            ELSE 'Invalid'
        END
    ) VIRTUAL,
    CHECK (salary_range!= 'Invalid')
);

在上述示例中,salary_range 虚拟列根据 salary 的值判断工资所属范围,并且通过 CHECK 约束确保 salary_range 不会为 Invalid,从而间接验证了 salary 的合理性。

  1. 依赖其他列的完整性约束:虚拟列还可以用于创建依赖于其他列的完整性约束。比如,在一个库存管理系统中,需要确保库存数量始终为非负数,并且库存价值(基于单价和数量计算)也符合逻辑。
CREATE TABLE inventory (
    product_id INT AUTO_INCREMENT PRIMARY KEY,
    product_name VARCHAR(50),
    unit_price DECIMAL(10, 2),
    quantity INT,
    inventory_value DECIMAL(15, 2) GENERATED ALWAYS AS (unit_price * quantity) VIRTUAL,
    CHECK (quantity >= 0 AND inventory_value >= 0)
);

这里通过 inventory_value 虚拟列结合 CHECK 约束,确保了库存数量和库存价值的合理性和一致性。

索引与查询优化

  1. 对虚拟列创建索引:虽然虚拟列的值不存储,但在某些情况下,可以对虚拟列创建索引以提高查询性能。例如,在一个包含大量客户订单的表中,经常需要根据订单总金额进行查询。
CREATE TABLE customer_orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT,
    order_date DATETIME,
    unit_price DECIMAL(10, 2),
    quantity INT,
    total_amount DECIMAL(15, 2) GENERATED ALWAYS AS (unit_price * quantity) VIRTUAL
);

CREATE INDEX idx_total_amount ON customer_orders (total_amount);

通过对 total_amount 虚拟列创建索引,当执行涉及 total_amount 的查询时,如 SELECT * FROM customer_orders WHERE total_amount > 1000;,MariaDB 可以利用索引快速定位符合条件的记录,从而提高查询效率。

  1. 查询优化与执行计划:虚拟列的使用可以影响查询的执行计划。在复杂查询中,虚拟列的合理定义可以减少数据扫描和计算的开销。例如,在一个多表关联查询中,如果通过虚拟列预先计算了一些中间结果,查询优化器可以更有效地生成执行计划。

假设有一个电子商务数据库,包含 orders 表、order_items 表和 products 表,需要查询每个订单的总金额以及对应的产品名称。

CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    order_date DATETIME
);

CREATE TABLE order_items (
    order_item_id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT,
    product_id INT,
    quantity INT,
    FOREIGN KEY (order_id) REFERENCES orders(order_id)
);

CREATE TABLE products (
    product_id INT AUTO_INCREMENT PRIMARY KEY,
    product_name VARCHAR(50),
    unit_price DECIMAL(10, 2)
);

-- 在 order_items 表中定义虚拟列计算订单项目金额
ALTER TABLE order_items
ADD COLUMN item_amount DECIMAL(15, 2) GENERATED ALWAYS AS (
    (SELECT unit_price FROM products WHERE products.product_id = order_items.product_id) * order_items.quantity
) VIRTUAL;

-- 查询每个订单的总金额和产品名称
SELECT 
    o.order_id,
    SUM(oi.item_amount) AS total_amount,
    p.product_name
FROM 
    orders o
JOIN 
    order_items oi ON o.order_id = oi.order_id
JOIN 
    products p ON oi.product_id = p.product_id
GROUP BY 
    o.order_id, p.product_name;

在上述示例中,通过在 order_items 表中定义 item_amount 虚拟列,预先计算了每个订单项目的金额。在查询时,查询优化器可以利用这个虚拟列更高效地进行聚合计算和多表关联,从而优化查询性能。

数据展示与用户体验

  1. 简化应用程序数据处理:在应用程序开发中,虚拟列可以简化数据处理逻辑。例如,在一个展示商品信息的电商应用中,商品详情页面除了显示基本的商品属性外,还可能需要显示一些计算得出的信息,如折扣后价格、库存状态等。
CREATE TABLE products (
    product_id INT AUTO_INCREMENT PRIMARY KEY,
    product_name VARCHAR(50),
    original_price DECIMAL(10, 2),
    discount DECIMAL(5, 2),
    discounted_price DECIMAL(10, 2) GENERATED ALWAYS AS (original_price * (1 - discount)) VIRTUAL,
    stock_quantity INT,
    stock_status VARCHAR(20) GENERATED ALWAYS AS (
        CASE 
            WHEN stock_quantity > 0 THEN 'In Stock'
            ELSE 'Out of Stock'
        END
    ) VIRTUAL
);

在应用程序中查询商品信息时,可以直接获取 discounted_pricestock_status 虚拟列的值,无需在应用程序代码中进行额外的计算和逻辑判断,从而简化了应用程序的开发和维护。

  1. 提供更直观的数据视图:对于数据库用户和数据分析人员来说,虚拟列可以提供更直观的数据视图。例如,在一个财务报表数据库中,除了存储原始的财务数据外,可以定义虚拟列来显示财务比率、增长率等分析数据。
CREATE TABLE financial_data (
    record_id INT AUTO_INCREMENT PRIMARY KEY,
    year INT,
    revenue DECIMAL(15, 2),
    profit DECIMAL(15, 2),
    profit_margin DECIMAL(5, 2) GENERATED ALWAYS AS (IF(revenue > 0, profit / revenue, 0)) VIRTUAL,
    prev_year_profit DECIMAL(15, 2),
    profit_growth DECIMAL(5, 2) GENERATED ALWAYS AS (
        IF(prev_year_profit > 0, (profit - prev_year_profit) / prev_year_profit, 0)
    ) VIRTUAL
);

通过这些虚拟列,财务人员可以更方便地进行数据分析,无需手动计算各种财务指标,提高了数据分析的效率和准确性。

数据迁移与兼容性

  1. 在数据迁移中使用虚拟列:在将数据从一个数据库系统迁移到 MariaDB 时,虚拟列可以帮助简化数据转换过程。例如,源数据库中可能有一些计算字段是以冗余存储的方式存在,而在 MariaDB 中可以通过虚拟列来重新实现这些计算,避免数据冗余。

假设从一个遗留系统迁移客户订单数据,源数据库中订单表有一个存储订单总金额的列 total_amount,该值在每次订单明细变化时手动更新。迁移到 MariaDB 后,可以将其转换为虚拟列。

-- 创建新的 MariaDB 订单表
CREATE TABLE migrated_orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    customer_id INT,
    order_date DATETIME,
    unit_price DECIMAL(10, 2),
    quantity INT,
    total_amount DECIMAL(15, 2) GENERATED ALWAYS AS (unit_price * quantity) VIRTUAL
);

-- 迁移数据并插入到新表
INSERT INTO migrated_orders (order_id, customer_id, order_date, unit_price, quantity)
SELECT order_id, customer_id, order_date, unit_price, quantity
FROM legacy_orders;

通过这种方式,在数据迁移过程中不仅避免了冗余存储,还能确保数据的一致性和计算逻辑的准确性。

  1. 兼容性与版本升级:虚拟列的使用在 MariaDB 的版本升级和兼容性方面也有一定作用。随着 MariaDB 版本的更新,可能会引入新的特性和优化,虚拟列的定义和使用方式可能会有所改进。例如,在较新的版本中,对虚拟列的索引支持可能更加完善,查询优化器对虚拟列的处理能力也可能增强。通过合理使用虚拟列,可以在版本升级时利用这些新特性,提升数据库的性能和功能,同时保持数据结构和应用程序逻辑的兼容性。

虚拟列使用的注意事项

表达式的复杂性与性能

  1. 复杂表达式的计算开销:虽然虚拟列提供了灵活的数据计算方式,但如果定义虚拟列的表达式过于复杂,会增加查询的计算开销。例如,涉及大量函数调用、子查询嵌套或者复杂的数学运算的表达式,在每次查询时都会执行计算,可能导致查询性能下降。
CREATE TABLE complex_calculation (
    value1 DECIMAL(10, 2),
    value2 DECIMAL(10, 2),
    complex_result DECIMAL(20, 5) GENERATED ALWAYS AS (
        (SELECT AVG(some_table.some_column) FROM some_table WHERE some_table.id = (
            SELECT another_table.id FROM another_table WHERE another_table.name = 'Some Name'
        )) * value1 + POW(value2, 3)
    ) VIRTUAL
);

在上述示例中,complex_result 虚拟列的表达式包含多层子查询和复杂的数学运算,查询涉及该虚拟列时,性能可能会受到较大影响。在这种情况下,需要评估是否可以将复杂计算提前到应用程序层面进行,或者考虑使用存储生成列来缓存计算结果。

  1. 查询优化与索引:尽管可以对虚拟列创建索引来提高查询性能,但并不是所有情况下索引都能有效发挥作用。对于复杂表达式的虚拟列,索引可能无法覆盖所有查询场景,甚至在某些情况下可能导致查询优化器生成不合理的执行计划。例如,如果虚拟列的表达式包含多个函数调用和条件判断,索引可能无法准确匹配查询条件,从而降低查询效率。因此,在对虚拟列创建索引之前,需要通过执行计划分析等手段,确保索引能够真正优化查询性能。

数据一致性与更新操作

  1. 更新相关列对虚拟列的影响:当表中与虚拟列相关的列发生更新时,虚拟列的值会自动重新计算。然而,这可能会带来一些数据一致性问题,特别是在并发更新的情况下。例如,在一个多用户的库存管理系统中,如果多个用户同时更新库存数量和产品单价,可能会导致虚拟列 inventory_value 的计算出现暂时的不一致。
-- 库存表
CREATE TABLE inventory (
    product_id INT AUTO_INCREMENT PRIMARY KEY,
    product_name VARCHAR(50),
    unit_price DECIMAL(10, 2),
    quantity INT,
    inventory_value DECIMAL(15, 2) GENERATED ALWAYS AS (unit_price * quantity) VIRTUAL
);

为了避免这种情况,可以使用事务来确保相关列的更新操作是原子性的,从而保证虚拟列值的一致性。

START TRANSACTION;
UPDATE inventory SET unit_price = unit_price * 1.1 WHERE product_id = 1;
UPDATE inventory SET quantity = quantity - 5 WHERE product_id = 1;
COMMIT;
  1. 存储生成列的更新性能:对于存储生成列,虽然查询性能较好,但更新操作可能会变得缓慢。因为每次更新相关列时,不仅要更新实际存储的列值,还要重新计算并更新存储生成列的值。如果存储生成列的计算表达式复杂,这种性能影响会更加明显。因此,在选择使用存储生成列时,需要综合考虑插入、更新操作的频率和性能要求。

兼容性与限制

  1. 不同数据库系统的兼容性:虽然 MariaDB 的虚拟列提供了强大的功能,但在不同数据库系统之间,虚拟列的支持和语法可能存在差异。如果项目可能涉及数据库迁移或者与其他数据库系统交互,需要注意虚拟列的兼容性问题。例如,某些数据库系统可能不支持虚拟列,或者支持的方式与 MariaDB 有很大不同。在设计数据库架构时,需要充分考虑这些因素,以确保系统的可移植性。

  2. MariaDB 版本限制:在 MariaDB 的不同版本中,虚拟列的功能和特性可能会有所不同。例如,早期版本可能对虚拟列的索引支持有限,或者在处理复杂表达式时存在性能问题。在使用虚拟列时,需要参考相应版本的官方文档,了解其功能限制和已知问题,以避免在开发和部署过程中遇到意外情况。同时,及时关注 MariaDB 的版本更新,以便在合适的时候利用新的特性和优化来提升虚拟列的使用效果。

安全性与权限

  1. 虚拟列的访问权限:与普通列一样,虚拟列的访问也受到数据库权限的控制。用户需要具有相应的 SELECT 权限才能查询虚拟列的值。在设计数据库权限时,要确保用户只能访问其有权限查看的虚拟列。例如,在一个企业级数据库中,某些虚拟列可能包含敏感的计算结果,如员工的绩效得分、财务预测数据等,需要严格限制只有特定角色的用户才能查询这些虚拟列。
-- 为用户授予查询虚拟列的权限
GRANT SELECT (employee_id, first_name, last_name, performance_score) ON employees TO 'user1'@'localhost';
  1. 表达式中的安全风险:定义虚拟列的表达式中可能包含函数调用、子查询等操作,如果不加以防范,可能存在安全风险。例如,恶意用户可能通过构造特殊的输入值,利用表达式中的函数漏洞进行 SQL 注入攻击。因此,在编写虚拟列表达式时,要确保对输入数据进行严格的验证和过滤,避免使用用户输入直接作为表达式的一部分。
-- 避免这种可能存在安全风险的表达式
CREATE TABLE vulnerable_table (
    user_input VARCHAR(50),
    risky_result VARCHAR(100) GENERATED ALWAYS AS (
        CONCAT('SELECT * FROM some_table WHERE column = ', user_input)
    ) VIRTUAL
);

应采用参数化查询等安全方式来确保表达式的安全性。

通过充分了解和掌握 MariaDB 虚拟列的定义、使用场景以及注意事项,数据库开发人员和管理员可以更加灵活、高效地设计和管理数据库,提升数据处理能力和应用程序的性能。同时,合理使用虚拟列也有助于保持数据的一致性、减少冗余存储,并提高数据库的可维护性和可扩展性。在实际应用中,需要根据具体的业务需求和数据特点,权衡虚拟列的优势和潜在问题,做出最合适的选择。