Lập chỉ mục / tối ưu hóa cho IN, THAM GIA, NHÓM THEO, ĐẶT HÀNG theo truy vấn


7

Tôi đang làm việc trên một truy vấn mà tôi cần phải sử dụng IN, BETWEEN, GROUP BY, JOIN, ORDER BYtất cả trong một truy vấn. Tôi đang vật lộn với hiệu năng cho truy vấn đó, vì vậy tôi cần trợ giúp để chọn chỉ mục hoặc thực hiện thay đổi cấu trúc bảng nếu chỉ mục không giúp được.

Một số cân nhắc

  1. Số lượng hàng cho cả hai bảng dưới đây là trong millions.
  2. Có chức năng nơi người dùng có thể lọc danh sách bằng cách name, age, gender, vv
  3. Có chức năng nơi người dùng có thể sắp xếp danh sách theo một số số liệu như age, visits_countv.v.
  4. Cần phân trang cho danh sách.

Cấu trúc bảng

Bảng 1

CREATE TABLE `table_1` (
  `visitor_id` varchar(32) CHARACTER SET ascii NOT NULL,
  `name` varchar(200) NOT NULL,
  `gender` varchar(1) NOT NULL DEFAULT 'M',
  `mobile_number` int(10) unsigned DEFAULT NULL,
  `age` tinyint(1) unsigned NOT NULL DEFAULT '1',
  `visits_count` mediumint(5) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`visitor_id`),
  KEY `indx_t1_test` (`visitor_id`,`visits_count`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

ban 2

CREATE TABLE `table_2` (
  `company_id` bigint(20) unsigned NOT NULL,
  `visitor_id` varchar(32) CHARACTER SET ascii NOT NULL,
  `time_duration` mediumint(5) unsigned NOT NULL DEFAULT '0',
  `visited_on` date NOT NULL,
  PRIMARY KEY (`company_id`,`visitor_id`,`visited_on`),
  KEY `indx_t2_test` (`visited_on`,`company_id`,`visitor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

Dữ liệu cơ bản nhất tôi muốn truy xuất

Muốn có 20 khách truy cập (Phân trang ) duy nhất ( GROUP BY / DISTINCT) đã truy cập nhóm công ty cụ thể ( INmột phần) giữa thời gian ( BETWEENmột phần) thời gian đã chọn theo tuổi ( ORDER BYphần) của họ.

Truy vấn 1

Truy vấn đầu tiên nếu tôi viết ra cho điều này thì nó sẽ là:

SELECT
    t1.visitor_id
FROM table_1 AS t1
INNER JOIN table_2 AS t2 ON t2.visitor_id = t1.visitor_id
WHERE
    t2.company_id IN (528,211,1275,521,1299,493,492,852,868,869,1235,486,485,1238,855,1237,651,538,1241,1240,548,543,1247,1253,490,468,582,583,569,477,488,802,1294,518,1274,476,545,1267,556,479,1266,1265,541,1189,1263,1152,1260,478,1257,885,1139,1256,804,708,547,561,1239,1142,1226,1148,1230,529,1223,1192,1191,874,830,822,818,817,794,718,487,709,706,705,669,513,455) AND
    t2.visited_on BETWEEN '2015-01-01' AND '2017-01-31'
GROUP BY t1.visitor_id
ORDER BY t1.`visits_count` DESC
LIMIT 20;

Khi tôi chạy truy vấn này cho bất kỳ công ty nào, nó sẽ trả về dữ liệu đủ nhanh (khi số lượng hàng khớp với số lượng nhỏ, hiệu suất truy vấn tốt).

Các vấn đề là khi số lượng các công ty tăng INmột phần của truy vấn (Tôi cần phải hỗ trợ 100 công ty cho phần này của truy vấn), phải mất thời gian về 36 secondsvới kết quả lợi nhuận.

Explain đầu ra của truy vấn này là:

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

Truy vấn 2

Truy vấn thứ hai tôi có thể nghĩ cho trường hợp tương tự, sau đó sẽ là một cái gì đó như thế này:

SELECT
(
    SELECT  
        t2.visitor_id
    FROM table_2 AS t2
    WHERE 
        t2.company_id IN (528,211,1275,521,1299,493,492,852,868,869,1235,486,485,1238,855,1237,651,538,1241,1240,548,543,1247,1253,490,468,582,583,569,477,488,802,1294,518,1274,476,545,1267,556,479,1266,1265,541,1189,1263,1152,1260,478,1257,885,1139,1256,804,708,547,561,1239,1142,1226,1148,1230,529,1223,1192,1191,874,830,822,818,817,794,718,487,709,706,705,669,513,455)
        AND t2.visitor_id = t1.`visitor_id`
        AND t2.visited_on BETWEEN '2015-01-01' AND '2017-01-31'
    LIMIT 1
) AS visitor_id
FROM `table_1` AS t1
HAVING visitor_id IS NOT NULL
ORDER BY t1.`visits_count` DESC
LIMIT 0, 20

Hành vi của truy vấn này ngược lại với truy vấn đầu tiên. Nếu tôi chạy truy vấn cho công ty có ít khách truy cập, hiệu suất của truy vấn này rất thấp (mất khoảng một phần 38 seconds) (chỉ một công ty INmột phần và công ty đó chỉ có 3-4 khách truy cập). Khi số lượng công ty INmột phần cao, nó sẽ trả về kết quả nhanh hơn so với một công ty (mất khoảng 13 seconds), nhưng hiệu quả vẫn không thể sử dụng được.

Explain đầu ra của truy vấn này là:

nhập mô tả hình ảnh ở đây

Truy vấn 3

Để loại bỏ việc sử dụng INmột phần truy vấn, tôi đã tạo bảng tạm thời và thêm id công ty vào bảng đó và sau đó sử dụng JOIN:

SELECT
    DISTINCT
    t1.visitor_id
FROM `table_1` AS t1
INNER JOIN `table_2` AS t2 ON t1.`visitor_id` = t2.visitor_id
INNER JOIN temp_table AS t3 ON t3.company_id = t2.company_id
ORDER BY t1.`visits_count` DESC
LIMIT 0, 20;

Truy vấn này cũng mất thời gian lên tới 22 giây . Tôi cần hiệu suất lên đến 2-3 secondsdanh sách này.

Thông tin thêm

  • innodb_buffer_pool_size là 12 GB
  • RAM là 30 GB
  • Tôi đang sử dụng db.r3.xlargeví dụ AWS RDS
  • SHOW TABLE STATUS đầu ra như sau:
    nhập mô tả hình ảnh ở đây

    1. Truy vấn SELECT COUNT(*) FROM table_2 WHERE company_id IN (...) AND visited_on BETWEEN '2015-01-01' AND '2017-01-31'trả về2660123

    2. Lần đầu tiên chỉ có nó là mất thời gian. Nếu tôi chạy lại cùng một truy vấn thì nó sẽ nhanh hơn rất nhiều (0,2 giây). Nhưng, nếu tôi thay đổi giới hạn thành LIMIT 20, 20lần nữa thì nó sẽ lặp lại 24 secondslần đầu tiên và cùng lần truy vấn thứ hai sẽ nhanh hơn. Nó có thể là vì innodb_buffer_pool_size.

    3. Đầu ra của EXPLAIN FORMAT=JSON SELECT ...;như sau.

      {
      "query_block": {
      "select_id": 1,
      "ordering_operation": {
        "using_filesort": true,
        "grouping_operation": {
          "using_temporary_table": true,
          "using_filesort": false,
          "nested_loop": [
            {
              "table": {
                "table_name": "t2",
                "access_type": "range",
                "possible_keys": [
                  "PRIMARY",
                  "indx_t2_test"
                ],
                "key": "PRIMARY",
                "used_key_parts": [
                  "company_id"
                ],
                "key_length": "8",
                "rows": 17301,
                "filtered": 100,
                "using_index": true,
                "attached_condition": "((`db`.`t2`.`company_id` in (528,211,1275,521,1299,493,492,852,868,869,1235,486,485,1238,855,1237,651,538,1241,1240,548,543,1247,1253,490,468,582,583,569,477,488,802,1294,518,1274,476,545,1267,556,479,1266,1265,541,1189,1263,1152,1260,478,1257,885,1139,1256,804,708,547,561,1239,1142,1226,1148,1230,529,1223,1192,1191,874,830,822,818,817,794,718,487,709,706,705,669,513,455)) and (`db`.`t2`.`visited_on` between '2015-01-01' and '2017-01-31'))"
              }
            },
            {
              "table": {
                "table_name": "t1",
                "access_type": "eq_ref",
                "possible_keys": [
                  "PRIMARY",
                  "indx_t1_test"
                ],
                "key": "PRIMARY",
                "used_key_parts": [
                  "visitor_id"
                ],
                "key_length": "34",
                "ref": [
                  "db.t2.visitor_id"
                ],
                "rows": 1,
                "filtered": 100
              }
            }
          ]
        }
      }
      }
      }

Đầu ra của truy vấn được đề xuất bởi Rick James:

SELECT
    t2.visitor_id
FROM (
    SELECT
        DISTINCT visitor_id
    FROM table_2
    WHERE 
        company_id IN (528,211,1275,521,1299,493,492,852, 868,
                        869,1235,486,485,1238,855,1237,651,538,1241,1240, 548,
                        543,1247,1253,490,468,582,583,569,477,488,802,1294, 518,
                        1274,476,545,1267,556,479,1266,1265,541,1189,1263, 1152,
                        1260,478,1257,885,1139,1256,804,708,547,561,1239, 1142,
                        1226,1148,1230,529,1223,1192,1191,874,830,822,818, 817,
                        794,718,487,709,706,705,669,513,455)
        AND visited_on BETWEEN '2015-01-01' AND '2017-01-31'
) AS t2
INNER JOIN table_1 AS t1 ON t2.visitor_id = t1.visitor_id
ORDER BY t1.`visits_count` DESC
LIMIT 20;

Explain đầu ra của truy vấn như sau:

nhập mô tả hình ảnh ở đây

Truy vấn này mất 58 giây

nhập mô tả hình ảnh ở đây

Explain đầu ra của truy vấn con bên trong như sau

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây


Truy vấn:

SELECT
    COUNT(DISTINCT company_id, visited_on, visitor_id), 
    COUNT(DISTINCT company_id, LEFT(visited_on, 7), visitor_id), 
    COUNT(*) 
FROM table_2;

trả về:

  • COUNT(DISTINCT company_id, visited_on, visitor_id) = 7607938.
  • COUNT(DISTINCT company_id, LEFT(visited_on, 7), visitor_id) = 5194480
  • COUNT(*) = 7607938

Lưu ý đầu ra này là với dữ liệu mới nhất, vì vậy số lượng hàng trong count(*)có thể đã tăng lên.


Không có chỉ mục mục đích chung tốt cho các truy vấn của bạn và nó sẽ phụ thuộc rất nhiều vào dữ liệu của bạn, ví dụ: xác suất của một hàng trong bảng 1 nằm trong tập kết quả của bạn. Có bao nhiêu kết quả bạn nhận được cho ví dụ truy vấn 1 (không có limitkhóa học)? Nếu đây là một phần không thể thiếu trong mô hình kinh doanh của bạn, bạn có thể muốn nghĩ về kết quả được tính toán trước. Để kiểm tra: bạn có thể thử thêm một chỉ mục (visits_count)trên bảng 1 và (visitor_id, company, visited_on)trên bảng 2, thử truy vấn 1 mà không có (sai) group by, select distinct t1.visitor_idthay vào đó sử dụng và buộc sử dụng các chỉ mục này?
Solarflare

@PareshBalar - Cách thức cấu trúc lược đồ, 2660123 hàng sẽ phải được kiểm tra. Bạn đã may mắn chỉ mất 58 giây. Truy vấn của bạn đang kiểm tra 25 tháng. Đây có phải là điển hình? Đó là, bạn có muốn số đo hàng tháng? Hoặc có thể là một số khoảng khác. (Tôi đang suy nghĩ về Bảng tóm tắt.)
Rick James

@RickJames không may là danh sách khách truy cập của nó với tính năng cuộn vô hạn, người dùng cũng có thể thay đổi phạm vi ngày thành danh sách bộ lọc. phạm vi ngày ban đầu sẽ là từ 3 đến 6 tháng, chỉ có khả năng NHƯNG là người dùng có thể thay đổi phạm vi ngày đó thành 2 năm HOẶC nhiều hơn. Tôi sẵn sàng thay đổi lược đồ nếu có khả năng tăng hiệu suất. Nếu tôi chỉ cần hiển thị số lượng khách truy cập hàng tháng hoặc một số loại thống kê, thì tôi có thể nghĩ về các bảng tóm tắt NHƯNG không chắc chắn làm thế nào tôi sẽ tạo bảng tóm tắt cho loại tính năng liệt kê này?
Paresh Balar

Hmmm ... Một du khách hiếm khi đến thăm một công ty hai lần trong một tháng. Điều này ngụ ý rằng Bảng Tóm tắt được chia nhỏ trong tháng của tôi không có khả năng giúp ích nhiều.
Rick James

Câu trả lời:


2

age int(3) unsigned- Điều đó cho phép bạn lưu trữ độ tuổi lên tới 4 tỷ và lãng phí 4 byte. Thay đổi thành TINYINT UNSIGNED(1 byte).

Ascii cho tên? Giới hạn ở Mỹ? Mặc dù vậy, không cho phép một số tên kỳ lạ.

Tôi bối rối bởi t2's PRIMARY KEY. Vì PK là duy nhất, điều này không cho phép ghi lại nhiều lần truy cập vào một công ty cho một người. Nếu hạn chế là OK, hãy thêm điều này (trong trường hợp Trình tối ưu hóa quyết định rằng phạm vi dữ liệu là bộ lọc tốt nhất):

INDEX(visited_on, conpany_id, visitor_id)

Nếu linh cảm của tôi là chính xác thì hãy thay đổi PK và thêm chỉ mục:

PRIMARY KEY(`company_id`, `visitor_id`, visited_on),
INDEX(visited_on, conpany_id, visitor_id)

Sau đó kiểm tra các câu hỏi khác nhau của bạn.


Đã thử thêm cả hai chỉ mục mà bạn đề xuất, bộ chọn tham gia truy vấn vẫn sử dụng Chỉ số PRIMARY và truy vấn mất nhiều thời gian22 seconds
Paresh Balar

0

Thêm một thử.

SELECT  visitor_id
    FROM (
        SELECT  DISTINCT visitor_id
            FROM  table_2
            WHERE  company_id IN (528,211,1275,521,1299,493,492,852, 868,
                        869,1235,486,485,1238,855,1237,651,538,1241,1240, 548,
                        543,1247,1253,490,468,582,583,569,477,488,802,1294, 518,
                        1274,476,545,1267,556,479,1266,1265,541,1189,1263, 1152,
                        1260,478,1257,885,1139,1256,804,708,547,561,1239, 1142,
                        1226,1148,1230,529,1223,1192,1191,874,830,822,818, 817,
                        794,718,487,709,706,705,669,513,455 
                          )
              AND  visited_on BETWEEN '2015-01-01' AND '2017-01-31' 
        ) AS t2
    INNER JOIN  table_1 AS t1  ON t2.visitor_id = t1.visitor_id
    ORDER BY  t1.`visits_count` DESC
    LIMIT  20;

Lý do tôi chọn cách tiếp cận này là JSON dường như nói rằng việc JOINđó không cần thiết phải được thực hiện trước GROUP BY. (Tôi đã thay đổi điều đó DISTINCTbởi vì đó là một cách hợp lý hơn để nêu nó.)

Tôi đoán rằng có rất nhiều khách truy cập sau khi lọc trên company_id và visit_on, nhưng sau đó danh sách đó bị giảm xuống rất nhiều. Làm giảm whittling trước sự JOINlà tại sao điều này có thể được nhanh hơn.

Hơn

Bây giờ dựa trên một số thông tin về phạm vi truy vấn sẽ được chạy và phân phối dữ liệu ...

Bạn có hai loại truy vấn đến từ cùng một giao diện người dùng:

  • Đẹp, văn minh, truy vấn (công ty nhỏ, phạm vi ngày ngắn) trong đó Trình tối ưu hóa có cơ hội thực hiện công việc tốt. (Gợi ý INDEX(company_id)INDEX(visited_on).)
  • Các truy vấn khác mà bạn cho phép người dùng yêu cầu một cái gì đó vốn đã chậm. (Trình tối ưu hóa sẽ tránh bất kỳ chỉ mục nào và chỉ cần quét toàn bộ bảng. Và đây là cách tốt nhất có thể làm.)

Thông thường, tại thời điểm này tôi sẽ hát những lời khen ngợi của Bảng Tóm tắt. Nhưng bạn có một yêu cầu ngăn chặn như vậy - DISTINCT. (Tôi có một blog về cách triển khai như vậy, nhưng tôi không muốn tham gia vào đó.

Một suy nghĩ khác là PARTITION BY RANGE(TO_DAYS(visited_on))PRIMARY KEY bắt đầu với company_id. Bản chất 2 chiều của quey của bạn được đề cập là một trong số ít các trường hợp sử dụng để phân vùng ở đây và thảo luận thêm ở đây . (Phân vùng theo tháng.)

Hoặc, bạn có thể tránh vấn đề bằng cách suy nghĩ lại UI. Nếu bạn giới hạn người dùng không quá 3 tháng, thì có thể sử dụng một chỉ mục bắt đầu bằng visited_on.

Một điều nữa ... VARCHAR(32)là cồng kềnh. Bình thường hóa nó xuống MEDIUMINT UNSIGNED; điều đó sẽ thu nhỏ dữ liệu, làm cho ít I / O hơn và bộ nhớ đệm tốt hơn, do đó tốc độ nhanh hơn.

Nhưng, tôi nhắc lại, bạn có một vấn đề khó khăn.


Phần nào của bảng được bao phủ bởi phạm vi ngày đó? Đó có phải là một phạm vi điển hình? Hay nó thường là một phạm vi nhỏ hơn? Hay rộng hơn?
Rick James

Tổng số hồ sơ trong table_2(không có bất kỳ điều kiện nào) 7607938và số lượng hồ sơ chỉ có điều kiện phạm vi ngày là 7167282, Không có điều này không phải là điển hình, tôi đã xem xét mức độ kịch bản cao với hơn 70 công ty và 2 năm phạm vi ngày, thông thường phạm vi ngày mặc định sẽ là 3 tháng hoặc 6 tháng NHƯNG người dùng có thể thay đổi điều đó thành 2 năm trở lên. Áp dụng tương tự cho công ty là tốt. đối với trường hợp bình thường, số lượng công ty sẽ là 30-40. Nhưng trong trường hợp tối đa sẽ là hơn 100 hoặc 200.
Paresh Balar

Ngoài ra số lượng hồ sơ sau khi whereáp dụng thực sự phụ thuộc vào quy mô công ty cũng như nếu công ty là một loại công ty dịch vụ công cộng thì số lượng khách truy cập có thể cao đối với công ty đó, ngay cả khi phạm vi ngày sẽ nhỏ và các công ty sẽ ít hơn INmột phần, số lượng hồ sơ có thể cao trong tập kết quả.
Paresh Balar

-1

Cần thêm Index vào (visitor_id, age, visit_on), điều này sẽ cải thiện hiệu suất của query1 được đề cập.

Nhóm theo và Sắp xếp bằng cách tạo bảng tạm thời do đó truy vấn chậm.


1
agevisited_onở trong các bảng khác nhau, tôi không nghĩ có cách nào để thêm chỉ mục mà bạn đề xuất.
Paresh Balar
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.