1. 程式人生 > 其它 >DNS域名解析服務(一)——正向解析和反向解析詳解

DNS域名解析服務(一)——正向解析和反向解析詳解

本文為個人慕課網《效能優化之MySQL優化》課堂筆記。優化目的有三點:避免出現頁面訪問錯誤、增加穩定性以及優化使用者體驗。本文從SQL及索引、資料庫表結構、系統配置以及硬體等方面來說明資料庫該如何優化。

本文為個人慕課網《效能優化之MySQL優化》課堂筆記

一、簡介

1.1 目的

  • 避免出現頁面訪問錯誤
    • 由於資料庫連線timeout產生5xx錯誤
    • 由於慢查詢造成頁面無法載入
    • 由於阻塞造成資料無法提交
  • 增加穩定性
    • 很多資料庫問題都是由於低效的查詢引起的
  • 優化使用者體驗
    • 流暢頁面的訪問速度
    • 良好的網站功能體驗

1.2 優化方面

  • 表結構設計:有益於 SQL 查詢、有益於 SQL 查詢的寫法。

  • 系統配置:MySQL 大多執行在 Linux 中,TCP/IP 連線數的限制、開啟檔案數的限制 (主要) 和安全性的限制。

    • MySQL 資料庫是映象的、是基於檔案的,查詢一個表就是開啟一個檔案。如果檔案數達到限制,檔案就無法開啟,就會進行頻繁的I/O操作。
  • 硬體優化:選擇更適合資料庫的CPU、更快的I/O和更多的記憶體。

    • 資料庫在記憶體中對資料進行操作,因此記憶體越大,效能可能越好。
    • 但是CPU數目越多,可能並不會對資料庫操作造成更多的影響,因為MySQL對CPU的利用有限制,並不會利用太多的核數,有些查詢只能用到單核。
    • I/O裝置,SSD、RAID組合和級別。但是並不能減少阻塞——鎖,是資料庫中儲存資料完整性的一種機制。因此說硬體的優化是成本最高但是效果最差的。

如果SQL和索引沒有優化好,產生大量的慢查詢、阻塞,這由於MySQL內部的鎖機制造成的,再好的硬體也是沒有辦法優化的。

二、資料和工具準備

2.1 資料準備

資料庫基於 MySQL 5.7,使用sakila資料庫:

http://dev.mysql.com/doc/index-other.html

這個資料庫結構基於影片租賃商店業務,記錄了影片的相關資訊、連鎖商店的資訊和客戶資訊等。

2.2 慢日誌的開啟方式和儲存格式

2.2.1 慢日誌的開啟方式

如何發現有問題的SQL?

使用MySQL慢查詢日誌對有效率問題的SQL進行監控

-- 查詢慢查詢日誌
show variables like 'slow_query_log';

-- 指定慢查詢日誌儲存檔案位置
set global slow_query_log_file = '/home/mysql/sql_log/mysql-slow.log';

-- 是否要把沒有使用索引的SQL語句記錄到慢查詢日誌中。通常情況下優化資料庫,主要是為了優化表的索引和查詢方式
set global log_queries_not_using_indexes = on;

-- 把超過long_query_time秒的查詢記錄到慢查詢日誌中,對於業務繁忙的環境,通常設定為100ms(0.01s)
set global long_query_time = 1;

操作步驟:

mysql> show variables like 'slow_query_log';	# 查詢慢查詢日誌
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| slow_query_log | ON    |
+----------------+-------+

mysql> show variables like '%log%'; # 查詢所有日誌
...
log_queries_not_using_indexes              | OFF
...

mysql> set global log_queries_not_using_indexes = on;
Query OK, 0 rows affected (0.01 sec)

mysql> show variables like 'long_query_time';
+-----------------+-----------+
| Variable_name   | Value     |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+

mysql> set global slow_query_log = on;	# 開啟慢查詢日誌
Query OK, 0 rows affected (0.00 sec)

接下來是進行資料庫的查詢:

mysql> use sakila;
Database changed

mysql> show tables;
+----------------------------+
| Tables_in_sakila           |
+----------------------------+
| actor                      |
| actor_info                 |
| address                    |
| category                   |
| city                       |
| country                    |
| customer                   |
| customer_list              |
| film                       |
| film_actor                 |
| film_category              |
| film_list                  |
| film_text                  |
| inventory                  |
| language                   |
| nicer_but_slower_film_list |
| payment                    |
| rental                     |
| sales_by_film_category     |
| sales_by_store             |
| staff                      |
| staff_list                 |
| store                      |
+----------------------------+
23 rows in set (0.00 sec)

mysql> select * from store limit 10;
+----------+------------------+------------+---------------------+
| store_id | manager_staff_id | address_id | last_update         |
+----------+------------------+------------+---------------------+
|        1 |                1 |          1 | 2006-02-15 04:57:12 |
|        2 |                2 |          2 | 2006-02-15 04:57:12 |
+----------+------------------+------------+---------------------+
2 rows in set (0.00 sec)

mysql> show variables like 'slow%';
+---------------------+------------------------------------+
| Variable_name       | Value                              |
+---------------------+------------------------------------+
| slow_launch_time    | 2                                  |
| slow_query_log      | ON                                 |
| slow_query_log_file | /home/mysql/sql_log/mysql-slow.log |
+---------------------+------------------------------------+
3 rows in set, 1 warning (0.00 sec)

2.2.2 慢日誌的儲存格式

檢視mysql-slow.log檔案

# Time: 2021-07-11T09:25:28.885390Z
# User@Host: root[root] @ localhost [::1]  Id:   804
# Query_time: 0.002543  Lock_time: 0.002367 Rows_sent: 2  Rows_examined: 2
use sakila;
SET timestamp=1625995528;
select * from store limit 10;
  1. 執行SQL的主機資訊:User@Host: root[root] @ localhost [::1] Id: 804
  2. SQL的執行資訊:Query_time: 0.002543 Lock_time: 0.002367 Rows_sent: 2 Rows_examined: 2
  3. SQL的執行時間:SET timestamp=1625995528;
  4. SQL的內容:select * from store limit 10;

2.3 慢查詢日誌分析

2.3.1 mysqldumpslow

分析工具:mysqldumpslow

優點:MySQL 自帶

缺點:資訊少

[root@sample ~]# mysqldumpslow

Usage: mysqldumpslow [ OPTS... ] [ LOGS... ]
Parse and summarize the MySQL slow query log. Options are
  --verbose    verbose
  --debug      debug
  --help       write this text to standard output


  -v           verbose
  -d           debug
  -s ORDER     what to sort by (al, at, ar, c, l, r, t), 'at' is default
                al: average lock time
                ar: average rows sent
                at: average query time
                 c: count
                 l: lock time
                 r: rows sent
                 t: query time
  -r           reverse the sort order (largest last instead of first)
  -t NUM       just show the top n queries
  -a           don't abstract all numbers to N and strings to 'S'
  -n NUM       abstract numbers with at least n digits within names
  -g PATTERN   grep: only consider stmts that include this string
  -h HOSTNAME  hostname of db server for *-slow.log filename (can be wildcard),
               default is '*', i.e. match all
  -i NAME      name of server instance (if using mysql.server startup script)
  -l           don't subtract lock time from total time
  
[root@sample ~]# mysqldumpslow -t 3 /home/msyql/data/mysql-slow.log | more
# 分析出較慢的前三個sql
Count: 1  Time=13.23s (13s)  Lock=0.00s (0s)  Rows=2844047.0 (2844047), root[root]@localhost
  SELECT /*!N SQL_NO_CACHE */ * FROM `salaries`

Count: 1  Time=0.01s (0s)  Lock=0.01s (0s)  Rows=463.0 (463), root[root]@localhost
  select * from info

Count: 1  Time=0.00s (0s)  Lock=0.00s (0s)  Rows=42.0 (42), root[root]@localhost
  select * from subscription
  
# 包含執行時間、鎖定時間、傳送的行數、由誰通過什麼伺服器執行、sql的具體內容

2.3.2 pt-query-digest

分析工具:pt-query-digest

優點:顯示資訊多

缺點:需要額外下載

# 使用方法
Usage: pt-query-digest [OPTIONS] [FILES] [DSN]
Options:
(省略)
 --limit=A                    Limit output to the given percentage or count (
                               default 95%:20) # 與mysqldumpslow的-t相同,限定分析(預設95%)的sql
 --explain=d                  Run EXPLAIN for the sample query with this DSN
                               and print results # 查詢SQL執行計劃

# 輸出到檔案
pt-query-digest slow-log > slow_log.report

# 輸出到資料庫表
pt-query-digest slow.log -review \
	h = 127.0.0.1, D = test, p = root, P = 3306, u = root, t = query_review \
	--create-reviewtable \
	--review-history t = hostname_slow

操作步驟:

[root@VM-0-8-centos bin]# pt-query-digest /www/server/data/mysql-slow.log | more

# 150ms user time, 10ms system time, 22.49M rss, 187.16M vsz
# Current date: Sun Jul 11 18:44:22 2021
# Hostname: VM-0-8-centos
# Files: /www/server/data/mysql-slow.log
# Overall: 25 total, 8 unique, 0.00 QPS, 0.00x concurrency _______________ (這裡包含25條查詢,其中不同的查詢有8條,即抽象出來8條)
# Time range: 2021-06-23T09:03:40 to 2021-07-11T10:26:26
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time            13s   176us     13s   530ms     2ms      3s   260us (執行時間)
# Lock time           20ms       0    15ms   812us     1ms     3ms   103us (鎖定時間)
# Rows sent          2.71M       3   2.71M 111.12k   40.45 524.78k    2.90 (傳送的行數)
# Rows examine       2.71M       3   2.71M 111.12k   40.45 524.78k    2.90 (掃描的行數,若其遠遠大於Rows send,說明索引不是很好)
# Query size         4.06k      18     238  166.24  202.40   74.00  202.40 ()

# Profile
# Rank Query ID                            Response time Calls R/Call  V/M
# ==== =================================== ============= ===== ======= ===
#    1 0xE3C753C2F267B2D767A347A2812914DF  13.2295 99.8%     1 13.2295  0.00 SELECT salaries
# MISC 0xMISC                               0.0306  0.2%    24  0.0013   0.0 <7 ITEMS>

# Query 1: 0 QPS, 0x concurrency, ID 0xE3C753C2F267B2D767A347A2812914DF at byte 0 (具體的sql)
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.00
# Time range: all events occurred at 2021-06-23T09:03:40
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count          4       1  (執行了1次,佔總共查詢次數的4%)
# Exec time     99     13s     13s     13s     13s     13s       0     13s
# Lock time      0       0       0       0       0       0       0       0
# Rows sent     99   2.71M   2.71M   2.71M   2.71M   2.71M       0   2.71M (insert查詢該行為0)
# Rows examine  99   2.71M   2.71M   2.71M   2.71M   2.71M       0   2.71M (insert查詢該行為0)
# Query size     1      49      49      49      49      49       0      49
# String:
# Databases    employees
# Hosts        localhost
# Users        root
# Query_time distribution
#   1us
#  10us
# 100us
--More--

2.4 通過慢查詢日誌發現有問題的 SQL

  • 查詢次數多且每次查詢佔用時間長的 SQL
    • 通常為 pt-query-digest 分析的前幾個查詢
  • I/O 大的 SQL
    • 注意 pt-query-digest 分析中的 Rows examine
    • 一個SQL的掃描行數越多,I/O消耗越大
  • 未命中索引的 SQL
    • 注意 pt-query-digest 分析中的 Rows examineRows send 的對比
    • Rows examine遠遠大於Rows send,說明索引命中率不高,基本用索引掃描或表掃描的方式來查詢

2.5 通過explain查詢分析SQL的執行計劃

資料庫中,SQL都是先進行執行計劃的分析,然後再進行SQL的具體查詢。執行計劃從側面反映出SQL執行效率。

explain 返回各列的含義

  • table:顯示這一行的資料是關於哪張表的
  • type:重要的列,顯示連線使用了何種型別。效能從最好到最差的連線型別為 const > eq_reg > ref > range > index > ALL
    • const 是常數查詢,一般是對於主鍵、唯一索引的查詢
    • eq_reg 是範圍的查詢,一般是唯一索引、主鍵的範圍查詢
    • ref 常見於連線的查詢中,比如一個表是基於某個索引的查詢
    • range 是基於索引的範圍查詢
    • index 通常是對於索引的掃描來進行操作
    • ALL 的操作是表掃描。上圖中的SQL沒有WHERE從句,執行的是表掃描操作
  • prossible_keys:顯示可能應用在該表中的索引。若為空,則沒有可以用到的索引
  • key:實際使用的索引。若為NULL,則沒有使用索引。上圖中是表掃描操作,沒有實際用到索引,因此prossible_keyskey都為空
  • key_len:使用的索引的長度。在不損失精確性的情況下,長度越短越好。這是因為 MySQL 的每次讀取都是以為單位的,而如果頁中索引長度越短,相應地,可以儲存的索引數量越大,它的查詢效率就越高。
  • ref:顯示索引的哪一列被使用了,如果可能的話,是一個常數。即常數的索引,執行效果是最好的
  • rows:表掃描的行數
  • Using filesort(擴充套件資訊):查詢結果用到了檔案排序的方式來執行優化,在ORDER BY中比較常見
    • 看到這個的時候,查詢就需要優化了
    • MySQL 需要進行額外的步驟來發現如何對返回的行來進行排序
    • 它根據連線型別以及儲存排序鍵值和匹配條件的全部行的行指標來排序全部行
  • Using temporary(擴充套件資訊):查詢用到了臨時表
    • 看到這個的時候,查詢就需要優化了
    • 這裡,MySQL 需要建立一個臨時表來儲存結果,這通常發生在對不同的列集進行 ORDER BY 上,而不是 GROUP BY

看到了最後兩種擴充套件資訊結果的都是需要優化的,因為用到了外部檔案或臨時表來進行資料的轉儲,這種SQL通常會出現在 GROUP BYORDER BY 從句中

三、SQL及索引優化

3.1 SQL 優化

3.1.1 COUNT() 和 MAX() 的優化

1. MAX() 查詢優化

MAX() 經常會被用於查找出最大值或最後的某一個事件的時間,通常可以用建立索引來優化。

查詢最後支付時間 —— 優化MAX()函式

顯示出SQL的執行計劃,是表掃描,效率低。如何優化這種SQL呢?

通常情況下,可以在payment_date上建立索引

create index idx_paydate on payment(payment_date);

再次查詢

看到Select tables optimized away,表示並不需要實際查詢表的資料,只需要通過索引就可以完全知道SQL的執行結果,這種索引稱作“覆蓋索引”。

因為索引是順序排列的,通過索引的統計資訊就可以知道最後一個payment_date的值。這樣就大大優化了SQL的執行效率,同時儘可能減少了I/O操作。不管資料量有多大,執行頻率有多高,它的執行效率基本上是恆定的。

2. COUNT() 查詢優化

在一條SQL中同時查出2006年和2007年電影的數量 —— 優化 COUNT() 函式

  • 錯誤的方式:
SELECT COUNT(release_year='2006' OR release_year='2007')
FROM film; 
-- 無法分開計算2006年和2007年的電影數量

SELECT COUNT(*)
FROM film
WHERE release_year='2006' AND release_year='2007';
-- release_year不可能同時為2006和2007,有邏輯錯誤
  • 正確的方式:
SELECT COUNT(release_year = '2006' OR NULL) AS '2006年電影數量', 
	   COUNT(release_year = '2007' OR NULL) AS '2007年電影數量'
FROM film;

# 結果集
+---------------------+---------------------+
| 2006年電影數量       | 2007年電影數量       |
+---------------------+---------------------+
|                1000 |                   0 |
+---------------------+---------------------+
1 row in set (0.01 sec)

注:COUNT(*) 和 COUNT(某一列)的區別

# 新建臨時表t
mysql> create table t(id int);
Query OK, 0 rows affected (0.16 sec)

mysql> insert into t values(1), (2), (null);
Query OK, 3 rows affected (0.01 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> select * from t;
+------+
| id   |
+------+
|    1 |
|    2 |
| NULL |
+------+
3 rows in set (0.00 sec)

mysql> select count(*), count(id) from t;
+----------+-----------+
| count(*) | count(id) |
+----------+-----------+
|        3 |         2 |
+----------+-----------+
1 row in set (0.00 sec)

可以看出:count(id) 的結果不包含空值的這一行;而count(*) 是表中所有的行數,包含空值的這一行。


3.1.2 子查詢的優化

通常情況下,需要把子查詢優化為join查詢,但在優化時要注意關聯鍵是否有一對多的關係,要注意重複資料(使用DISTINCT去重)。

-- 查詢sandra出演的所有影片
explain SELECT title, release_year, LENGTH
FROM film
WHERE film_id IN (
    SELECT film_id
    FROM film_actor
    WHERE actor_id IN (
        SELECT actor_id
        FROM actor
        WHERE first_name = 'sandra'
    )
);

explain SELECT DISTINCT title, release_year, LENGTH
FROM film
JOIN film_actor ON film_actor.film_id = film.film_id
JOIN actor ON actor.actor_id = film_actor.actor_id
WHERE actor.first_name = 'sandra';
  • 子查詢執行計劃:

  • 連線查詢執行計劃:

3.1.3 GROUP BY 的優化

-- 未優化語句
explain SELECT actor.first_name, actor.last_name, COUNT(*)
FROM sakila.film_actor
INNER JOIN sakila.actor USING(actor_id)
GROUP BY film_actor.actor_id;

執行計劃:

雖然對一個表中的列進行了GROUP BY操作,但是同樣會出現使用臨時表和檔案排序的方式。另外,由於沒有where語句,該查詢對actor表進行表掃描的操作。

優化語句:

explain SELECT actor.first_name, actor.last_name, c.cnt
FROM sakila.actor INNER JOIN (
    SELECT actor_id, COUNT(*) AS cnt
    FROM sakila.film_actor
    GROUP BY actor_id
) AS c USING(actor_id);

執行計劃:

這裡已經沒有使用臨時表或檔案排序的方式了,取而代之的是使用索引,進行了一個彙總操作。如果資料量非常大,這就能節省大量的I/O操作時間

當然這並不是絕對的,如果在未優化的語句中增加一些過濾條件,這種改寫方式也要相應進行改變,即在子查詢裡增加過濾條件,而不是查詢完之後,再在外層增加過濾條件。

有時候又會利用子查詢,在連線中增加效率。因此對SQL優化要使用靈活的方式。

3.1.4 LIMIT 查詢優化

LIMIT 常用於分頁處理,時常會伴隨 ORDER BY 從句使用,因此大多時候會使用Filesorts,這樣會造成大量的I/O問題。

explain SELECT film_id, description
FROM sakila.film
ORDER BY title
LIMIT 50, 5;

執行計劃:

可以看到,該SQL語句使用了表掃描的操作,同時使用了檔案排序的方式,因此在資料量非常大的情況下會造成I/O問題。

優化

步驟1:使用有索引的列或主鍵進行 ORDER BY 操作
explain SELECT film_id, description
FROM sakila.film
ORDER BY film_id
LIMIT 50, 5;

執行計劃:

改寫之後的語句並沒有使用檔案排序的方式了,取而代之的是主鍵的排序。這裡掃描了55行,即50行後的5行——50 + 5

但是,如果50改成500呢?

可以看到,掃描了505行,I/O操作越來越大。如果翻頁翻到後面的時候,幾千行,它的響應速度就會很慢,所以有必要對其進行進一步的優化。

步驟2:記錄上次返回的主鍵,在下次查詢時使用主鍵過濾
explain SELECT film_id, description
FROM sakila.film
WHERE film_id > 55 AND film_id <= 60
ORDER BY film_id
LIMIT 1, 5;


* 避免了資料量大時掃描過多的記錄

但是有一個缺點,必須要求主鍵是順序排序且連續的。如果主鍵中間空缺了某幾列,那麼列出的記錄可能不足5行。

=> 解決方法:建立一個附加的列,例如index_id列,這一列的資料是自增的,並且加上索引。

3.2 索引優化

3.2.1 選擇合適的列建立索引

  1. 在 WHERE 從句、GROUP BY 從句、ORDER BY 從句和 ON 從句中出現的列
  2. 索引欄位越小越好
  3. 離散度大的列放到聯合索引的前面

SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 584;

問:index(staff_id, customer_id) 好?還是 index(customer_id, staff_id) 好?

答:由於 customer_id 的離散度更大,所以應該使用index(customer_id, staff_id)

  • 如果某列在SQL查詢中執行的頻率非常高,並且查詢中所包含的列相對比較少,那麼就可以通過覆蓋索引的方式來進行優化。

  • 索引欄位越小越好。上文說過,這是因為在資料庫中資料的儲存單位是以“頁”為單位,如果索引欄位小,那麼儲存的資料量越多,一次I/O操作獲取的資料量就越大。

  • 判斷列的離散程度,需要對列進行統計操作,唯一值越多,離散度越好

mysql> select count(distinct customer_id), count(distinct staff_id) from payment;
+-----------------------------+--------------------------+
| count(distinct customer_id) | count(distinct staff_id) |
+-----------------------------+--------------------------+
|                         599 |                        2 |
+-----------------------------+--------------------------+
1 row in set (0.02 sec)
# customer_id有599個,離散度比staff_id大

3.2.2 索引的優化

過多的索引不但會影響寫入效率,同時也會影響查詢。這是因為資料庫在進行查詢分析的時候,首先要選擇使用哪一個索引來進行查詢,如果索引越多,這個分析的過程就越慢,同樣會減小查詢的效率,因此需要優化和維護索引。

重複及冗餘索引

重複索引是指相同的列以相同的順序建立的同類型索引,如下表中PRIMARY kEYID列上的索引就是重複索引

CREATE TABLE test (
    id INT NOT NULL PRIMARY KEY,
    name VARCHAR(10) NOT NULL,
    title VARCHAR(50) NOT NULL,
    UNIQUE(id)
)ENGINE = INNODB;

冗餘索引是指多個索引的字首列相同,或是在聯合索引中包含了主鍵的索引,下面這個例子中 KEY(name, id) 就是一個冗餘索引

CREATE TABLE test (
    id INT NOT NULL PRIMARY KEY,
    name VARCHAR(10) NOT NULL,
    title VARCHAR(50) NOT NULL,
    KEY(name, id)
)ENGINE = INNODB;
方法一:查詢重複及冗餘索引
USE information_schema;

SELECT a.TABLE_SCHEMA AS '資料庫名',
	   a.TABLE_NAME AS '表名',
	   a.INDEX_NAME AS '索引1',
	   b.INDEX_NAME AS '索引2',
	   a.COLUMN_NAME AS '重複列名'
FROM STATISTICS a
JOIN STATISTICS b ON a.TABLE_SCHEMA = b.TABLE_SCHEMA AND a.TABLE_NAME = b.TABLE_NAME
AND a.SEQ_IN_INDEX = b.SEQ_IN_INDEX AND a.COLUMN_NAME = b.COLUMN_NAME
WHERE a.SEQ_IN_INDEX = 1 AND a.INDEX_NAME <> b.INDEX_NAME;
# 結果集
+--------------+------------------+-------------+-------------+--------------+
| 資料庫名     | 表名             | 索引1       | 索引2       | 重複列名     |
+--------------+------------------+-------------+-------------+--------------+
| db_mail_send | services_content | FK_id_6_idx | FK_id_5_idx | service_id   |
| db_mail_send | services_content | FK_id_5_idx | FK_id_6_idx | service_id   |
+--------------+------------------+-------------+-------------+--------------+
2 rows in set (0.02 sec)

只檢查了字首,並沒有檢查哪些索引是包含主鍵的。

方法二:使用 pt-duplicate-key-checker 工具檢查

下載地址:https://www.percona.com/downloads/percona-toolkit/LATEST/

安裝命令:perl Makefile.PL; make; make install

[root@VM-0-8-centos ~]# pt-duplicate-key-checker -uroot -p '123456' -h 127.0.0.1
# ########################################################################
# db_mail_send.services_content                                           
# ########################################################################

# FK_id_6_idx is a duplicate of FK_id_5_idx
# Key definitions:
#   KEY `FK_id_6_idx` (`service_id`),
#   KEY `FK_id_5_idx` (`service_id`),
# Column types:
#	  `service_id` bigint(20) not null comment '????'
# To remove this duplicate index, execute:
ALTER TABLE `db_mail_send`.`services_content` DROP INDEX `FK_id_6_idx`;

# ########################################################################
# Summary of indexes                                                      
# ########################################################################

# Size Duplicate Indexes   8
# Total Duplicate Indexes  1
# Total Indexes            135

刪除不用索引

目前MySQL中還沒有記錄索引的使用情況,但是在PerconMySQL和MariaDB中可以通過INDEX_STATISTICS表來檢視哪些索引未使用,但在MySQL中目前只能通過慢查日誌配合pt-index-usage工具來進行索引使用情況的分析。

pt-index-usage -uroot -p '123456' mysql-slow.log

若是一主多從的情況,如果在不同的從上負載的業務不一樣,那麼我們在蒐集慢查日誌的時候,要蒐集所有主從的慢查日誌進行統一的分析,這樣才能分析出來哪些索引是真正不使用的。

四、資料庫表結構的優化

4.1 選擇合適的資料型別

何為合適

  1. 使用可以存下資料的最小資料型別。
    • 如時間型別,既可以使用VARCHAR,又可以使用DATATIME,還可以使用TIMESTAMP以及整型INT來儲存,選擇需求中最小的一個數據型別即可 => 使用INTTIMESTAMP(MySQL中二者佔用位元組數相同)。
  2. 使用簡單的資料型別。INT要比VARCHAR型別在MySQL處理上簡單。
  3. 儘可能地使用NOT NULL定義欄位。
    • 這是由於INNODB的儲存特定所決定的,對於非NOT NULL的表,它可能需要一些額外的欄位進行儲存,同時也會增加I/O和儲存的開銷。所以設計表時,儘可能把每個欄位設計成NOT NULL並給出預設值。
  4. 儘量少用TEXT型別。
    • 非用不可時最好考慮分表——單獨提出來放到另一張附加表中;
    • 一方面提高主表的查詢效率,另一方面在需要的時候通過附加表的方式來進行大資料的查詢。

使用 INT 儲存日期時間

利用 FROM_UNIXTIME(), UNIX_TIMESTAMP() 兩個函式來進行轉換:

  • FROM_UNIXTIME() 可以把INT型別的時間戳轉換成日期時間格式
  • UNIX_TIMESTAMP() 可以把正常的日期時間轉換成INT型別
CREATE TABLE test (
    id INT AUTO_INCREMENT NOT NULL,
    timestr INT,
    PRIMARY KEY(id)
);
INSERT INTO test(timestr) VALUES (UNIX_TIMESTAMP('2021-07-11 23:38:05'));
SELECT FROM_UNIXTIME(timestr) FROM test;

使用 BIGINT 儲存 IP 地址

IP 地址大多數情況下使用 VARCHAR,需要約 15 Byte;另外一種方法可以使用 BIGINT,只需要使用約 8 Byte。

利用 INET_ATON(), INET_NTOA() 兩個函式來進行轉換:

  • INET_ATON():IP 地址格式到整型
  • INET_NTOA():整型到 IP 地址格式
CREATE TABLE sessions (
    id INT AUTO_INCREMENT NOT NULL,
    ipaddredss BIGINT,
    PRIMARY KEY(id)
);
INSERT INTO session(ipaddredss) VALUES (INET_ATON('192.168.0.1'));
SELECT INET_NTOA(ipaddress) FROM sessions;

4.2 表的正規化化設計

正規化化是指資料庫設計的規範,目前說到正規化化一般是指第三設計正規化 3FN,即不存在非關鍵欄位對任意候選關鍵欄位的傳遞函式依賴

商品名稱 價格 重量 有效期 分類 分類描述
可樂 3.00 250ml 2021.12 飲料 碳酸飲料
雪碧 3.00 250ml 2021.11 飲料 碳酸飲料

存在傳遞函式依賴關係,不符合3FN:(商品名稱) -> (分類) -> (分類描述)

不符合3FN要求的表存在下列問題:

  1. 資料冗餘:(分類,分類描述) 對於每一個商品都會記錄
  2. 資料的插入異常
    • 如果表中沒有飲料商品,就沒有必要記錄飲料分類的相關資訊
  3. 資料的更新異常
    • 只想更新某一個商品的描述資訊,就需要更新這個表中所有關於該商品的描述資訊
  4. 資料的刪除異常
    • 刪除了所有商品,就無法查詢所有的分類

解決問題:表的拆分

上述表拆分為如下形式:

商品名稱 價格 重量 有效期
可樂 3.00 250ml 2021.12
蘋果 8.00 500g
分類 分類描述
酒水飲料 碳酸飲料
生鮮食品 水果
分類 商品名稱
酒水飲料 可樂
生鮮食品 蘋果

4.3 表的反正規化化

反正規化化是指為了查詢效率的考慮把原本符合3FN的表適當地增加冗餘,以達到優化查詢效率的目的,該操作是以空間換時間。

如果表結構是完全符合正規化化的,那麼在查詢的時候勢必需要關聯很多的表。而對於大多數資料庫來說,如果表的關聯非常多的話,就會影響查詢效率。

這四張表完全符合3FN,如果在這種情況下要查詢出訂單的基本資訊(收貨人地址、電話,訂單的金額等等),如何構造SQL呢?至少要關聯三張表:使用者表、訂單表和訂單商品表。如果要反映出商品資訊,還要關聯商品表。得到SQL語句:

SELECT b.使用者名稱, b.電話, b.地址, a.訂單ID, SUM(c.商品價格 * c.商品數量) AS 訂單價格
FROM `訂單表` a
JOIN `使用者表` b ON a.使用者ID = b.使用者ID
JOIN `訂單商品表` c ON c.訂單ID = b.訂單ID
GROUP BY b.使用者名稱, b.電話, b.地址, a.訂單ID;

這裡使用GROUP BY就會使用臨時表,增加I/O操作,減小SQL效率,因此這個SQL並不很好。而以當前的表結構來說,要優化SQL幾乎是不可能的。為了優化SQL,就要對錶結構進行優化操作。如下圖所示:

反正規化化後再查詢訂單資訊

訂單表中增加了訂單價格、使用者名稱、電話、地址,那麼SQL語句變為:

SELECT a.使用者名稱, a.電話, a.地址, a.訂單ID, a.訂單價格
FROM `訂單表` a;

這樣可以提高SQL效率,儘量少關聯一些表,因此表結構的設計對優化SQL起到關鍵性的作用。

4.4 表的垂直拆分

把原來的一個有很多列的表拆分成多個表,這解決了表的寬度問題。

拆分原則:

  • 把不用的欄位單獨存放到一個表中
  • 把大欄位獨立存放到一個表中
  • 把經常一起使用的欄位放到一起

前面說過,在表中儘量不要使用TEXT型別,若非用不可,就儘量將其單獨放到另一張附加表中。因為在查詢過程中,是很少使用description這種大列的,而titledescription又經常在一起使用,所以把這兩列單獨提出來放到一張附加表中。得到拆分後的表結構:

以上為表垂直拆分的一個例子。

4.5 表的水平拆分

該方法是為了解決單表的資料量過大的問題,水平拆分後的每一個表的結構都是完全一致的。

如果單表中的資料量達到了上億條,儘管加了索引,但是查詢效率可能會非常低,並且寫入效率也會減少。這個時候考慮對錶進行水平拆分。

  • 常用的拆分方法:

    1. customer_id 進行 hash運算,如果要拆分成5個表則使用 mod(customer_id, 5) 取出 0 到 4 個值
    2. 針對不同的 hashID 把資料存到不同的表中
  • 拆分後面臨的挑戰:

    1. 跨分割槽表進行資料查詢
    2. 統計及後臺報表操作
  • 解決:

    • 把前後臺的查詢分開,前端查詢用拆分後的表,後端用匯總表。

五、配置優化

5.1 資料庫系統配置優化

資料庫是基於作業系統的,目前大多數MySQL都是安裝在Linux系統之上,所有對於作業系統的一些引數配置也會影響到MySQL的效能,如最大記憶體大小不超過4GB,最大檔案大小不超過2GB。下面是常用的優化配置:

網路方面的配置需修改 /etc/sysctl.conf 檔案

# 增加TCP支援的佇列數
net.ipv4.tcp_max_syn_backlog = 65535
# 減少斷開連線時,資源回收,加快timewait狀態連線的回收
net.ipv4.tcp_max_tw_buckets = 8000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 10

開啟檔案數的限制,可以使用ulimit -a檢視目錄的各位限制,可以修改 /etc/security/limits.conf 檔案

MySQL資料庫的每一個表都是一個檔案,對於INNODB,表查詢的時候都會去開啟表文件,就會佔用一個開啟檔案的數量;如果是MyISAM表,可能會佔用三個開啟檔案的數量。而系統對開啟檔案數本身是有限制的,預設情況下是1024,如果資料庫表非常多的話,就有必要增加開啟檔案數的限制。

# 對limtis.conf檔案增加以下內容
sort nofile 65535
hard nofile 65535

除此之外最好在MySQL伺服器上關閉iptables, selinux等防火牆軟體,特別是大型網站,因為使用iptables等不可避免有一些網路損耗,最好使用硬體防火牆來進行代替。另外還需要考慮檔案分割槽是什麼型別的,需要在實際中考慮。

配置檔案優化

MySQL 可以通過啟動時指定配置引數和使用配置檔案兩種方法進行配置

在大多數情況下配置檔案位於/etc/my.cnf/etc/mysql/my.cnf ,在Windows系統中配置檔案可以位於C:/windows/my.ini檔案,MySQL查詢配置檔案的順序可以通過以下方法獲得:

$ /usr/sbin/mysqld --verbose --help | grep -A 1 'Default options'

注意:如果有多個位置存在配置檔案,並且涉及到相同的檔案引數,則後面檔案的引數會覆蓋前面的。 即,越往後的查詢越起作用。

常用引數說明

innodb_buffer_pool_size

非常重要的一個引數,用於配置InnoDB的緩衝池,如果資料庫中只有InnoDB表,則推薦配置量為總記憶體的 75%,如果還有MyISAM,就適當減少配置量大小。

SELECT ENGINE,
ROUND(SUM(data_length + index_length)/1024/1024, 1) AS "Total MB",
FROM INFORMATION_SCHEMA.TABLES
WHERE table_schema NOT IN (
   "information_schema", "performance_schema"
)
GROUP BY ENGINE;

# 如果系統中只含有InnoDB表,一般建議緩衝池的最小大小要大於所有InnoDB表資料加索引的和
innodb_buffer_pool_size >= Total MB
innodb_buffer_pool_instances

MySQL 5.5 中新增加引數,可以控制緩衝池的個數(增加併發性)。預設情況下只有一個緩衝池,在一定程度下會增加阻塞的頻率(順序使用資源)。

innodb_log_buffer_size

innodb log 緩衝的大小,由於日誌最長每秒鐘重新整理,所以一般不用太大。(先把日誌寫到日誌快取區,然後再提交到磁碟)

innodb_flush_log_at_trx_commit

決定了資料庫在多長時間把變更重新整理到磁碟。關鍵引數,對InnoDB的I/O效率影響很大。預設值為1,取值範圍為0, 1, 2,一般建議設為2,但如果資料安全性要求比較高則使用預設值1,保證事務不丟失。

  • 0:每次提交不重新整理,每一秒才把變更重新整理到磁碟一次。
  • 1:每次提交把變更重新整理到磁碟,最安全。
  • 2:每次提交到緩衝區,每一秒才把緩衝區重新整理到磁碟。
innodb_read_io_threads 和 innodb_write_io_threads

以上兩個引數決定了InnoDB讀寫的I/O程序數,預設為4。根據CPU的核數來分別調整讀、寫負載,以增加讀寫併發的執行緒數

innodb_file_per_table

關鍵引數,控制InnoDB每一個表使用獨立的表空間,預設為OFF,即所有表都會建立在共享表空間中。共享表空間一個檔案,併發寫入的效率就會大大降低。另外共享表空間不能單獨收縮,如果讀取了一個很大的日誌表,想要收縮共享表空間,只能先把所有的資料匯出來之後再匯入進去才能收縮。因此建議設為ON——每一個表使用獨立的表空間。

innodb_stats_on_metdata

決定了MySQL在什麼情況下會重新整理InnoDB表的統計資訊。通常,需要重新整理統計資訊以保持優化器能正確使用到正確的索引,重新整理頻率高也會影響資料庫的效能。預設情況下,查詢系統表都會對統計資訊進行重新整理,這個時候是不必要的,設為OFF。

5.2 第三方配置工具

MySQL Configuration Wizard,現在該工具已經整合在了 Percona Toolkit 裡面

5.3 伺服器硬體優化

5.3.1 如何選擇CPU

是選擇單核更快的CPU還是選擇核數更多的CPU?

  1. MySQL有一些工作只能使用到單核CPU
    • 一條SQL語句的執行
    • Replicate 複製過程中的複製程序
  2. MySQL對CPU核數的支援並不是越多越快
    • MySQL 5.5 使用的伺服器不要超過32核(有國外的網站進行過測試,當伺服器CPU超過32核,5.5版本的效能反而有所下降)

5.3.2 磁碟I/O優化

常用RAID級別:

  • RAID0:條帶,把多個磁碟連結成一個磁碟使用,這個級別I/O效果最好。但是這種方式的安全性不好,一旦某一個條帶損壞,那麼整個資料就會丟失。

  • RAID1:映象,要求至少有兩個磁碟,每組磁碟儲存的資料相同。

  • RAID5:把至少3個硬碟合併成1個邏輯盤使用,資料讀寫時會建立奇偶校驗資訊,並且該資訊和相對應的資料分別儲存於不同的磁碟上。當RAID5的一個磁碟資料損壞,利用剩下的資料和相應的奇偶校驗資訊可以恢復資料。

  • RAID1+0:即RAID1和RAID0的結合,同時具備兩個級別的優缺點,既保證了安全性,又增加了磁碟的讀寫效率。一般建議資料庫使用這個級別

SNA(網路區域儲存) 和 NAT(儲存區域網路) 是否適合資料庫?

  1. 常用於高可用解決方案
    • 如果一臺伺服器宕機了,那麼另一臺伺服器接管這個磁碟矩陣來進行訪問。
  2. 順序讀寫效率很高,但是隨機讀寫不如人意
    • 資料庫大部分進行的隨機讀寫,因此使用這種優化對I/O效率並沒有好處
  3. 資料庫隨機讀寫比率很高

需要重新整理統計資訊以保持優化器能正確使用到正確的索引,重新整理頻率高也會影響資料庫的效能。預設情況下,查詢系統表都會對統計資訊進行重新整理,這個時候是不必要的,設為OFF。

5.2 第三方配置工具

MySQL Configuration Wizard,現在該工具已經整合在了 Percona Toolkit 裡面

5.3 伺服器硬體優化

5.3.1 如何選擇CPU

是選擇單核更快的CPU還是選擇核數更多的CPU?

  1. MySQL有一些工作只能使用到單核CPU
    • 一條SQL語句的執行
    • Replicate 複製過程中的複製程序
  2. MySQL對CPU核數的支援並不是越多越快
    • MySQL 5.5 使用的伺服器不要超過32核(有國外的網站進行過測試,當伺服器CPU超過32核,5.5版本的效能反而有所下降)

5.3.2 磁碟I/O優化

常用RAID級別:

  • RAID0:條帶,把多個磁碟連結成一個磁碟使用,這個級別I/O效果最好。但是這種方式的安全性不好,一旦某一個條帶損壞,那麼整個資料就會丟失。

  • RAID1:映象,要求至少有兩個磁碟,每組磁碟儲存的資料相同。

  • RAID5:把至少3個硬碟合併成1個邏輯盤使用,資料讀寫時會建立奇偶校驗資訊,並且該資訊和相對應的資料分別儲存於不同的磁碟上。當RAID5的一個磁碟資料損壞,利用剩下的資料和相應的奇偶校驗資訊可以恢復資料。

  • RAID1+0:即RAID1和RAID0的結合,同時具備兩個級別的優缺點,既保證了安全性,又增加了磁碟的讀寫效率。一般建議資料庫使用這個級別

SNA(網路區域儲存) 和 NAT(儲存區域網路) 是否適合資料庫?

  1. 常用於高可用解決方案
    • 如果一臺伺服器宕機了,那麼另一臺伺服器接管這個磁碟矩陣來進行訪問。
  2. 順序讀寫效率很高,但是隨機讀寫不如人意
    • 資料庫大部分進行的隨機讀寫,因此使用這種優化對I/O效率並沒有好處
  3. 資料庫隨機讀寫比率很高