InnoDB(4.2) 行记录格式

行记录格式

现在的MySQL支持4种行记录格式

  • Redundant
  • Compact
  • Dynamic
  • Compressed

Compact行记录格式

Compact行记录可以高效地存储数据,即一个页种如果存放的行数据越多,其性能就越高。

下图为Compact行记录的存储方式
在这里插入图片描述

变长字段长度列表

Compact行记录格式的首部是一个变长字段长度列表,该列表是非NULL的,并且是按照列的顺序逆序处置的

变长字段就是指字段类型为VARCHAR、VARBINARY,这些可以动态扩展的列。

举个栗子

//创建一个表
create table me(
	a varchar(10),
    b varchar(10),
    c char(10),
    d varchar(10)
)engine=innodb;
//插入数据
insert into select a,bb,ccc,dddd;

那么插入的第一条记录的变长字段列表为[04,02,01],分别对应dddd,bb,a的长度(逆序排可变长度列)

其长度有如下两条规则

  • 若列的长度小于255( 2 8 2^8 28),占用一个字节表示(一个字节刚好8位)。
  • 如果超过255,那就占用两个字节表示,2个字节就是16位,也就是65536,varchar最大长度就是16位,超过了就会变成Text类型。
NULL标志位

该位指示了该行数据中是否有NULL值,有则使用1来表示,该部分占的字节大小大概为1字节,比如当NULL标志位为06时,06转换为二进制为110,表示第二列和第三列为NULL(如果超过了8列会怎么办?)。

记录头信息

记录头信息固定占5字节(40位)

详细如下

名称大小(位)描述
()1未知
()1未知
deleted_flag1该行是否被删除
min_rec_flag1如果该记录是预先被定义为最小的记录,值为1
n_owned4该记录拥有的记录数
heap_no13索引堆中该条记录的排序记录
record_type3记录该行记录的类型,000表示普通,001表示B+树的结点指针。。。
next_record16页中下一条行记录的相对未知
Total40

记录头信息(Record Header)的最后两个字节就是next_record,next_record代表下一个记录的偏移量,即当前记录的位置加上next_record的偏移量就是下一条行记录的起时位置,所以在页的内部,实则上也是通过一种链表的形式来串连各条行记录的

存储列的部分

最后的部分就是实际存储每个列的数据,这里需要注意的是,NULL不占该部分的任何空间,NULL除了占有NULL标志位的1字节空间外,实际储存是不占有任何空间的。

另外一点要注意的是,每行数据除了拥有用户定义的列外,其实还会有两个隐藏列,为事务ID列和回滚指针列,分别为6字节和7字节的大小,同时,若InnoDB表没有设置主键,需要使用到rowid,还会有一个6字节的rowid列

实战看一下里面Compact记录

这是b表的数据

其中a 、c、e都是varchar型,而d、f是Int型
在这里插入图片描述
现在来查看一下b.ib里面的情况

hexdump -C bibd //linux命令

在这里插入图片描述
红色框住的一部分就是表中第一行的数据

可以看到04 03 01对应的就是eeee,ccc,a的长度(也是反序)

然后04就是NULL标志位,04转换成二进制就是0010,即第一第二列不为空,第三列为空,第四列不为空,正好也是对应行的数据

Redundant行记录

这种行记录是为了兼容5.0版本之前而存在的,采用了以下格式进行储存
在这里插入图片描述
Redundant行记录与Compact行记录最大的区别就是少了字段长度偏移列表,其他都是比较类似的。

字段长度偏移列表

Redundant行记录第一个部分是字段长度偏移列表,但与Compact行记录不一样(Compact是变长字段长度偏移列表),同样是按照列的顺序进行逆序放置的,同样拥有下面两条规则,

  • 若列的长度小于255(字符),占用1个字节表示
  • 若大于255,占用2个字节表示
  • 与Compact不同,Redundant存储的是所有列的长度,包含NULL列,NULL列如果是可变长度类型的话,记录为0,如果不是可变长度会有长度。
名称大小描述
()1未知
()1未知
deleted_flag1该行是否以被删除
min_rec_flag1该记录是预先被定义为最小的记录时,该字段为1
n_owned4该记录拥有的记录数
heap_no13索引堆中该条记录的索引号
n_fields10该行记录中列的数量(与Compact不同)
1byte_offs_flag1偏移列表为1字节还是2字节(与Compact不同)
next_record16页中下一条行记录的相对位置
Total48
存储列的部分

存储列的部分与Compact格式基本一样,同样隐藏了2个列(事务ID列和回滚指针列),除了对NULL值的处理。

对NULL值的处理

Redundant对NULL值的处理可能会占用空间的,在前面的字段长度偏移字段里面是有记录NULL值长度大小的,如果为可变类型才为0,且该NULL值在存储列部分不占空间,但有些比如char类型,为NULL的话,在字段长度偏移字段是有长度大小的(不为0,具体根据建表时定义的长度计算),对于该NULL值,在存储列部分是占空间的。

行溢出数据

InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外,一般是针对于BLOB、TEXT这些大类型,行溢出是指字节溢出,而不是字符长度溢出,不过其实BLOB并不一定将数据放出行溢出,而且VARCHAR类型即使没有溢出也依然有可能被存放在行溢出数据

MySQL的VARCHAR类型可以存放 2 16 2^{16} 216字节,即65535字节,如果超过这个字节或者等于这个字节会变成TEXT类型(要改变SQL_MODE),比如下面的建表语句,但其实使用65532会报错的,因为字段本身还会有其他开销,所以只可以存65532字节

//注意这里一定要使用latin1的字符集,因为在这个字符集内,一个字符的最大占用字节为1
//所以varchar(65535)才会对应字节长度为65535,
//如果使用utf8的话,一个字符的最大占用字节为3,总最大的字节长度就是65535*3
CREATE TABLE i(
	a VARCHAR(65535)
)ENGINE=INNODB,CHARSET=latin1;

此外要注意的是,这个65535字节,并不是指单个varchar,而是指整个表非大字段类型的字段的bytes总和

但是问题来了?InnoDB存储引擎的页为16KB,也就是16384字节,是怎么存储65535字节的呢?

其实,在一般情况下,InnoDB存储引擎的数据都是存放在页的B-tree node中的(也就是索引的叶结点),但是如果发生行溢出的时候,就会存放在页类型为Uncompress BLOB页(未压缩的二进制页),而叶子结点里面对于该列只会存储前768字节数据,后面是偏移量,该偏移量是指向行溢出页的,其余的都在Uncompress BLOB页中

总体来说的结构会如下所示
在这里插入图片描述
下面就来详细说一下什么时候会发生行溢出

首先,我们要认识InnoDB存储引擎表是索引组织,即是一棵B+树,而B+树最低标准是基于2-3结点创造的,即每个结点都必须要有至少2个数据(对于InnoDB来说,子结点就是页,里面的数据就是行,所以每页里面都至少有2行)否则不可能形成树结构,而会形成链表,所以,InnoDB发生行溢出数据的条件,就是叶子结点无法存储至少2行数据
在这里插入图片描述
即并不是只要varchar或者text等类型,超过了768字节就会发生行溢出,只是如果发生了行溢出,那么varchar和text等类型,只会保留768字节,剩余的部分会放到BLOB PAGE里面。而发生行一处的条件就是一个页里面,存放不下两条行记录(书上说的是每一行的列的总字节长度为阈值为8098字节,即varchar(8098)【跟16KB的一半差不多】,即超过了这个字节,那么该页存储了这行就无法存储第二行,也就要发生行溢出)。所以,即使是BLOB和TEXT类型,也是要判断该页是否可以存放两行记录

那可不可能是其他类型的列发生行溢出呢?比如Int、BIGINT

这是不可能的,因为InnoDB对单表有列数的限定,最多只有1024个列,而类型里面占空间最大的为8位,即使1024个列都是最大位数,都不会超过(所以发生行数据溢出的,肯定是有变长类型或者TEXT和BLOB类型,虽然这里1024*8是大于8098的,是因为8098没有加上偏移量【指向溢出页的】)。

Compressed和Dynamic行记录模式

从MySQL5.7开始,默认的行记录格式为Dynamic格式,Compressed和Dynamic行记录格式称为Barracuda,而以前支持的Compact和Redundant称为Antelope文件格式。

这两种记录格式,如果发生了行溢出情况,对于存放在VARCHAR、VARBINARY、BLOB、TEXT中的数据采用了完全的行溢出的方式(即该行记录中最长的一列),在该行记录的存储列的部分,对于BLOB类型的,只会存放20个字节的指针,实际的数据都会在溢出页中,这里的溢出页称为Off Page,而之前的Compact和Redundant这两种格式会存放768个前缀字节。

唯一的不同是Compressed的行记录格式的另一个功能就是,存储在溢出页其中的行数据会进行压缩,使用zlib算法,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。不过这个是牺牲CPU来获取存储空间的,因为执行算法需要使用CPU
在这里插入图片描述

Dynamic、Compressed记录行的格式

Dynamic记录行的格式其实是跟Compact是一致的,除了使用了完全行溢出外,其他属性基本没变。

CREATE DATABASE hehe;
USE hehe;
CREATE TABLE a(
	a VARCHAR(4),
	b VARCHAR(5),
	c INT,
	d VARCHAR(6),
	e INT
)ENGINE=INNODB,CHARSET=utf8;
INSERT INTO a SELECT "aaa","bb",NULL,"c",4;

这是表的状态
在这里插入图片描述
同样,查看它的输出

hexdump -C a.ibd

在这里插入图片描述
可以看到01 02 03对应的是c、bb、aaa,然后04同样是NULL标志区,转换来就是0010,对应第三列为NULL。

所以可以推测Dynamic和Compressed的记录行的格式跟Compact是一样的,只不过在列部分内对于溢出行的处理方式不一样

CHAR的行结构存储

我们一般会认为VARCHAR是可变长的,而CHAR是不可变长的。

但实际上是,CHAR在InnoDB中也是被认为是变长字符类型,比如CHAR(10),10是指字符长度,然后utf8里面一个字符最大3个字节,所以CHAR(10)代表最大可以储存30字节的字符,但如果不够长度,后面依然是会进行自动填充的。

这也就意味着在Compact行记录里面的变长字段长度列表是可以看到CHAR类型列的

<think>我们根据用户提供的实体、属性和关系来设计图书管理系统的E-R图(概念模型)和MySQL数据库结构。注意:E-R图是概念设计,而MySQL是物理实现,所以我们需要先设计E-R图,然后将其转换为MySQL的表结构。###1.E-R图(实体-关系图)设计####实体及其属性:-**读者(Reader)**:-借书证号(主键,PK)-姓名-性别-系别-班级-信誉分-加入日期-状态(如:正常、冻结等)-**书籍(Book)**:-书号(主键,PK)-书名-作者-出版社-入库时间-价格-是否借出(可用布尔类型,0表示未借出,1表示已借出)-位置(存放位置,如书架编号)-分类(图书分类号或分类名)-**借阅记录(BorrowRecord)**:-记录ID(主键,PK)-借书证号(外键,FK,引用读者的借书证号)-书号(外键,FK,引用书籍的书号)-借阅日期-应还日期(根据借阅日期和借阅期限计算得出)-归还日期(实际归还日期,可为空,表示未归还)-是否逾期(可通过应还日期和归还日期计算,或存储时更新)-罚款金额(如果有逾期,则根据逾期天数计算罚款)####关系:-读者和借阅记录之间是1对多关系(一个读者可以有多条借阅记录)-书籍和借阅记录之间是1对多关系(一本书可以被借阅多次,但在同一时间只能被一位读者借阅,因此在借阅记录中,同一本书在未归还前不会再次出现)-因此,读者和书籍之间通过借阅记录形成多对多关系(一个读者借多本书,一本书被多个读者借过)。####E-R图描述:-三个实体框:Reader,Book,BorrowRecord-关系:-Reader与BorrowRecord之间有一条连线,标注为“借阅”,在Reader端标记“1”,在BorrowRecord端标记“N”(表示一个读者可以有多条借阅记录)。-Book与BorrowRecord之间有一条连线,标注为“被借阅”,在Book端标记“1”,在BorrowRecord端标记“N”(表示一本书可以有多条借阅记录,但同一时间只能有一条未归还的记录)。-属性分别写在对应实体框内。###2.MySQL数据库结构设计(物理模型)根据E-R图,我们设计以下表结构。注意:我们将实体转换为表,属性转换为列,关系通过外键实现。####读者表(Reader)|列名|数据类型|约束|说明||--------------|--------------|--------------|------------||reader_id|VARCHAR(20)|PRIMARYKEY|借书证号||name|VARCHAR(50)|NOTNULL|姓名||gender|ENUM('男','女')|NOTNULL|性别||department|VARCHAR(50)||系别||class_name|VARCHAR(50)||班级||credit_score|INT|DEFAULT100|信誉分(默认100)||join_date|DATE|NOTNULL|加入日期||status|ENUM('正常','冻结')|DEFAULT'正常'|状态|####书籍表(Book)|列名|数据类型|约束|说明||---------------|---------------|-------------|----------------||book_id|VARCHAR(20)|PRIMARYKEY|书号||title|VARCHAR(100)|NOTNULL|书名||author|VARCHAR(50)|NOTNULL|作者||publisher|VARCHAR(50)||出版社||storage_date|DATE|NOTNULL|入库时间||price|DECIMAL(10,2)||价格||is_borrowed|BOOLEAN|DEFAULT0|是否借出(0:未借出,1:已借出)||location|VARCHAR(50)||位置(书架号等)||category|VARCHAR(20)||分类|####借阅记录表(BorrowRecord)|列名|数据类型|约束|说明||---------------|--------------|------------------------------------|--------------------------------------------------------------||record_id|INT|PRIMARYKEY,AUTO_INCREMENT|记录ID(自增主键)||reader_id|VARCHAR(20)|FOREIGNKEYREFERENCESReader(reader_id)|借书证号(外键)||book_id|VARCHAR(20)|FOREIGNKEYREFERENCESBook(book_id)|书号(外键)||borrow_date|DATE|NOTNULL|借阅日期(记录借书当天的日期)||due_date|DATE|NOTNULL|应还日期(由借阅日期加上借阅期限得到,比如30天)||return_date|DATE||归还日期(实际归还日期,允许NULL,表示未还)||is_overdue|BOOLEAN|DEFAULT0|是否逾期(0:未逾期,1:逾期,可通过触发器或程序更新)||fine_amount|DECIMAL(10,2)|DEFAULT0.00|罚款金额(如果有逾期,按每天多少元计算)|###3.关于关系在MySQL中的实现-**一对多关系**:通过在“多”的一方(借阅记录表)中添加外键指向“一”的一方(读者表和书籍表)的主键来实现。-**多对多关系**:读者和书籍之间的多对多关系通过借阅记录表来实现,因为借阅记录表同时包含读者和书籍的外键。###4.额外考虑:约束和索引-外键约束:在借阅记录表中,`reader_id`和`book_id`分别设置外键约束,引用读者表和书籍表的主键,并设置级联更新和删除策略(根据业务需求,通常设置为RESTRICT,防止误删读者或书籍时导致借阅记录无效)。-索引:除了主键外,可以在经常查询的列上创建索引,例如:-读者表的`reader_id`(主键索引已存在),可以给`name`、`department`等添加普通索引。-书籍表的`title`、`author`等常用查询列添加索引。-借阅记录表的`reader_id`、`book_id`(外键已经自动创建索引),还可以在`borrow_date`、`due_date`等列上创建索引以加快查询速度。###5.示例建表SQL语句```sql--创建读者表CREATETABLEReader(reader_idVARCHAR(20)PRIMARYKEY,nameVARCHAR(50)NOTNULL,genderENUM('男','女')NOTNULL,departmentVARCHAR(50),class_nameVARCHAR(50),credit_scoreINTDEFAULT100,join_dateDATENOTNULL,statusENUM('正常','冻结')DEFAULT'正常');--创建书籍表CREATETABLEBook(book_idVARCHAR(20)PRIMARYKEY,titleVARCHAR(100)NOTNULL,authorVARCHAR(50)NOTNULL,publisherVARCHAR(50),storage_dateDATENOTNULL,priceDECIMAL(10,2),is_borrowedBOOLEANDEFAULT0,locationVARCHAR(50),categoryVARCHAR(20));--创建借阅记录表CREATETABLEBorrowRecord(record_idINTAUTO_INCREMENTPRIMARYKEY,reader_idVARCHAR(20)NOTNULL,book_idVARCHAR(20)NOTNULL,borrow_dateDATENOTNULL,due_dateDATENOTNULL,return_dateDATE,is_overdueBOOLEANDEFAULT0,fine_amountDECIMAL(10,2)DEFAULT0.00,FOREIGNKEY(reader_id)REFERENCESReader(reader_id)ONUPDATECASCADEONDELETERESTRICT,FOREIGNKEY(book_id)REFERENCESBook(book_id)ONUPDATECASCADEONDELETERESTRICT);--为借阅记录表的借书证号、书号、借阅日期等创建索引(外键会自动创建索引,所以这里只添加额外的索引)CREATEINDEXidx_borrow_dateONBorrowRecord(borrow_date);CREATEINDEXidx_due_dateONBorrowRecord(due_date);```###6.注意事项-**自动计算字段**:在借阅记录中,应还日期(due_date)可以在应用层计算(如借阅日期+30天),也可以使用MySQL的生成列(GENERATEDCOLUMN)实现,但考虑到借阅期限可能变化,建议应用层计算。-**是否逾期和罚款金额**:这两个字段可以在还书时通过触发器或应用程序计算更新。例如,还书时,比较应还日期(due_date)和归还日期(return_date),如果归还日期大于应还日期,则计算逾期天数,然后乘以每日罚款费率,更新罚款金额,并将is_overdue设为1。-**信誉分更新**:当发生逾期时,可以在还书时自动扣减读者的信誉分,并当信誉分低于某个阈时自动冻结账户(更新读者表的状态)。###总结通过以上设计,我们完成了图书管理系统的概念模型(E-R图)和物理模型(MySQL表结构)。这个设计能够满足基本的图书借阅管理需求,并可通过程序扩展功能。§§相关问题§§1.如何实现借书和还书的业务逻辑(包括更新书籍状态、计算逾期罚款等)?2.如何设计触发器来自动更新借阅记录中的逾期状态和罚款金额?3.如何优化这个数据库设计以支持大量图书和读者的高效查询?4.如果增加管理员实体和操作日志,该如何扩展E-R图?5.如何设计数据库事务来处理借书和还书的并发操作?</think> 以下是针对图书管理系统的概念模型设计(E-R图)和MySQL数据库结构实现方案,严格基于您提供的实体关系和属性列表: ### 一、概念模型设计(E-R图) ```mermaid erDiagram Reader ||--o{ BorrowRecord : "借阅" Book ||--o{ BorrowRecord : "被借阅" Reader { string reader_id PK "借书证号" string name "姓名" enum gender "性别" string department "系别" string class "班级" int credit_score "信誉分" date join_date "加入日期" enum status "状态" } Book { string book_id PK "书号" string title "书名" string author "作者" string publisher "出版社" datetime store_date "入库时间" decimal price "价格" bool is_borrowed "是否借出" string location "位置" string category "分类" } BorrowRecord { int record_id PK "记录ID" string reader_id FK "借书证号" string book_id FK "书号" date borrow_date "借阅日期" date due_date "应还日期" date return_date "归还日期" bool is_overdue "是否逾期" decimal fine_amount "罚款金额" } ``` #### 关键关系说明: 1. **读者 → 借阅记录**(1:N关系) - 每位读者($R_i$)可对应多条借阅记录($BR_j$) - 外键关联:`BorrowRecord.reader_id` → `Reader.reader_id` 2. **书籍 → 借阅记录**(1:M关系) - 每本书($B_k$)可被多次借阅(不同时间段) - 外键关联:`BorrowRecord.book_id` → `Book.book_id` 3. **读者 ↔ 书籍**(M:N间接关系) - 通过借阅记录实现多对多关系 - 约束公式:$\forall B_k \in Book, \sum_{\text{未归还}} is\_borrowed = 1$(单本书同一时间只能被一人借阅) ### 二、MySQL数据库结构实现 #### 1. 读者表(Reader) ```sql CREATE TABLE Reader ( reader_id VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, gender ENUM('男','女') NOT NULL, department VARCHAR(30) NOT NULL, class VARCHAR(30) NOT NULL, credit_score INT DEFAULT 100 CHECK (credit_score BETWEEN 0 AND 100), join_date DATE NOT NULL, status ENUM('正常','冻结') DEFAULT '正常' ) ENGINE=InnoDB; ``` #### 2. 书籍表(Book) ```sql CREATE TABLE Book ( book_id VARCHAR(20) PRIMARY KEY, title VARCHAR(100) NOT NULL, author VARCHAR(50) NOT NULL, publisher VARCHAR(50), store_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, price DECIMAL(10,2) CHECK (price >= 0), is_borrowed BOOLEAN DEFAULT 0, location VARCHAR(50) NOT NULL, category VARCHAR(30) NOT NULL ) ENGINE=InnoDB; ``` #### 3. 借阅记录表(BorrowRecord) ```sql CREATE TABLE BorrowRecord ( record_id INT AUTO_INCREMENT PRIMARY KEY, reader_id VARCHAR(20) NOT NULL, book_id VARCHAR(20) NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE, is_overdue BOOLEAN GENERATED ALWAYS AS ( CASE WHEN return_date IS NULL AND CURDATE() > due_date THEN 1 WHEN return_date > due_date THEN 1 ELSE 0 END ) VIRTUAL, fine_amount DECIMAL(10,2) DEFAULT 0.00, FOREIGN KEY (reader_id) REFERENCES Reader(reader_id) ON UPDATE CASCADE, FOREIGN KEY (book_id) REFERENCES Book(book_id) ON UPDATE CASCADE, CONSTRAINT chk_dates CHECK (borrow_date <= due_date) ) ENGINE=InnoDB; ``` ### 三、关键业务规则实现 1. **自动逾期计算** 使用生成列自动计算逾期状态: ```sql is_overdue BOOLEAN GENERATED ALWAYS AS (...) ``` 2. **数据完整性约束** - 借阅日期 ≤ 应还日期:`CHECK (borrow_date <= due_date)` - 信誉分区间约束:`CHECK (credit_score BETWEEN 0 AND 100)` - 非负金额约束:`CHECK (price >= 0)` 3. **动态状态联动** 当书籍被借出时,需更新图书状态: ```sql UPDATE Book SET is_borrowed = 1 WHERE book_id = [借阅书籍ID]; ``` ### 四、功能扩展建议 1. **视图设计** ```sql -- 当前逾期记录视图 CREATE VIEW OverdueRecords AS SELECT r.reader_id, r.name, b.title, br.borrow_date, br.due_date FROM BorrowRecord br JOIN Reader r ON br.reader_id = r.reader_id JOIN Book b ON br.book_id = b.book_id WHERE br.is_overdue = 1 AND br.return_date IS NULL; ``` 2. **索引优化** ```sql CREATE INDEX idx_book_location ON Book(location); CREATE INDEX idx_borrow_dates ON BorrowRecord(borrow_date, due_date); ``` ### 五、E-R图转换MySQL规则 | E-R元素 | MySQL实现 | 示例 | |---------------------|------------------------------|-------------------------------| | 实体(Entity) | 表(Table) | `Reader`表 | | 属性(Attribute) | 列(Column) | `reader_id VARCHAR(20) PK` | | 1:N关系 | 外键(Foreign Key) | `BorrowRecord.reader_id → Reader` | | M:N关系 | 关联表(Junction Table) | 借阅记录表作为关联表 | | 主键(PK) | PRIMARY KEY约束 | `PRIMARY KEY (reader_id)` | 此设计完全满足图书管理系统的基本需求,支持借还书管理、逾期计算、信誉分机制等功能,并通过关系数据库约束保障数据完整性[^1][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值