深刻理解Mysql數據存儲

歡迎關注個人微信公衆號【 Mflyyou】獲取持續更新。

github.com/zhangpanqin/MFlyYou 收集技術文章及個人系列文章,歡迎 Star。html

前言

本文內容

  • Mysql 數據文件說明
  • Mysql 數據邏輯存儲架構
  • Mysql 表空間,主要是系統表空間和獨立表空間
  • Mysql 數據類型java

    • 時區對 datetime 和 timestamp 影響,java 中 LocalDatetime 保存時,時間和預期不符的緣由分析和解決辦法
    • varchar(n) 和 char(n) 保存時,n 能取多少,n 的含義。一行數據中 varchar 能存多少個
    • 整型、小數

本文內容基於 Mysql 8.0.21 ,系統爲 Centos 7。node

Mysql 架構說明

https://dev.mysql.com/doc/refman/8.0/en/images/mysql-architecture.png

客戶端連接 MysqlServer 層,Server 層會對 sql 進行語法解析和優化並生成執行計劃,而後調用存儲引擎提供的接口獲取要查詢的數據。mysql

存儲引擎層從計算機文件系統上讀取對應的文件中的數據返回給 Server 層,Server 層再將數據返回給客戶端。git

存儲引擎不瞭解的話,使用 InnoDB 就行,這也是比較經常使用的。github

Redis 爲何會比 Mysql 快,很大的緣由是 Redis 的數據都在內存中,再加上比較好的數據結構,查詢的速度固然不是一個量級的。但同時 Redis 不會存儲那麼多的數據量,幾個 T 的內存仍是挺貴的。sql

Mysql 將數據儲存在硬盤上,爲了提升查詢速度,比較好的作法是將索引數據和一部分熱數據(常常訪問的數據)放到內存中(Mysql 的 Buffer Poll)。數據庫

當檢索數據的時候,Mysql 經過索引查找,就能夠知道數據在磁盤哪塊了,從硬盤對應位置讀取對應的數據到內存中返回給客戶端。bash

若是查詢的時候沒有走索引就須要掃描整個表數據文件,由於內存比硬盤小,會不停的從硬盤讀取表中的一部分數據到內存,而後在內存中篩選出符合要求的數據,再去硬盤讀取一部分數據作篩選直到整個表數據讀取一遍。若是你有 20 g 數據,你想一下須要讀取多長時間。服務器

Mysql 8.0 相對 Mysql 7.0 性能上有很大提高,條件容許建議使用 Mysql 8.0。

Mysql 數據存儲

鏈接 Mysql

# -h 指定 mysqld 的服務地址
# -P 指定鏈接端口
# -u 執行用戶(生產環境不建議使用 root 用戶鏈接,合理使用權限管理。每一個庫使用不一樣的帳號密碼)
# -p 輸入密碼
mysql -hlocalhost -P3306 -uroot -p

查看 Mysql 數據文件的目錄

-- 鏈接以後輸入如下命令,查看數據儲存在哪裏了
-- /var/lib/mysql/  Centos 7.0 存儲位置
show variables like '%datadir%';

系統表空間

系統表空間是全部表共享的,它保存了數據表結構,事務信息 等

SHOW VARIABLES LIKE 'innodb_data_file_path%'
-- ibdata1:12M:autoextend

獨立表空間

獨立表空間是指每張表單獨用一個文件儲存每張表對應的數據和索引,文件擴展名爲 ibd。

每一個數據庫會有一個對應目錄用於保存當前數據庫中的數據文件

在 /var/lib/mysql/ 目錄下
drwxr-x---    8 zhangpanqin  admin   256B  2 11  2020 leetcode
drwxr-x---    8 zhangpanqin  admin   256B 10 18  2019 mysql
-rw-r-----    1 zhangpanqin  admin    36M 11 29 18:31 mysql.ibd

當咱們在某個數據庫中建立一張表時,除了在系統表空間生成元數據和表結構,也會在對應的數據庫目錄下,新建一個 tablename.ibd 文件。

/usr/local/var/mysql/leetcode

-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 department.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 employee.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 logs.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 person.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 14  2020 scores.ibd
-rw-r-----  1 zhangpanqin  admin   112K  2 11  2020 weather.ibd

Mysql 8.0 是默認開啓獨立表空間的,默認每張表使用一個文件進行保存數據和索引

-- 查看是否開啓獨立表空間配置
mysql> show variables like '%innodb_file_per_table%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| innodb_file_per_table | ON    |
+-----------------------+-------+

InnoDB 邏輯儲存結構

image-20200921113000653

表空間

InnoDB 存儲引擎下,表相關的全部數據(好比業務數據和索引數據)都儲存在表空間(tablespace)中。每張表都有一個本身的文件(.ibd)去儲存相關數據(開啓獨立表空間設置)。

表空間又能夠細分爲 segmentextentpagerow

表空間又包含多個段(segment),常見的數據段有:

  • Leaf node segment 數據段,存儲當前表中的數據
  • Non-Leaf node segment 索引段,存儲當前表中的索引

段包含不少個區,每一個區始終爲 1MB 。區由多個連續連續的頁組成,頁的大小一般是 16KB,因此一個區能夠有 64 (1024/16=64)個連續頁。

頁是 InnoDB 與磁盤交互的最小單位。從磁盤上讀取數據,一次性是讀取一頁數據。將內存中的數據落盤到硬盤上,也是操做一頁數據。

好比咱們修改了 id=3 某行數據,數據持久化的時候,是須要將這行所在的頁所有落盤在硬盤上。

update test_table set a=2 where id =3;

頁也有類型,數據頁,索引頁等等。

-- 查看頁的大小,默認是 16KB =16*1024bit
mysql> show variables like '%innodb_page_size%';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+

每頁存放一行一行的數據。

mysql> SHOW TABLE STATUS LIKE "test_data_type"\G;
*************************** 1. row ***************************
           Name: test_data_type
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 22
 Avg_row_length: 14894
    Data_length: 327680
Max_data_length: 0
   Index_length: 0
      Data_free: 0
 Auto_increment: 243
    Create_time: 2020-09-21 01:55:51
    Update_time: 2020-09-21 02:06:59
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options:
        Comment:

Row_format 定義了一行數據在數據頁中怎麼保存。

image-20200921120326750

VARCHAR(M)TEXT 類型的字段爲變長字段,變長字段佔用多少字節,記錄在 變長字段長度列表

記錄頭信息中,記錄着當前行的類型和下一條記錄位置等信息。

行數據中,除了咱們表結構中本身定義的字段,還有 Mysql 添加的元數據字段。好比 行 id(當沒有主鍵數據的時候添加),事務 id (主要用於 MVCC)和 回滾指針等。

一頁能夠存 16KB 數據,可是 VARCAHR(m),能夠存 65535 字節。這些變長數據大於一頁須要怎麼存呢。這個現象也叫作行溢出。

行溢出的數據會單獨存在一頁中,在真實數據中對應的列中記錄一個指針指向溢出的數據。

<font color=red>以上內容瞭解便可,只是爲了理解原理及輔助表設計。</font>

數據類型

<font color=red> 表設計的時候必定要選取合適的數據類型,能用數字就不要用字符串,一是減小存儲時空間的浪費,二是減小查詢時內存的浪費。</font>

整型

類型 描述 佔用字節 範圍
tinyint 對應 java 中 byte 1 字節 有符號-128 至 127。
無符號 0 至 255
smallint 對應 java 中 short 2 字節 有符號 -32768 至 32767。
無符號 0 至 65535
int 對應 java 中 int 4 字節 有符號 -2147483648 至 2147483647。
無符號 0 至 4294967295
bigint 對應 java 中 long 8 字節 有符號 -9223372036854775808 至 9223372036854775807
無符號 0 至 18446744073709551615

小數

類型 描述 佔用字節
FLOAT(M, D) 對應 java 中 float 4 字節
DOUBLE(M, D) 對應 java 中 double 8 字節
DECIMAL(M, D) 對應 java 中 BigDecimal。定點數,能夠精確保存小數 M 和 D 決定

M 表示小數的有效數字,D 表示小數點後的有效數字。

FLOAT(4, 1) 不能存 4000.1 會報錯誤。

日期和時間

類型 描述 佔用字節 取值範圍
YEAR 年份 1 字節 1901~2155
DATE 日期,年月日 3 字節 1000-01-01~ 9999-12-31
TIME(fsp) 時間,時分秒 3 字節 -838:59:59.000000 ~ 838:59:59.000000
DATETIME(fsp) 日期+時間 5 字節 1000-01-01 00:00:00.000000 ~ 9999-12-31 23:59:59.999999
TIMESTAMP(fsp) 底層存儲的是 UTC 時間戳,
顯示值會隨mysql 數據庫所在時區變化
4 字節 1970-01-01 00:00:01.000000 ~ 2038-01-19 03:14:07.999999

TIMESTAMP(fsp) 中的 fsp 是指秒的精度(x.xxx xxx),fsp取值 0,1,2,3,4,5,6。

TIMEDATETIMETIMESTAMP 這幾種類型支持小數秒。

DATETIME(0) 精確到秒,沒有小數位。

DATETIME(3) 精確到豪秒,有三位小數。

<font color=red>日期和時間存儲時區的設置有關,必定要搞清楚原理。 </font>

TIMESTAMP 的顯示和數據庫系統設置的時區有關。TIMESTAMP 底層實際存儲的是毫秒值,顯示的時間是根據設置的時區(time_zone)轉換爲時間顯示的。

還有 Java 1.8 新增的 LocalDateTime 須要怎麼轉換 Mysql 中的時間呢。

-- 查看 mysql 的時區設置
SHOW VARIABLES LIKE "%time_zone%";
mysql> SHOW VARIABLES LIKE "%time_zone%";
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | CET    |
| time_zone        | +08:00 |
+------------------+--------+

system_time_zone Mysql 啓動的時候獲取計算機系統所在的時區。只要計算機的時間準確就沒有問題。

time_zone 設置的是鏈接 mysql 的會話中,時間 (java.util.Date)轉換爲字符串時的 TimeZone。time_zone 這個值能夠被 jdbc 鏈接中的 serverTimezone=Asia/Shanghai 覆蓋。

由於咱們是在東八區,但願時間都轉換爲東八區時間。

[mysqld]
# 將時間轉換爲東八區的時間
default-time-zone = '+08:00'
CREATE TABLE `test_data_type` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `test_data_time` datetime DEFAULT NULL,
  `test_timestamp` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=243 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

-- 實際保存數據的時候須要將日期轉換爲字符串,拼接成這樣的 sql
INSERT INTO test_data_type (test_data_time,test_timestamp) VALUES ('2020-12-12 12:12:12','2020-12-12 12:12:12');

好比咱們將 java 中的 LocalDateTime 存爲 datetime 類型。

@Data
@TableName(value = "test_data_type")
public class TestDataType {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @TableField(value = "test_data_time")
    private LocalDateTime testDataTime;

    @TableField(value = "test_timestamp")
    private LocalDateTime testTimestamp;
}

當咱們保存數據的時候,須要根據配置的 time_zone,將 LocalDateTime 轉爲 String,在替換到 sql 中的 ?

INSERT INTO test_data_type (test_data_time,test_timestamp) VALUES (?,?);
// NativeProtocol.configureTimezone 能夠看到這個邏輯
@Test
public void run33() {
    // 首先獲取到在服務器設置的 time_zone,若是沒有設置的話,默認取 mysql 服務器所在時區
    String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");
    // jdbc 連接中設置的參數
    String canonicalTimezone = getStringProperty("serverTimezone");
    if(canonicalTimezone==null||canonicalTimezone.length()<0){
        canonicalTimezone=configuredTimeZoneOnServer;
    }
    // jdbc url 參數中配置的 serverTimezone 和 time_zone 都是爲了獲取 TimeZone,serverTimezone 的優先級更高
     final TimeZone timeZone = TimeZone.getTimeZone(canonicalTimezone);
     // Timestamp 繼承了 java.util.Date
     // 這裏會將得到 LocalDateTime 獲取其年月日時分秒上的值
    final Timestamp timestamp = Timestamp.valueOf(LocalDateTime.now());
    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
    simpleDateFormat.applyPattern("yyyy.MM.dd HH:mm:ss");
    simpleDateFormat.setTimeZone(timeZone);
    
    // 而後將這個 time 替換 ?
       String time= simpleDateFormat.format(timestamp);
}

時區設置結論

<font color=red>當咱們保存數據的建立時間的時候,只需獲取當前時間就行。當前時區與東八區的時差,mysql 配置的 time_zoneserverTimezone 轉換成字符串時會自動加上。</font>

final Date createTime = new Date();
// LocalDateTime 必定不要本身補時差,否則時間會對不上
final LocalDateTime createTime2 = LocalDateTime.now();
// 下面這個用法是錯誤的。這樣數據庫中保存的時間比實際時間多了八個小時
final LocalDateTime errorCreateTime =  LocalDateTime.now(ZoneId.of("UTC+8"));

字符串

varchar

varchar (M) 中 M 指的是字符數。<font color=red>Mysql 限制在一行數據中,全部 varchar 列的總字節數不能超過 65535 字節。 </font>

-- utf8mb4 實際會佔用 1-3 字節
CREATE TABLE `test_varchar`  (
  `test_name` varchar(65535) CHARACTER SET utf8mb4  NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4;

執行上述 sql 報錯爲:

1074 - Column length too big for column 'test_name' (max = 16383); use BLOB or TEXT instead, Time: 0.000000s

驗證全部 varchar 列總數據不能超過 65535

CREATE TABLE `test_varchar`  (
  `test_name1` varchar(7000) CHARACTER SET utf8mb4  NOT NULL,
    `test_name2` varchar(7000) CHARACTER SET utf8mb4  NOT NULL,
    `test_name3` varchar(7000) CHARACTER SET utf8mb4  NOT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4;

執行上述 sql 報錯信息爲

1118 - Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs, Time: 0.002000s

varchar (M) 實際佔用字節數,除了數據的佔用,還有數據字節數大小的記錄(1-2 字節)。

<font color=red>varchar(255) 存儲的 abc 的時候佔用 4 個字節,可是這個數據加載到內存的時候是佔用定義的時候指定的字節數 (255*3 utf8 編碼)因此這個數值不要隨便填寫</font>

char

char(M) M 也是指的字符數,列採用的字符集不一樣,char 類型數據佔用大小也不同。char 類型的數據沒有達到指定字符數,數據庫會自動補充空格,返回數據的時候在去掉空格。

當一個字符串太長的時候必定要採起 text 類型的數據,text 類型的數據存儲的時候會做爲行溢出數據存儲,就沒有 65535 大小的限制。

// 能夠看到佔用
https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html
數據類型 總字節數 內容字節
VARCHAR(M)、VARBINARY(M) L+ 1 bytes if column values require 0 − 255 bytes,
L+ 2 bytes if values may require more than 255 bytes
TINYBLOB 、TINYTEXT L+1 L<2^8
BLOB、TEXT L + 2 L<2^16
MEDIUMBLOB、MEDIUMTEXT L+ 3 L<2^24
LONGBLOB、LONGTEXT L+ 4 L<2^32
歡迎關注個人微信公衆號【 Mflyyou】獲取文章的持續更新。

github.com/zhangpanqin/MFlyYou 收集技術文章及個人系列文章,歡迎 Star。


本文由 張攀欽的博客 http://www.mflyyou.cn/ 創做。 可自由轉載、引用,但需署名做者且註明文章出處。

如轉載至微信公衆號,請在文末添加做者公衆號二維碼。微信公衆號名稱:Mflyyou

相關文章
相關標籤/搜索