Làm cách nào để DISTINCT ON nhanh hơn trong PostgreSQL?


13

Tôi có một bảng station_logstrong cơ sở dữ liệu PostgreQuery 9.6:

    Column     |            Type             |    
---------------+-----------------------------+
 id            | bigint                      | bigserial
 station_id    | integer                     | not null
 submitted_at  | timestamp without time zone | 
 level_sensor  | double precision            | 
Indexes:
    "station_logs_pkey" PRIMARY KEY, btree (id)
    "uniq_sid_sat" UNIQUE CONSTRAINT, btree (station_id, submitted_at)

Tôi đang cố gắng để có được level_sensorgiá trị cuối cùng dựa trên submitted_at, cho mỗi station_id. Có khoảng 400 station_idgiá trị duy nhất và khoảng 20 nghìn hàng mỗi ngày station_id.

Trước khi tạo chỉ mục:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Duy nhất (chi phí = 4347852,14..4450301.72 hàng = 89 width = 20) (thời gian thực tế = 22202.080..27619.167 hàng = 98 vòng = 1)
   -> Sắp xếp (chi phí = 4347852,14..4399076.93 hàng = 20489916 width = 20) (thời gian thực tế = 22202.077..26540.827 hàng = 20489812 vòng = 1)
         Khóa sắp xếp: trạm_id, submit_at DESC
         Phương pháp sắp xếp: hợp nhất bên ngoài Đĩa: 681040kB
         -> Seq Quét trên trạm_logs (chi phí = 0,00..598895,16 hàng = 20489916 chiều rộng = 20) (thời gian thực tế = 0,023..3443,587 hàng = 20489812 vòng = $
 Thời gian lập kế hoạch: 0,072 ms
 Thời gian thực hiện: 27690.644 ms

Tạo chỉ mục:

CREATE INDEX station_id__submitted_at ON station_logs(station_id, submitted_at DESC);

Sau khi tạo chỉ mục, cho cùng một truy vấn:

 Duy nhất (chi phí = 0,56..2156367,51 hàng = 89 chiều rộng = 20) (thời gian thực tế = 0,184..16263.413 hàng = 98 vòng lặp = 1)
   -> Quét chỉ mục bằng cách sử dụng trạm_id__submit_at trên trạm_logs (chi phí = 0,56..2105142,98 hàng = 20489812 width = 20) (thời gian thực tế = 0.181..1 $
 Thời gian lập kế hoạch: 0,206 ms
 Thời gian thực hiện: 16263.490 ms

Có cách nào để làm cho truy vấn này nhanh hơn? Ví dụ như 1 giây, 16 giây vẫn còn quá nhiều.


2
Có bao nhiêu id trạm riêng biệt, tức là có bao nhiêu hàng truy vấn trả về? Và phiên bản nào của Postgres?
ypercubeᵀᴹ

Postgre 9.6, khoảng 400 trạm_id duy nhất và khoảng 20 nghìn bản ghi mỗi ngày trên mỗi trạm_id
Kokizzu

Truy vấn này trả về một "giá trị level_sensor cuối cùng dựa trên submitted_at, đối với mỗi station_id". DISTINCT ON liên quan đến một lựa chọn ngẫu nhiên trừ trường hợp bạn không cần nó.
philipxy

Câu trả lời:


18

Đối với chỉ 400 trạm, truy vấn này sẽ ồ ạt nhanh hơn:

SELECT s.station_id, l.submitted_at, l.level_sensor
FROM   station s
CROSS  JOIN LATERAL (
   SELECT submitted_at, level_sensor
   FROM   station_logs
   WHERE  station_id = s.station_id
   ORDER  BY submitted_at DESC NULLS LAST
   LIMIT  1
   ) l;

dbfiddle ở đây
(so sánh các kế hoạch cho truy vấn này, thay thế của Abelisto và bản gốc của bạn)

Kết quả EXPLAIN ANALYZEtheo quy định của OP:

 Vòng lặp lồng nhau (chi phí = 0,56..356,65 hàng = 102 chiều rộng = 20) (thời gian thực tế = 0,034..0.979 hàng = 98 vòng lặp = 1)
   -> Seq Quét trên các trạm s (chi phí = 0,00..3,02 hàng = 102 chiều rộng = 4) (thời gian thực tế = 0,009..0.016 hàng = 102 vòng lặp = 1)
   -> Giới hạn (chi phí = 0,56..3,45 hàng = 1 chiều rộng = 16) (thời gian thực tế = 0,009..0.009 hàng = 1 vòng lặp = 102)
         -> Quét chỉ mục bằng cách sử dụng trạm_id__submit_at trên trạm_logs (chi phí = 0,56..664062,38 hàng = 230223 chiều rộng = 16) (thời gian thực tế = 0,009 $
               Chỉ số Cond: (trạm_id = s.id)
 Thời gian lập kế hoạch: 0,542 ms
 Thời gian thực hiện: 1.013 ms   - !!

Chỉ mục duy nhất bạn cần là chỉ số bạn đã tạo : station_id__submitted_at. Các UNIQUEràng buộc uniq_sid_satcũng làm công việc, về cơ bản. Duy trì cả hai dường như lãng phí không gian đĩa và ghi hiệu suất.

Tôi đã thêm NULLS LASTvào ORDER BYtrong truy vấn vì submitted_atkhông được xác định NOT NULL. Lý tưởng nhất, nếu có thể!, Thêm một NOT NULLràng buộc vào cột submitted_at, bỏ chỉ mục bổ sung và xóa NULLS LASTkhỏi truy vấn.

Nếu submitted_atcó thể NULL, hãy tạo UNIQUEchỉ mục này để thay thế cả chỉ mục hiện tại ràng buộc duy nhất của bạn:

CREATE UNIQUE INDEX station_logs_uni ON station_logs(station_id, submitted_at DESC NULLS LAST);

Xem xét:

Đây là giả sử một bảng riêng biệtstation với một hàng cho mỗi hàng có liên quan station_id(thường là PK) - mà bạn nên có một trong hai cách. Nếu bạn không có nó, hãy tạo nó. Một lần nữa, rất nhanh với kỹ thuật rCTE này:

CREATE TABLE station AS
WITH RECURSIVE cte AS (
   (
   SELECT station_id
   FROM   station_logs
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT l.station_id
   FROM   cte c
   ,      LATERAL (   
      SELECT station_id
      FROM   station_logs
      WHERE  station_id > c.station_id
      ORDER  BY station_id
      LIMIT  1
      ) l
   )
TABLE cte;

Tôi sử dụng nó trong fiddle là tốt. Bạn có thể sử dụng một truy vấn tương tự để giải quyết trực tiếp nhiệm vụ của mình mà không cần stationbảng - nếu bạn không thể bị thuyết phục để tạo ra nó.

Hướng dẫn chi tiết, giải thích và giải pháp thay thế:

Tối ưu hóa chỉ số

Truy vấn của bạn nên rất nhanh bây giờ. Chỉ khi bạn vẫn cần tối ưu hóa hiệu suất đọc ...

Có thể có ý nghĩa khi thêm level_sensorcột cuối cùng vào chỉ mục để cho phép quét chỉ mục , như joanolo đã nhận xét .
Con: Nó làm cho chỉ mục lớn hơn - làm tăng thêm một ít chi phí cho tất cả các truy vấn sử dụng nó.
Pro: Nếu bạn thực sự nhận được chỉ mục quét nó, truy vấn trong tay hoàn toàn không phải truy cập các trang heap, điều này làm cho nó nhanh gấp đôi. Nhưng đó có thể là một lợi ích không đáng kể cho truy vấn rất nhanh bây giờ.

Tuy nhiên , tôi không mong đợi rằng sẽ làm việc cho trường hợp của bạn. Bạn đã đề cập:

... khoảng 20k hàng mỗi ngày station_id.

Thông thường, điều đó cho thấy tải ghi không ngừng ( station_idcứ sau 5 giây). Và bạn quan tâm đến hàng mới nhất . Quét chỉ mục chỉ hoạt động đối với các trang heap hiển thị cho tất cả các giao dịch (bit trong bản đồ hiển thị được đặt). Bạn sẽ phải chạy các VACUUMcài đặt cực kỳ tích cực cho bảng để theo kịp tải ghi và nó vẫn không hoạt động hầu hết thời gian. Nếu các giả định của tôi là chính xác, quét chỉ mục sẽ bị loại bỏ, đừng thêm level_sensorvào chỉ mục.

OTOH, nếu các giả định của tôi giữ và bảng của bạn đang phát triển rất lớn , chỉ số BRIN có thể giúp ích. Liên quan:

Hoặc, thậm chí chuyên biệt hơn và hiệu quả hơn: Một chỉ mục một phần chỉ cho các bổ sung mới nhất để cắt bỏ phần lớn các hàng không liên quan:

CREATE INDEX station_id__submitted_at_recent_idx ON station_logs(station_id, submitted_at DESC NULLS LAST)
WHERE submitted_at > '2017-06-24 00:00';

Chọn dấu thời gian mà bạn biết rằng các hàng trẻ hơn phải tồn tại. Bạn phải thêm một WHEREđiều kiện phù hợp cho tất cả các truy vấn, như:

...
WHERE  station_id = s.station_id
AND    submitted_at > '2017-06-24 00:00'
...

Bạn phải điều chỉnh chỉ mục và truy vấn theo thời gian.
Câu trả lời liên quan với nhiều chi tiết hơn:


Bất cứ lúc nào tôi biết rằng tôi muốn một vòng lặp lồng nhau (thường xuyên), sử dụng LATITH là một hiệu suất tăng cho một số tình huống.
Paul Draper

6

Hãy thử cách cổ điển:

create index idx_station_logs__station_id on station_logs(station_id);
create index idx_station_logs__submitted_at on station_logs(submitted_at);

analyse station_logs;

with t as (
  select station_id, max(submitted_at) submitted_at 
  from station_logs 
  group by station_id)
select * 
from t join station_logs l on (
  l.station_id = t.station_id and l.submitted_at = t.submitted_at);

dbfiddle

GIẢI THÍCH PHÂN TÍCH bởi ThreadStarter

 Nested Loop  (cost=701344.63..702110.58 rows=4 width=155) (actual time=6253.062..6253.544 rows=98 loops=1)
   CTE t
     ->  HashAggregate  (cost=701343.18..701344.07 rows=89 width=12) (actual time=6253.042..6253.069 rows=98 loops=1)
           Group Key: station_logs.station_id
           ->  Seq Scan on station_logs  (cost=0.00..598894.12 rows=20489812 width=12) (actual time=0.034..1841.848 rows=20489812 loop$
   ->  CTE Scan on t  (cost=0.00..1.78 rows=89 width=12) (actual time=6253.047..6253.085 rows=98 loops=1)
   ->  Index Scan using station_id__submitted_at on station_logs l  (cost=0.56..8.58 rows=1 width=143) (actual time=0.004..0.004 rows=$
         Index Cond: ((station_id = t.station_id) AND (submitted_at = t.submitted_at))
 Planning time: 0.542 ms
 Execution time: 6253.701 ms
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.