CHỌN DISTINCT trên truy vấn con sử dụng kế hoạch không hiệu quả


8

Tôi đã có một bảng progresses(chứa theo thứ tự hàng trăm ngàn bản ghi hiện tại):

    Column     |            Type             |                        Modifiers                        
---------------+-----------------------------+---------------------------------------------------------
 id            | integer                     | not null default nextval('progresses_id_seq'::regclass)
 lesson_id     | integer                     | 
 user_id       | integer                     | 
 created_at    | timestamp without time zone | 
 deleted_at    | timestamp without time zone | 
Indexes:
    "progresses_pkey" PRIMARY KEY, btree (id)
    "index_progresses_on_deleted_at" btree (deleted_at)
    "index_progresses_on_lesson_id" btree (lesson_id)
    "index_progresses_on_user_id" btree (user_id)

và một khung nhìn v_latest_progressestruy vấn gần đây nhất progressbởi user_idlesson_id:

SELECT DISTINCT ON (progresses.user_id, progresses.lesson_id)
  progresses.id AS progress_id,
  progresses.lesson_id,
  progresses.user_id,
  progresses.created_at,
  progresses.deleted_at
 FROM progresses
WHERE progresses.deleted_at IS NULL
ORDER BY progresses.user_id, progresses.lesson_id, progresses.created_at DESC;

Người dùng có thể có nhiều tiến bộ cho bất kỳ bài học cụ thể nào, nhưng chúng tôi thường muốn truy vấn một tập các tiến trình được tạo gần đây nhất cho một nhóm người dùng hoặc bài học nhất định (hoặc kết hợp cả hai).

Chế độ xem v_latest_progressesthực hiện điều này độc đáo và thậm chí còn hiệu quả khi tôi chỉ định một bộ user_ids:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN ([the same list of ids given by the subquery in the second example below]);
                                                                               QUERY PLAN                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=526.68..528.66 rows=36 width=57)
   ->  Sort  (cost=526.68..527.34 rows=265 width=57)
         Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
         ->  Index Scan using index_progresses_on_user_id on progresses  (cost=0.47..516.01 rows=265 width=57)
               Index Cond: (user_id = ANY ('{ [the above list of user ids] }'::integer[]))
               Filter: (deleted_at IS NULL)
(6 rows)

Tuy nhiên, nếu tôi cố gắng thực hiện cùng một truy vấn thay thế tập hợp user_ids bằng truy vấn con, nó sẽ trở nên rất kém hiệu quả:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Merge Semi Join  (cost=69879.08..72636.12 rows=19984 width=57)
   Merge Cond: (progresses.user_id = users.id)
   ->  Unique  (cost=69843.45..72100.80 rows=39969 width=57)
         ->  Sort  (cost=69843.45..70595.90 rows=300980 width=57)
               Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
               ->  Seq Scan on progresses  (cost=0.00..31136.31 rows=300980 width=57)
                     Filter: (deleted_at IS NULL)
   ->  Sort  (cost=35.63..35.66 rows=10 width=4)
         Sort Key: users.id
         ->  Index Scan using index_users_on_company_id on users  (cost=0.42..35.46 rows=10 width=4)
               Index Cond: (company_id = 44)
(11 rows)

Điều tôi đang cố gắng tìm hiểu là tại sao PostgreSQL muốn thực hiện DISTINCTtruy vấn trên toàn bộ progressesbảng trước khi nó lọc theo truy vấn con trong ví dụ thứ hai.

Có ai có lời khuyên nào về cách cải thiện truy vấn này không?

Câu trả lời:


11

Aaron,

Trong công việc gần đây của tôi, tôi đã xem xét một số câu hỏi tương tự với PostgreSQL. PostgreSQL hầu như luôn luôn khá giỏi trong việc tạo ra kế hoạch truy vấn phù hợp, nhưng nó không phải lúc nào cũng hoàn hảo.

Một số gợi ý đơn giản là đảm bảo chạy ANALYZEtrên progressesbàn của bạn để đảm bảo rằng bạn đã cập nhật số liệu thống kê, nhưng điều này không được đảm bảo để khắc phục sự cố của bạn!

Vì những lý do có lẽ quá dài dòng cho bài đăng này, tôi đã tìm thấy một số hành vi kỳ quặc trong việc thu thập số liệu thống kê ANALYZEvà trình hoạch định truy vấn có thể cần được giải quyết lâu dài. Trong ngắn hạn, mẹo là viết lại truy vấn của bạn để thử và hack kế hoạch truy vấn bạn muốn.

Không có quyền truy cập vào dữ liệu của bạn để kiểm tra, tôi sẽ đưa ra hai gợi ý có thể có sau đây.

1) Sử dụng ARRAY()

PostgreSQL xử lý các mảng và các bộ bản ghi khác nhau trong trình hoạch định truy vấn của nó. Đôi khi bạn sẽ kết thúc với một kế hoạch truy vấn giống hệt nhau. Trong trường hợp này, như trong nhiều trường hợp của tôi, bạn không.

Trong truy vấn ban đầu của bạn, bạn đã có:

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" 
IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);

Như một bước đầu tiên trong việc cố gắng sửa nó, hãy thử

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44));

Lưu ý sự thay đổi của subquery từ INđến =ANY(ARRAY()).

2) Sử dụng CTE

Một mẹo khác là buộc tối ưu hóa riêng biệt, nếu đề xuất đầu tiên của tôi không hoạt động. Tôi biết nhiều người sử dụng thủ thuật này, bởi vì các truy vấn trong CTE được tối ưu hóa và cụ thể hóa tách biệt với truy vấn chính.

EXPLAIN 
WITH user_selection AS(
  SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44
)
SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "id" FROM user_selection));

Về cơ bản, bằng cách tạo CTE user_selectionbằng WITHmệnh đề, bạn đang yêu cầu PostgreSQL thực hiện tối ưu hóa riêng cho truy vấn con

SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44

và sau đó cụ thể hóa những kết quả đó. Tôi sau đó, một lần nữa sử dụng =ANY(ARRAY())biểu thức để cố gắng tự thao tác kế hoạch.

Trong những trường hợp này, có lẽ bạn không thể tin tưởng vào kết quả của EXPLAINnó, bởi vì nó đã nghĩ rằng nó đã tìm ra giải pháp ít tốn kém nhất. Hãy chắc chắn chạy một EXPLAIN (ANALYZE,BUFFERS)...để tìm hiểu những gì nó thực sự chi phí về thời gian và trang đọc.


Hóa ra, đề nghị đầu tiên của bạn làm việc kỳ diệu. Chi phí cho truy vấn đó là 144.07..144.6, CÁCH dưới 70.000 tôi đã nhận được! Cảm ơn rât nhiều.
Aaron

1
Hà! Rất vui vì tôi có thể giúp. Tôi đấu tranh thông qua các vấn đề "hack kế hoạch truy vấn" rất nhiều; đó là một chút nghệ thuật trên đỉnh cao của khoa học.
Chris

Tôi đã học các thủ thuật trái và phải trong nhiều năm để có được cơ sở dữ liệu để làm những gì tôi muốn và tôi phải nói rằng đây là một trong những tình huống lạ mà tôi đã xử lý. Nó thực sự là một nghệ thuật. Tôi thực sự đánh giá cao lời giải thích cũng nghĩ ra của bạn!
Aaron
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.