Để 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