PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

Tối ưu hóa truy vấn GROUP BY để truy xuất hàng mới nhất cho mỗi người dùng

Để có hiệu suất đọc tốt nhất, bạn cần có chỉ mục đa cột:

CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);

Để thực hiện chỉ quét chỉ mục có thể, hãy thêm cột payload nếu không thì không cần thiết trong chỉ mục bao gồm INCLUDE mệnh đề (Postgres 11 trở lên):

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);

Xem:

  • Việc bao gồm các chỉ mục trong PostgreSQL có giúp THAM GIA các cột không?

Dự phòng cho các phiên bản cũ hơn:

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);

Tại sao DESC NULLS LAST ?

  • Chỉ mục không được sử dụng trong phạm vi truy vấn ngày

Đối với số ít hàng trên mỗi user_id hoặc các bảng nhỏ DISTINCT ON thường nhanh nhất và đơn giản nhất:

  • Chọn hàng đầu tiên trong mỗi GROUP BY nhóm?

Đối với nhiều hàng trên mỗi user_id một quét bỏ qua chỉ mục (hoặc quét chỉ mục lỏng lẻo ) hiệu quả hơn (nhiều). Điều đó không được triển khai cho đến Postgres 12 - công việc đang diễn ra cho Postgres 14. Nhưng có những cách để mô phỏng nó một cách hiệu quả.

Biểu thức bảng thông thường yêu cầu Postgres 8.4+ .
LATERAL yêu cầu Postgres 9.3+ .
Các giải pháp sau vượt ra ngoài những gì được đề cập trong Postgres Wiki .

1. Không có bảng riêng biệt với người dùng duy nhất

Với users riêng biệt bảng, giải pháp trong 2. bên dưới thường đơn giản hơn và nhanh hơn. Bỏ qua.

1a. CTE đệ quy với LATERAL tham gia

WITH RECURSIVE cte AS (
   (                                -- parentheses required
   SELECT user_id, log_date, payload
   FROM   log
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT l.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT l.user_id, l.log_date, l.payload
      FROM   log l
      WHERE  l.user_id > c.user_id  -- lateral reference
      AND    log_date <= :mydate    -- repeat condition
      ORDER  BY l.user_id, l.log_date DESC NULLS LAST
      LIMIT  1
      ) l
   )
TABLE  cte
ORDER  BY user_id;

Điều này rất đơn giản để truy xuất các cột tùy ý và có lẽ tốt nhất trong Postgres hiện tại. Giải thích thêm trong chương 2a. bên dưới.

1b. CTE đệ quy với truy vấn con tương quan

WITH RECURSIVE cte AS (
   (                                           -- parentheses required
   SELECT l AS my_row                          -- whole row
   FROM   log l
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT l                            -- whole row
           FROM   log l
           WHERE  l.user_id > (c.my_row).user_id
           AND    l.log_date <= :mydate        -- repeat condition
           ORDER  BY l.user_id, l.log_date DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
   )
SELECT (my_row).*                              -- decompose row
FROM   cte
WHERE  (my_row).user_id IS NOT NULL
ORDER  BY (my_row).user_id;

Thuận tiện để truy xuất một cột đơn hoặc toàn bộ hàng . Ví dụ sử dụng toàn bộ loại hàng của bảng. Có thể có các biến thể khác.

Để xác nhận một hàng đã được tìm thấy trong lần lặp trước đó, hãy kiểm tra một cột KHÔNG ĐẦY ĐỦ (như khóa chính).

Giải thích thêm cho truy vấn này trong chương 2b. bên dưới.

Có liên quan:

  • Truy vấn N hàng liên quan cuối cùng trên mỗi hàng
  • NHÓM THEO một cột, trong khi sắp xếp theo cột khác trong PostgreSQL

2. Với những người dùng users riêng biệt bảng

Bố cục bảng hầu như không quan trọng miễn là có chính xác một hàng cho mỗi user_id có liên quan được đảm bảo. Ví dụ:

CREATE TABLE users (
   user_id  serial PRIMARY KEY
 , username text NOT NULL
);

Lý tưởng nhất là bảng được sắp xếp vật lý đồng bộ với log bàn. Xem:

  • Tối ưu hóa phạm vi truy vấn dấu thời gian Postgres

Hoặc nó đủ nhỏ (cardinality thấp) nên hầu như không quan trọng. Ngoài ra, việc sắp xếp các hàng trong truy vấn có thể giúp tối ưu hóa hơn nữa hiệu suất. Xem phần bổ sung của Gang Liang. Nếu thứ tự sắp xếp vật lý của users bảng trùng khớp với chỉ mục trên log , điều này có thể không liên quan.

2a. LATERAL tham gia

SELECT u.user_id, l.log_date, l.payload
FROM   users u
CROSS  JOIN LATERAL (
   SELECT l.log_date, l.payload
   FROM   log l
   WHERE  l.user_id = u.user_id         -- lateral reference
   AND    l.log_date <= :mydate
   ORDER  BY l.log_date DESC NULLS LAST
   LIMIT  1
   ) l;

JOIN LATERAL cho phép tham chiếu đến FROM trước các mục trên cùng một cấp độ truy vấn. Xem:

  • Sự khác biệt giữa LATERAL JOIN và một truy vấn con trong PostgreSQL là gì?

Kết quả trong một lần tra cứu chỉ mục (-chỉ) cho mỗi người dùng.

Không trả về hàng nào cho người dùng bị thiếu trong users bàn. Thông thường, một khóa ngoại ràng buộc thực thi tính toàn vẹn tham chiếu sẽ loại trừ điều đó.

Ngoài ra, không có hàng nào cho người dùng mà không có mục nhập phù hợp trong log - phù hợp với câu hỏi ban đầu. Để giữ những người dùng đó trong kết quả, hãy sử dụng LEFT JOIN LATERAL ... ON true thay vì CROSS JOIN LATERAL :

  • Gọi một hàm trả về tập hợp với một đối số mảng nhiều lần

Sử dụng LIMIT n thay vì LIMIT 1 để truy xuất nhiều hơn một hàng (nhưng không phải tất cả) trên mỗi người dùng.

Về hiệu quả, tất cả những điều này đều hoạt động giống nhau:

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

Tuy nhiên, cái cuối cùng có mức độ ưu tiên thấp hơn. JOIN rõ ràng liên kết trước dấu phẩy. Sự khác biệt nhỏ đó có thể quan trọng với nhiều bảng tham gia hơn. Xem:

  • "tham chiếu không hợp lệ đến mục nhập mệnh đề FROM cho bảng" trong truy vấn Postgres

2b. Truy vấn con có liên quan

Lựa chọn tốt để truy xuất cột đơn từ một hàng đơn . Ví dụ về mã:

  • Tối ưu hóa truy vấn tối đa theo nhóm

Điều này cũng có thể xảy ra đối với nhiều cột , nhưng bạn cần nhiều thông minh hơn:

CREATE TEMP TABLE combo (log_date date, payload int);

SELECT user_id, (combo1).*              -- note parentheses
FROM (
   SELECT u.user_id
        , (SELECT (l.log_date, l.payload)::combo
           FROM   log l
           WHERE  l.user_id = u.user_id
           AND    l.log_date <= :mydate
           ORDER  BY l.log_date DESC NULLS LAST
           LIMIT  1) AS combo1
   FROM   users u
   ) sub;

Giống như LEFT JOIN LATERAL ở trên, biến thể này bao gồm tất cả người dùng, ngay cả khi không có mục nhập trong log . Bạn nhận được NULL cho combo1 , bạn có thể dễ dàng lọc bằng WHERE mệnh đề trong truy vấn bên ngoài nếu cần.
Nitpick:trong truy vấn bên ngoài, bạn không thể phân biệt được truy vấn con không tìm thấy hàng hay tất cả các giá trị cột đều là NULL - cùng một kết quả. Bạn cần một NOT NULL trong truy vấn con để tránh sự mơ hồ này.

Một truy vấn con có liên quan chỉ có thể trả về một giá trị duy nhất . Bạn có thể quấn nhiều cột thành một kiểu kết hợp. Nhưng để phân hủy nó sau này, Postgres yêu cầu một loại composite nổi tiếng. Bản ghi ẩn danh chỉ có thể được phân tách khi cung cấp danh sách định nghĩa cột.
Sử dụng kiểu đã đăng ký như kiểu hàng của bảng hiện có. Hoặc đăng ký kiểu kết hợp một cách rõ ràng (và vĩnh viễn) với CREATE TYPE . Hoặc tạo một bảng tạm thời (tự động bị loại bỏ vào cuối phiên) để đăng ký loại hàng của nó tạm thời. Cú pháp truyền:(log_date, payload)::combo

Cuối cùng, chúng tôi không muốn phân hủy combo1 trên cùng một cấp độ truy vấn. Do một điểm yếu trong trình lập kế hoạch truy vấn, điều này sẽ đánh giá truy vấn con một lần cho mỗi cột (vẫn đúng trong Postgres 12). Thay vào đó, hãy đặt nó thành một truy vấn con và phân rã trong truy vấn bên ngoài.

Có liên quan:

  • Nhận các giá trị từ hàng đầu tiên và hàng cuối cùng cho mỗi nhóm

Thể hiện tất cả 4 truy vấn với 100 nghìn mục nhập nhật ký và 1 nghìn người dùng:
db <> fiddle here - trang 11
Old sqlfiddle



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Các trường mô hình duy nhất không phân biệt chữ hoa chữ thường trong Django?

  2. Trình điều khiển QPSQL không được tải Qt

  3. Không tìm thấy trình điều khiển phù hợp khi bao gồm các trình điều khiển cần thiết với maven-assembly-plugin

  4. Cách tìm các bản ghi trùng lặp trong PostgreSQL

  5. Tính đa hình trong bảng cơ sở dữ liệu SQL?