Tối ưu hóa truy vấn LATITH THAM GIA trên một bảng lớn


8

Tôi đang sử dụng Postgres 9.5. Tôi có một bảng ghi lại các lượt truy cập trang từ một số trang web. Bảng này chứa khoảng 32 triệu hàng kéo dài từ ngày 1 tháng 1 năm 2016 đến ngày 30 tháng 6 năm 2016.

CREATE TABLE event_pg (
   timestamp_        timestamp without time zone NOT NULL,
   person_id         character(24),
   location_host     varchar(256),
   location_path     varchar(256),
   location_query    varchar(256),
   location_fragment varchar(256)
);

Tôi đang cố điều chỉnh một truy vấn đếm số người thực hiện một chuỗi các lần truy cập trang nhất định. Truy vấn có nghĩa là trả lời các câu hỏi như "có bao nhiêu người đã xem trang chủ và sau đó truy cập trang trợ giúp và sau đó xem trang cảm ơn"? Kết quả trông như thế này

╔════════════╦════════════╦═════════════╗
  home-page  help site   thankyou    
╠════════════╬════════════╬═════════════╣
 10000       9800       1500         
╚════════════╩════════════╩═════════════╝

Lưu ý rằng các con số đang giảm có ý nghĩa, bởi vì 10000 người đã xem trang chủ 9800 đã vào trang trợ giúp và trong số 1500 người đó đã tiếp tục truy cập trang cảm ơn.

SQL cho chuỗi 3 bước sử dụng các phép nối bên như sau:

SELECT 
  sum(view_homepage) AS view_homepage,
  sum(use_help) AS use_help,
  sum(thank_you) AS thank_you
FROM (
  -- Get the first time each user viewed the homepage.
  SELECT X.person_id,
    1 AS view_homepage,
    min(timestamp_) AS view_homepage_time
  FROM event_pg X 
  WHERE X.timestamp_ between '2016-04-23 00:00:00.0' and timestamp '2016-04-30 23:59:59.999'
  AND X.location_host like '2015.testonline.ca'
  GROUP BY X.person_id
) e1 
LEFT JOIN LATERAL (
  SELECT
    Y.person_id,
    1 AS use_help,
    timestamp_ AS use_help_time
  FROM event_pg Y 
  WHERE 
    Y.person_id = e1.person_id AND
    location_host = 'helpcentre.testonline.ca' AND
    timestamp_ BETWEEN view_homepage_time AND timestamp '2016-04-30 23:59:59.999'
  ORDER BY timestamp_
  LIMIT 1
) e2 ON true 
LEFT JOIN LATERAL (
  SELECT
    1 AS thank_you,
    timestamp_ AS thank_you_time
  FROM event_pg Z 
  WHERE Z.person_id = e2.person_id AND
    location_fragment =  '/file/thank-you' AND
    timestamp_ BETWEEN use_help_time AND timestamp '2016-04-30 23:59:59.999'
  ORDER BY timestamp_
  LIMIT 1
) e3 ON true;

Tôi có một chỉ mục trên timestamp_, person_idvà các locationcột. Các truy vấn về phạm vi ngày của một vài ngày hoặc vài tuần rất nhanh (1 đến 10 giây). Trường hợp bị chậm là khi tôi cố gắng chạy truy vấn cho mọi thứ trong khoảng thời gian từ ngày 1 tháng 1 đến ngày 30 tháng 7. Mất hơn một phút. Nếu bạn so sánh hai giải thích bên dưới, bạn có thể thấy nó không còn sử dụng chỉ mục timestamp_ và thay vào đó là Seq Scan vì chỉ mục sẽ không mua cho chúng tôi bất cứ điều gì vì chúng tôi đang truy vấn "mọi lúc" .

Bây giờ tôi nhận ra tính chất vòng lặp lồng nhau của phép nối bên sẽ làm chậm nhiều bản ghi hơn mà nó phải lặp qua nhưng có cách nào tôi có thể tăng tốc truy vấn này cho các phạm vi ngày lớn để nó có tỷ lệ tốt hơn không?

Câu trả lời:


10

Ghi chú sơ bộ

  • Bạn đang sử dụng các loại dữ liệu lẻ. character(24)? char(n)là một loại lỗi thời và hầu như luôn luôn là lựa chọn sai. Bạn có các chỉ mục trên person_idvà tham gia vào nó nhiều lần. integersẽ hiệu quả hơn nhiều vì nhiều lý do. (Hoặc bigint, nếu bạn dự định ghi hơn 2 tỷ hàng trong suốt vòng đời của bảng.) Liên quan:

  • LIKElà vô nghĩa nếu không có ký tự đại diện. Sử dụng =thay thế. Nhanh hơn.
    x.location_host LIKE '2015.testonline.ca'
    x.location_host = '2015.testonline.ca'

  • Sử dụng count(e1.*)hoặc count(*)thay vì thêm một cột giả với giá trị 1cho mỗi truy vấn con. (Ngoại trừ lần cuối ( e3), nơi bạn không cần bất kỳ dữ liệu thực tế nào.)

  • Đôi khi bạn không nhất quán trong việc chuyển chuỗi theo nghĩa đen timestampvà đôi khi không ( timestamp '2016-04-30 23:59:59.999'). Hoặc nó có ý nghĩa, sau đó làm điều đó mọi lúc, hoặc không, sau đó không làm điều đó.
    Nó không. Khi được so sánh với một timestampcột, một chuỗi ký tự được ép buộc bằng timestampmọi cách. Vì vậy, bạn không cần một diễn viên rõ ràng.

  • Kiểu dữ liệu Postgres timestampcó tới 6 chữ số phân số. BETWEENBiểu hiện của bạn để lại trường hợp góc. Tôi thay thế chúng bằng các biểu thức ít bị lỗi hơn.

Chỉ mục

Quan trọng: để tối ưu hóa hiệu suất tạo các chỉ mục nhiều màu .
Đối với truy vấn con đầu tiên hp:

CREATE INDEX event_pg_location_host_timestamp__idx
ON event_pg (location_host, timestamp_);

Hoặc, nếu bạn có thể quét chỉ mục từ nó, hãy thêm person_idvào chỉ mục:

CREATE INDEX event_pg_location_host_timestamp__person_id_idx
ON event_pg (location_host, timestamp_, person_id);

Đối với các khoảng thời gian rất lớn kéo dài hầu hết hoặc tất cả các bảng, chỉ mục này nên được ưu tiên hơn - nó cũng hỗ trợ hlptruy vấn con, vì vậy hãy tạo một trong hai cách:

CREATE INDEX event_pg_location_host_person_id_timestamp__idx
ON event_pg (location_host, person_id, timestamp_);

Dành cho tnk:

CREATE INDEX event_pg_location_fragment_timestamp__idx
ON event_pg (location_fragment, person_id, timestamp_);

Tối ưu hóa với các chỉ mục một phần

Nếu các vị từ của bạn trên location_hostlocation_fragmentlà hằng số, chúng ta có thể sử dụng chỉ số phần rẻ hơn nhiều thay vì , đặc biệt là kể từ khi bạn location_*cột dường như lớn:

CREATE INDEX event_pg_hp_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE  location_host = '2015.testonline.ca';

CREATE INDEX event_pg_hlp_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE  location_host = 'helpcentre.testonline.ca';

CREATE INDEX event_pg_tnk_person_id_ts_idx ON event_pg (person_id, timestamp_)
WHERE  location_fragment = '/file/thank-you';

Xem xét:

Một lần nữa, tất cả các chỉ số này nhỏ hơn đáng kể và nhanh hơn với integerhoặc bigintcho person_id.

Nói chung, bạn cần vào ANALYZEbảng sau khi tạo một chỉ mục mới - hoặc đợi cho đến khi autovacuum khởi động để làm điều đó cho bạn.

Để có được các bản quét chỉ mục , bảng của bạn phải VACUUMđủ. Kiểm tra ngay sau khi VACUUMlàm bằng chứng của khái niệm. Đọc trang Wiki Postgres được liên kết để biết chi tiết nếu bạn không quen với việc quét chỉ mục .

Truy vấn cơ bản

Thực hiện những gì tôi đã thảo luận. Truy vấn cho phạm vi nhỏ ( vài hàng mỗi person_id):

SELECT count(*)::int           AS view_homepage
     , count(hlp.hlp_ts)::int AS use_help
     , count(tnk.yes)::int     AS thank_you
FROM  (
   SELECT DISTINCT ON (person_id)
          person_id, timestamp_ AS hp_ts
   FROM   event_pg
   WHERE  timestamp_ >= '2016-04-23'
   AND    timestamp_ <  '2016-05-01'
   AND    location_host = '2015.testonline.ca'
   ORDER  BY person_id, timestamp_
   ) hp
LEFT JOIN LATERAL (
   SELECT timestamp_ AS hlp_ts
   FROM   event_pg y 
   WHERE  y.person_id = hp.person_id
   AND    timestamp_ >= hp.hp_ts
   AND    timestamp_ <  '2016-05-01'
   AND    location_host = 'helpcentre.testonline.ca'
   ORDER  BY timestamp_
   LIMIT  1
   ) hlp ON true 
LEFT JOIN LATERAL (
   SELECT true AS yes                   -- we only need existence
   FROM   event_pg z
   WHERE  z.person_id = hp.person_id    -- we can use hp here
   AND    location_fragment = '/file/thank-you'
   AND    timestamp_ >= hlp.hlp_ts      -- this introduces dependency on hlp anyways.
   AND    timestamp_ <  '2016-05-01'
   ORDER  BY timestamp_
   LIMIT  1
   ) tnk ON true;

DISTINCT ONthường rẻ hơn cho vài hàng mỗi person_id. Giải thích chi tiết:

Nếu bạn có nhiều hàng trên mỗiperson_id(nhiều khả năng cho các khoảng thời gian lớn hơn), CTE đệ quy được thảo luận trong câu trả lời này trong chương 1a có thể nhanh hơn (nhiều):

Xem nó tích hợp dưới đây.

Tối ưu hóa & tự động hóa truy vấn tốt nhất

Đó là câu hỏi hóc búa cũ: một kỹ thuật truy vấn là tốt nhất cho một tập nhỏ hơn, một cho một tập lớn hơn. Trong trường hợp cụ thể của bạn, chúng tôi có một chỉ báo rất tốt từ đầu - độ dài của khoảng thời gian nhất định - mà chúng tôi có thể sử dụng để quyết định.

Chúng tôi gói tất cả trong một hàm PL / pgSQL. Việc triển khai của tôi chuyển từ DISTINCT ONsang rCTE khi khoảng thời gian nhất định dài hơn ngưỡng đã đặt:

CREATE OR REPLACE FUNCTION f_my_counts(_ts_low_inc timestamp, _ts_hi_excl timestamp)
  RETURNS TABLE (view_homepage int, use_help int, thank_you int) AS
$func$
BEGIN

CASE
WHEN _ts_hi_excl <= _ts_low_inc THEN
   RAISE EXCEPTION 'Timestamp _ts_hi_excl (1st param) must be later than _ts_low_inc!';

WHEN _ts_hi_excl - _ts_low_inc < interval '10 days' THEN  -- example value !!!
-- DISTINCT ON for few rows per person_id
   RETURN QUERY
   WITH hp AS (
      SELECT DISTINCT ON (person_id)
             person_id, timestamp_ AS hp_ts
      FROM   event_pg
      WHERE  timestamp_ >= _ts_low_inc
      AND    timestamp_ <  _ts_hi_excl
      AND    location_host = '2015.testonline.ca'
      ORDER  BY person_id, timestamp_
      )
    , hlp AS (
      SELECT hp.person_id, hlp.hlp_ts
      FROM   hp
      CROSS  JOIN LATERAL (
         SELECT timestamp_ AS hlp_ts
         FROM   event_pg
         WHERE  person_id = hp.person_id
         AND    timestamp_ >= hp.hp_ts
         AND    timestamp_ < _ts_hi_excl
         AND    location_host = 'helpcentre.testonline.ca'  -- match partial idx
         ORDER  BY timestamp_
         LIMIT  1
         ) hlp
      )
   SELECT (SELECT count(*)::int FROM hp)   -- AS view_homepage
        , (SELECT count(*)::int FROM hlp)  -- AS use_help
        , (SELECT count(*)::int            -- AS thank_you
           FROM   hlp
           CROSS  JOIN LATERAL (
              SELECT 1                     -- we only care for existence
              FROM   event_pg
              WHERE  person_id = hlp.person_id
              AND    location_fragment = '/file/thank-you'
              AND    timestamp_ >= hlp.hlp_ts
              AND    timestamp_ < _ts_hi_excl
              ORDER  BY timestamp_
              LIMIT  1
              ) tnk
           );

ELSE
-- rCTE for many rows per person_id
   RETURN QUERY
   WITH RECURSIVE hp AS (
      (  -- parentheses required
      SELECT person_id, timestamp_ AS hp_ts
      FROM   event_pg
      WHERE  timestamp_ >= _ts_low_inc
      AND    timestamp_ <  _ts_hi_excl
      AND    location_host = '2015.testonline.ca'  -- match partial idx
      ORDER  BY person_id, timestamp_
      LIMIT  1
      )
      UNION ALL
      SELECT x.*
      FROM   hp, LATERAL (
         SELECT person_id, timestamp_ AS hp_ts
         FROM   event_pg
         WHERE  person_id  > hp.person_id  -- lateral reference
         AND    timestamp_ >= _ts_low_inc  -- repeat conditions
         AND    timestamp_ <  _ts_hi_excl
         AND    location_host = '2015.testonline.ca'  -- match partial idx
         ORDER  BY person_id, timestamp_
         LIMIT  1
         ) x
      )
    , hlp AS (
      SELECT hp.person_id, hlp.hlp_ts
      FROM   hp
      CROSS  JOIN LATERAL (
         SELECT timestamp_ AS hlp_ts
         FROM   event_pg y 
         WHERE  y.person_id = hp.person_id
         AND    location_host = 'helpcentre.testonline.ca'  -- match partial idx
         AND    timestamp_ >= hp.hp_ts
         AND    timestamp_ < _ts_hi_excl
         ORDER  BY timestamp_
         LIMIT  1
         ) hlp
      )
   SELECT (SELECT count(*)::int FROM hp)   -- AS view_homepage
        , (SELECT count(*)::int FROM hlp)  -- AS use_help
        , (SELECT count(*)::int            -- AS thank_you
           FROM   hlp
           CROSS  JOIN LATERAL (
              SELECT 1                     -- we only care for existence
              FROM   event_pg
              WHERE  person_id = hlp.person_id
              AND    location_fragment = '/file/thank-you'
              AND    timestamp_ >= hlp.hlp_ts
              AND    timestamp_ < _ts_hi_excl
              ORDER  BY timestamp_
              LIMIT  1
              ) tnk
           );
END CASE;

END
$func$  LANGUAGE plpgsql STABLE STRICT;

Gọi:

SELECT * FROM f_my_counts('2016-01-23', '2016-05-01');

Định nghĩa rCTE hoạt động với CTE theo định nghĩa. Tôi cũng đã trượt các CTE cho DISTINCT ONtruy vấn (như tôi đã thảo luận với @Lennart trong các nhận xét ), cho phép chúng tôi sử dụng CROSS JOINthay vì LEFT JOINgiảm tập hợp với mỗi bước, vì chúng tôi có thể đếm riêng từng CTE. Điều này có tác dụng làm việc theo hướng ngược lại:

  • Trên một đã có chúng tôi giảm số lượng hàng sẽ làm cho tham gia thứ ba rẻ hơn.
  • Mặt khác, chúng tôi giới thiệu chi phí hoạt động cho các CTE và cần thêm RAM đáng kể, điều này có thể đặc biệt quan trọng đối với các truy vấn lớn như của bạn.

Bạn sẽ phải kiểm tra cái nào vượt trội hơn cái kia.


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Paul White 9
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.