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

MySQL Schema设计常见陷阱及避免方法

2024-04-197.0k 阅读

MySQL Schema设计常见陷阱及避免方法

数据类型选择不当

  1. 陷阱描述
    • 在MySQL数据库设计中,数据类型的选择至关重要。选择过小的数据类型可能导致数据溢出,而选择过大的数据类型则会浪费存储空间,影响性能。例如,对于存储用户年龄的字段,如果选择TINYINT类型,其无符号范围是0到255,如果应用场景中用户年龄可能超过255(虽然这种情况在现实中不太常见,但如果涉及到一些特殊统计或模拟场景就可能出现),就会发生数据溢出。另一方面,如果选择BIGINT来存储年龄,就会无端占用8个字节的空间,而实际上TINYINT(1个字节)就足以满足常规需求。
    • 对于日期和时间类型,如果使用错误也会带来问题。比如,在记录事件发生时间时,DATE类型只能精确到日期,如果需要精确到具体时间,使用DATE就不合适,而应该使用DATETIMETIMESTAMP类型。DATETIME能表示的范围更广(从'1000 - 01 - 01 00:00:00'到'9999 - 12 - 31 23:59:59'),TIMESTAMP能表示的范围是'1970 - 01 - 01 00:00:01' UTC到'2038 - 01 - 19 03:14:07' UTC,并且TIMESTAMP会根据服务器时区自动转换,在一些需要考虑时区的应用中如果使用不当也会出错。
  2. 避免方法
    • 深入了解业务需求,根据数据的实际范围来选择合适的数据类型。对于数值类型,要提前预估数据的最大值和最小值。例如,在设计一个存储产品销量的字段时,如果预计销量不会超过20亿,使用INT类型(有符号范围是 - 2147483648到2147483647)就足够了,没必要使用BIGINT
    • 对于日期和时间类型,明确是否需要精确到时间,以及是否需要考虑时区等因素。如果只是记录用户注册日期,DATE类型即可;如果要记录用户登录的具体时间点,且对时区不敏感,DATETIME是不错的选择;如果对时区敏感,并且数据主要集中在近几十年内,TIMESTAMP可能更合适。
  3. 代码示例
-- 错误示例:使用TINYINT存储可能较大的数值
CREATE TABLE wrong_type_example (
    large_number TINYINT
);
-- 插入数据可能导致溢出
INSERT INTO wrong_type_example (large_number) VALUES (256);

-- 正确示例:根据业务需求选择合适的INT类型
CREATE TABLE right_type_example (
    large_number INT
);
-- 插入较大数值
INSERT INTO right_type_example (large_number) VALUES (1000000);

未正确使用索引

  1. 陷阱描述
    • 索引是提高MySQL查询性能的重要手段,但如果使用不当,不仅不能提升性能,反而会降低数据库的写入性能,增加存储开销。一种常见的情况是索引过多。当表上的索引过多时,每次插入、更新或删除操作,MySQL都需要更新所有相关的索引,这会大大增加操作的时间开销。例如,在一个频繁进行数据插入的表上,为每个字段都创建了索引,可能导致插入操作变得极其缓慢。
    • 另一种情况是索引列选择不当。如果在选择性很低的列上创建索引,即该列的重复值很多,那么索引的作用就不大。比如在一个存储用户性别的列上创建索引,因为性别只有两种取值(男/女),索引的选择性很低,查询时MySQL可能不会使用该索引,反而会进行全表扫描。
    • 还有一种情况是没有考虑复合索引的最左前缀原则。复合索引是多个列组合而成的索引,如果查询条件不满足最左前缀原则,索引可能无法生效。例如,创建了一个复合索引(col1, col2, col3),如果查询语句是SELECT * FROM table_name WHERE col2 = 'value',此时该复合索引就不会被使用。
  2. 避免方法
    • 仔细评估业务查询需求,只在经常用于查询条件、排序或连接操作的列上创建索引。避免创建不必要的索引。例如,对于一些用于内部统计但很少在查询条件中出现的字段,不要创建索引。
    • 选择选择性高的列创建索引。可以通过计算COUNT(DISTINCT column_name) / COUNT(*)来评估列的选择性,该值越接近1,选择性越高。对于选择性低的列,除非有特殊需求,否则不建议创建索引。
    • 在使用复合索引时,要确保查询条件遵循最左前缀原则。如果业务中有多种查询条件组合,可能需要创建多个复合索引来满足不同的查询需求,但也要注意避免索引过多。
  3. 代码示例
-- 创建表并插入数据
CREATE TABLE employee (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    gender ENUM('Male', 'Female'),
    department VARCHAR(50)
);
INSERT INTO employee (id, name, gender, department) VALUES 
(1, 'John Doe', 'Male', 'HR'),
(2, 'Jane Smith', 'Female', 'Engineering');

-- 错误示例:在选择性低的列gender上创建索引
CREATE INDEX wrong_index ON employee (gender);
-- 查询语句,索引可能不生效
SELECT * FROM employee WHERE gender = 'Male';

-- 正确示例:在常用查询条件列department上创建索引
CREATE INDEX right_index ON employee (department);
-- 查询语句,索引生效
SELECT * FROM employee WHERE department = 'Engineering';

-- 复合索引示例
CREATE TABLE order_info (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    amount DECIMAL(10, 2)
);
-- 创建复合索引
CREATE INDEX order_index ON order_info (customer_id, order_date);
-- 遵循最左前缀原则的查询,索引生效
SELECT * FROM order_info WHERE customer_id = 1 AND order_date = '2023 - 01 - 01';
-- 不遵循最左前缀原则的查询,索引可能不生效
SELECT * FROM order_info WHERE order_date = '2023 - 01 - 01';

表设计缺乏规范化

  1. 陷阱描述
    • 数据库规范化是减少数据冗余、提高数据一致性的重要方法。如果表设计缺乏规范化,会导致数据冗余严重。例如,在一个存储学生信息和课程信息的数据库中,如果在学生表中重复存储课程名称等信息,当课程名称发生变化时,就需要在多个学生记录中同时更新,否则就会出现数据不一致的情况。
    • 缺乏规范化还可能导致插入、更新和删除异常。以订单表为例,如果在订单表中同时存储客户信息(如客户姓名、地址等),当某个客户的地址发生变化时,需要更新所有包含该客户订单的记录,这不仅繁琐,而且容易出错。如果某个客户没有订单了,直接删除该客户相关订单记录可能会导致客户信息丢失(删除异常)。
  2. 避免方法
    • 遵循数据库规范化的范式。第一范式(1NF)要求每个列都是原子的,即不可再分。例如,不能在一个列中存储多个电话号码,而应该为每个电话号码创建一个单独的列。
    • 第二范式(2NF)要求在满足1NF的基础上,非主键列必须完全依赖于主键。例如,在订单详情表中,如果主键是订单号和产品编号,那么产品的价格等信息就应该完全依赖于这两个主键,而不应该出现部分依赖(如只依赖订单号)。
    • 第三范式(3NF)要求在满足2NF的基础上,非主键列之间不能存在传递依赖。例如,在员工表中,如果员工部门和部门经理存在依赖关系,不应该在员工表中同时存储部门经理信息,而应该通过外键关联到部门表,部门表中存储部门经理信息。
  3. 代码示例
-- 非规范化表设计示例
CREATE TABLE non_normalized_order (
    order_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    customer_address VARCHAR(100),
    product_name VARCHAR(50),
    product_price DECIMAL(10, 2)
);
INSERT INTO non_normalized_order (order_id, customer_name, customer_address, product_name, product_price) VALUES 
(1, 'Alice', '123 Main St', 'Widget A', 10.00),
(2, 'Bob', '456 Elm St', 'Widget B', 15.00);

-- 规范化表设计示例
CREATE TABLE customer (
    customer_id INT PRIMARY KEY,
    customer_name VARCHAR(50),
    customer_address VARCHAR(100)
);
CREATE TABLE product (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(50),
    product_price DECIMAL(10, 2)
);
CREATE TABLE order_table (
    order_id INT PRIMARY KEY,
    customer_id INT,
    product_id INT,
    FOREIGN KEY (customer_id) REFERENCES customer(customer_id),
    FOREIGN KEY (product_id) REFERENCES product(product_id)
);
INSERT INTO customer (customer_id, customer_name, customer_address) VALUES 
(1, 'Alice', '123 Main St'),
(2, 'Bob', '456 Elm St');
INSERT INTO product (product_id, product_name, product_price) VALUES 
(1, 'Widget A', 10.00),
(2, 'Widget B', 15.00);
INSERT INTO order_table (order_id, customer_id, product_id) VALUES 
(1, 1, 1),
(2, 2, 2);

忽视外键约束

  1. 陷阱描述
    • 外键约束用于建立表与表之间的关联关系,确保数据的完整性。如果忽视外键约束,可能会出现孤立数据。例如,在订单表和客户表的关系中,如果没有设置外键约束,当删除客户表中的某个客户记录时,订单表中与该客户相关的订单记录仍然存在,这些订单记录就成为了孤立数据,在业务逻辑上可能会导致错误。
    • 另外,没有外键约束也难以保证数据的一致性。比如在员工表和部门表的关系中,如果员工表中的部门编号可以随意填写,而不与部门表中的实际部门编号匹配,就会出现数据不一致的情况,影响业务查询和统计的准确性。
  2. 避免方法
    • 在设计数据库时,根据业务关系合理设置外键约束。例如,在订单表中,为客户ID字段设置外键,关联到客户表的主键。这样,当删除客户表中的记录时,MySQL可以根据外键约束的设置(如ON DELETE CASCADEON DELETE SET NULL等)进行相应处理,避免孤立数据的产生。
    • 在创建表时就明确外键约束,同时也要注意外键的性能影响。虽然外键约束能保证数据完整性,但在插入、更新和删除操作时会有一定的性能开销,需要在设计时综合考虑。
  3. 代码示例
-- 没有外键约束的表设计
CREATE TABLE department (
    department_id INT PRIMARY KEY,
    department_name VARCHAR(50)
);
CREATE TABLE employee (
    employee_id INT PRIMARY KEY,
    employee_name VARCHAR(50),
    department_id INT
);
-- 可能出现孤立数据,删除department表记录后,employee表对应记录仍存在

-- 有外键约束的表设计
CREATE TABLE department_with_fk (
    department_id INT PRIMARY KEY,
    department_name VARCHAR(50)
);
CREATE TABLE employee_with_fk (
    employee_id INT PRIMARY KEY,
    employee_name VARCHAR(50),
    department_id INT,
    FOREIGN KEY (department_id) REFERENCES department_with_fk(department_id) ON DELETE CASCADE
);
-- 当删除department_with_fk表中的记录时,employee_with_fk表中相关记录也会被删除

没有考虑扩展性

  1. 陷阱描述
    • 在数据库设计初期,如果没有考虑扩展性,当业务增长或需求发生变化时,可能需要对数据库结构进行大规模修改,这不仅成本高,还可能影响业务的正常运行。例如,在一个电商系统中,最初设计产品表时只考虑了普通商品的属性,没有预留扩展字段或采用可扩展的设计方式。随着业务发展,需要增加一些特殊商品的特有属性,如电子产品的保修期限、服装的尺码等,此时可能需要对产品表进行结构修改,添加新的列,这可能会导致与原有的查询、存储过程等代码不兼容。
    • 另外,没有考虑扩展性还体现在数据库架构层面。如果一开始采用了简单的单库单表架构,随着数据量和业务量的增长,可能会面临性能瓶颈,而要将其扩展为分布式数据库架构,难度较大,需要重新设计数据的存储和访问方式。
  2. 避免方法
    • 在表设计时,可以预留一些扩展字段,如extra_info字段,采用JSONTEXT类型来存储一些未来可能需要扩展的信息。这样,当业务需求变化时,可以通过修改应用程序逻辑来解析和使用这些扩展信息,而不需要直接修改表结构。
    • 从数据库架构层面,要根据业务的发展趋势进行合理规划。如果预计业务增长较快,一开始可以采用分库分表的架构设计,或者选择易于扩展的数据库架构,如采用分布式数据库系统。同时,要保持数据库设计的灵活性,避免过于僵化的结构。
  3. 代码示例
-- 可扩展的表设计示例
CREATE TABLE product_extensible (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(50),
    extra_info JSON
);
-- 插入数据,包含扩展信息
INSERT INTO product_extensible (product_id, product_name, extra_info) VALUES 
(1, 'Smartphone', '{"brand": "Apple", "warranty": "1 year"}'),
(2, 'T - Shirt', '{"size": "L", "color": "Blue"}');
-- 查询扩展信息
SELECT product_name, extra_info ->> '$[warranty]' AS warranty FROM product_extensible WHERE product_name = 'Smartphone';

字符集和排序规则选择不当

  1. 陷阱描述
    • 字符集和排序规则决定了MySQL如何存储和比较字符串数据。如果选择不当,可能会导致数据显示异常或查询结果不准确。例如,在一个多语言的应用中,如果选择了不支持某些语言字符的字符集,当插入这些字符时,可能会出现乱码。常见的字符集如latin1主要用于西欧语言,如果要存储中文等亚洲语言,就需要选择utf8utf8mb4字符集。
    • 排序规则也很重要,不同的排序规则会影响字符串的比较结果。例如,utf8_general_ci是不区分大小写的排序规则,而utf8_bin是二进制比较,区分大小写。如果在需要区分大小写的查询场景中使用了不区分大小写的排序规则,可能会得到错误的查询结果。
  2. 避免方法
    • 根据应用程序所涉及的语言范围选择合适的字符集。对于国际化应用,utf8mb4是一个很好的选择,它支持几乎所有的Unicode字符。在创建数据库和表时,明确指定字符集和排序规则。
    • 在设计查询时,要根据排序规则的特性来编写SQL语句。如果需要严格区分大小写的查询,要确保使用的排序规则支持这种区分,或者在查询条件中使用合适的函数(如BINARY关键字)来强制进行区分大小写的比较。
  3. 代码示例
-- 创建数据库时指定字符集和排序规则
CREATE DATABASE my_database CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE my_database;
-- 创建表时指定字符集和排序规则
CREATE TABLE user (
    user_id INT PRIMARY KEY,
    user_name VARCHAR(50)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 插入数据
INSERT INTO user (user_id, user_name) VALUES (1, 'John'), (2, 'john');
-- 使用不区分大小写的排序规则查询
SELECT * FROM user WHERE user_name = 'john';
-- 使用区分大小写的查询,通过BINARY关键字
SELECT * FROM user WHERE BINARY user_name = 'john';

视图设计不合理

  1. 陷阱描述
    • 视图是从一个或多个表中导出的虚拟表,提供了一种方便的数据查询和安全控制方式。然而,如果视图设计不合理,可能会带来性能问题。例如,在视图中进行复杂的连接操作、子查询嵌套过多等,会导致视图查询效率低下。当基于这样的视图进行查询时,MySQL需要花费大量时间来解析和执行视图定义中的复杂逻辑。
    • 另外,如果视图依赖的基础表结构发生变化,而没有及时更新视图,可能会导致视图无法正常使用。比如基础表中删除了某个视图所依赖的列,那么在查询视图时就会出错。
  2. 避免方法
    • 在设计视图时,尽量保持视图逻辑的简洁性。避免在视图中进行过于复杂的计算和嵌套查询。如果确实需要复杂逻辑,可以考虑将其拆分成多个简单的视图,或者在应用程序层面进行处理。
    • 要密切关注视图依赖的基础表结构变化,及时更新视图定义。可以通过定期检查视图的有效性,或者在基础表结构变更时,制定相应的视图更新流程来确保视图的正常使用。
  3. 代码示例
-- 复杂视图示例,包含多个子查询和连接
CREATE VIEW complex_view AS
SELECT a.id, b.name, c.value
FROM table_a a
JOIN (
    SELECT id, name FROM table_b WHERE some_condition
) b ON a.id = b.id
JOIN (
    SELECT id, value FROM table_c WHERE another_condition
) c ON a.id = c.id;
-- 查询复杂视图,性能可能较差
SELECT * FROM complex_view;

-- 优化后的视图设计,拆分成简单视图
CREATE VIEW view_b AS
SELECT id, name FROM table_b WHERE some_condition;
CREATE VIEW view_c AS
SELECT id, value FROM table_c WHERE another_condition;
CREATE VIEW optimized_view AS
SELECT a.id, b.name, c.value
FROM table_a a
JOIN view_b b ON a.id = b.id
JOIN view_c c ON a.id = c.id;
-- 查询优化后的视图,性能可能更好
SELECT * FROM optimized_view;

存储过程和函数设计缺陷

  1. 陷阱描述
    • 存储过程和函数是MySQL中封装业务逻辑的重要工具,但如果设计存在缺陷,可能会影响数据库的性能和可维护性。例如,在存储过程中使用大量的临时变量,可能会占用过多的内存资源。特别是在高并发场景下,过多的临时变量可能导致内存不足的问题。
    • 另外,存储过程和函数的参数设计不合理也会带来问题。如果参数过多,不仅在调用时不方便,而且可能导致代码可读性差。同时,如果没有对参数进行有效的验证,可能会导致非法数据进入存储过程,引发错误或异常。
  2. 避免方法
    • 在设计存储过程和函数时,尽量减少不必要的临时变量使用。可以通过优化算法和逻辑,直接在SQL语句中完成数据处理,避免过多的中间变量。
    • 合理设计参数,参数数量不宜过多。对每个参数进行清晰的定义,并在存储过程或函数内部进行参数验证,确保输入的数据是合法的。同时,要对存储过程和函数进行良好的注释,提高代码的可读性和可维护性。
  3. 代码示例
-- 存储过程使用过多临时变量示例
DELIMITER //
CREATE PROCEDURE bad_proc()
BEGIN
    DECLARE var1 INT;
    DECLARE var2 INT;
    DECLARE var3 INT;
    -- 大量临时变量操作
    SET var1 = (SELECT COUNT(*) FROM table_a);
    SET var2 = (SELECT SUM(value) FROM table_b);
    SET var3 = var1 + var2;
    SELECT var3;
END //
DELIMITER ;

-- 优化后的存储过程,减少临时变量
DELIMITER //
CREATE PROCEDURE good_proc()
BEGIN
    SELECT COUNT(*) + SUM(value)
    FROM table_a
    JOIN table_b ON table_a.id = table_b.id;
END //
DELIMITER ;

未考虑事务处理

  1. 陷阱描述
    • 事务是一组数据库操作,这些操作要么全部成功,要么全部失败,以保证数据的一致性和完整性。如果在数据库操作中未考虑事务处理,可能会出现数据不一致的情况。例如,在一个转账操作中,涉及从一个账户扣除金额并向另一个账户增加金额两个操作。如果没有将这两个操作放在一个事务中,当扣除金额操作成功后,由于某种原因(如系统故障)导致增加金额操作失败,就会出现资金丢失的情况,破坏了数据的一致性。
    • 另外,事务隔离级别设置不当也会带来问题。如果隔离级别设置过低,可能会出现脏读、不可重复读或幻读等现象。例如,在READ - UNCOMMITTED隔离级别下,一个事务可以读取到另一个未提交事务的数据,这可能导致数据的不一致和错误的业务决策。
  2. 避免方法
    • 根据业务需求合理使用事务。对于涉及多个相互关联的数据库操作,要将它们放在一个事务中。在MySQL中,可以使用START TRANSACTIONCOMMITROLLBACK语句来控制事务。
    • 选择合适的事务隔离级别。如果对数据一致性要求非常高,并且不允许出现脏读、不可重复读等现象,可以选择SERIALIZABLE隔离级别,但要注意该级别可能会对并发性能有一定影响。如果业务对并发性能要求较高,且对一些轻微的数据不一致可以接受,可以选择READ - COMMITTEDREPEATABLE - READ隔离级别。
  3. 代码示例
-- 未使用事务的转账操作示例
-- 假设存在账户表account,有id, balance字段
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 假设这里出现错误,导致下一行语句未执行
UPDATE account SET balance = balance + 100 WHERE id = 2;

-- 使用事务的转账操作示例
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- 设置事务隔离级别示例
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- 事务操作
COMMIT;