PostgreSQL - tìm nạp hàng có giá trị Tối đa cho một cột


96

Tôi đang xử lý một bảng Postgres (được gọi là "cuộc sống") chứa bản ghi với các cột cho time_stamp, usr_id, transaction_id và lives_remaining. Tôi cần một truy vấn sẽ cung cấp cho tôi tổng số lives_remaining gần đây nhất cho mỗi usr_id

  1. Có nhiều người dùng (usr_id's riêng biệt)
  2. time_stamp không phải là mã định danh duy nhất: đôi khi các sự kiện của người dùng (từng hàng trong bảng) sẽ xảy ra với cùng time_stamp.
  3. trans_id là duy nhất chỉ trong phạm vi thời gian rất nhỏ: theo thời gian nó lặp lại
  4. còn lại_lives (cho một người dùng nhất định) có thể tăng và giảm theo thời gian

thí dụ:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  07:00 | 1 | 1 | 1    
  09:00 | 4 | 2 | 2    
  10h00 | 2 | 3 | 3    
  10h00 | 1 | 2 | 4    
  11h00 | 4 | 1 | 5    
  11h00 | 3 | 1 | 6    
  13:00 | 3 | 3 | 1    

Vì tôi sẽ cần truy cập các cột khác của hàng với dữ liệu mới nhất cho mỗi usr_id nhất định, tôi cần một truy vấn đưa ra kết quả như sau:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  11h00 | 3 | 1 | 6    
  10h00 | 1 | 2 | 4    
  13:00 | 3 | 3 | 1    

Như đã đề cập, mỗi usr_id có thể được hoặc mất mạng, và đôi khi các sự kiện có dấu thời gian này xảy ra gần nhau đến mức chúng có cùng dấu thời gian! Do đó, truy vấn này sẽ không hoạt động:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

Thay vào đó, tôi cần sử dụng cả time_stamp (đầu tiên) và trans_id (thứ hai) để xác định hàng chính xác. Sau đó, tôi cũng cần chuyển thông tin đó từ truy vấn con đến truy vấn chính sẽ cung cấp dữ liệu cho các cột khác của các hàng thích hợp. Đây là truy vấn hack mà tôi phải làm việc:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

Được rồi, vì vậy điều này hiệu quả, nhưng tôi không thích nó. Nó yêu cầu một truy vấn trong một truy vấn, một liên kết tự và đối với tôi, dường như nó có thể đơn giản hơn nhiều bằng cách lấy hàng mà MAX tìm thấy có dấu thời gian và trans_id lớn nhất. Bảng "sống" có hàng chục triệu hàng để phân tích cú pháp, vì vậy tôi muốn truy vấn này nhanh nhất và hiệu quả nhất có thể. Tôi mới sử dụng RDBM và Postgres nói riêng, vì vậy tôi biết rằng tôi cần phải sử dụng hiệu quả các chỉ mục thích hợp. Tôi hơi mất cách để tối ưu hóa.

Tôi tìm thấy một cuộc thảo luận tương tự ở đây . Tôi có thể thực hiện một số loại Postgres tương đương với một hàm phân tích của Oracle không?

Bất kỳ lời khuyên nào về việc truy cập thông tin cột liên quan được sử dụng bởi một hàm tổng hợp (như MAX), tạo chỉ mục và tạo các truy vấn tốt hơn sẽ được đánh giá cao!

PS Bạn có thể sử dụng phần sau để tạo trường hợp ví dụ của tôi:

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);

Josh, bạn có thể không thích thực tế là truy vấn tự kết hợp, v.v., nhưng điều đó là OK đối với RDBMS có liên quan.
vladr

1
Những gì mà tự kết hợp thực sự sẽ kết thúc dịch sang là một ánh xạ chỉ mục đơn giản, trong đó SELECT bên trong (một trong đó có MAX) quét chỉ mục để loại bỏ các mục không liên quan và nơi SELECT bên ngoài chỉ lấy phần còn lại của các cột từ bảng tương ứng với chỉ số thu hẹp.
vladr

Vlad, cảm ơn vì những lời khuyên và lời giải thích. Nó đã mở ra cho tôi cách bắt đầu hiểu hoạt động bên trong của cơ sở dữ liệu và cách tối ưu hóa các truy vấn. Quassnoi, cảm ơn vì truy vấn tuyệt vời và mẹo về khóa chính; Cả hóa đơn nữa. Rất hữu ích.
Joshua Berry

cảm ơn bạn đã chỉ cho tôi cách lấy MAX BY2 cột!

Câu trả lời:


90

Trên một bảng có 158 nghìn hàng giả ngẫu nhiên (usr_id được phân phối đồng đều từ 0 đến 10k, trans_idđược phân phối đồng đều từ 0 đến 30),

Theo chi phí truy vấn, bên dưới, tôi đang đề cập đến ước tính chi phí của trình tối ưu hóa dựa trên chi phí của Postgres (với xxx_costcác giá trị mặc định của Postgres ), là ước tính hàm được cân nhắc của các tài nguyên I / O và CPU cần thiết; bạn có thể có được điều này bằng cách kích hoạt PgAdminIII và chạy "Truy vấn / Giải thích (F7)" trên truy vấn với "Tùy chọn truy vấn / Giải thích" được đặt thành "Phân tích"

  • Truy vấn Quassnoy của có một ước tính chi phí 745k (!), Và hoàn thành trong 1,3 giây (được đưa ra một chỉ số hợp chất trên ( usr_id, trans_id, time_stamp))
  • Truy vấn của Bill có ước tính chi phí là 93 nghìn và hoàn thành trong 2,9 giây (được cung cấp một chỉ mục kết hợp trên ( usr_id, trans_id))
  • Query # 1 dưới đây có một ước tính chi phí 16k, và hoàn thành trong 800ms (được đưa ra một chỉ số hợp chất trên ( usr_id, trans_id, time_stamp))
  • Query # 2 bên dưới có một ước tính chi phí 14k, và hoàn thành trong 800ms (được đưa ra một chỉ số chức năng hỗn hợp trên ( usr_id, EXTRACT(EPOCH FROM time_stamp), trans_id))
    • đây là Postgres cụ thể
  • Query # 3 dưới đây (Postgres 8.4+) có một ước tính chi phí và thời gian hoàn thành tương đương với (hoặc tốt hơn) truy vấn # 2 (được đưa ra một chỉ số hợp chất trên ( usr_id, time_stamp, trans_id)); nó có lợi thế là chỉ quét livesbảng một lần và, nếu bạn tạm thời tăng (nếu cần) work_mem để phù hợp với sắp xếp trong bộ nhớ, nó sẽ là nhanh nhất trong tất cả các truy vấn.

Tất cả thời gian ở trên bao gồm việc truy xuất bộ kết quả 10k hàng đầy đủ.

Mục tiêu của bạn là ước tính chi phí tối thiểu thời gian thực hiện truy vấn tối thiểu, nhấn mạnh vào chi phí ước tính. Việc thực thi truy vấn có thể phụ thuộc đáng kể vào điều kiện thời gian chạy (ví dụ: các hàng liên quan đã được lưu đầy đủ trong bộ nhớ hay chưa), trong khi ước tính chi phí thì không. Mặt khác, hãy nhớ rằng ước tính chi phí chính xác là ước tính.

Thời gian thực hiện truy vấn tốt nhất có được khi chạy trên cơ sở dữ liệu chuyên dụng mà không tải (ví dụ: chơi với pgAdminIII trên PC phát triển.) Thời gian truy vấn sẽ thay đổi trong quá trình sản xuất dựa trên mức độ lan truyền tải / truy cập dữ liệu thực tế. Khi một truy vấn xuất hiện nhanh hơn một chút (<20%) so với truy vấn kia nhưng có chi phí cao hơn nhiều , nhìn chung sẽ khôn ngoan hơn nếu chọn truy vấn có thời gian thực hiện cao hơn nhưng chi phí thấp hơn.

Khi bạn mong đợi rằng sẽ không có sự cạnh tranh về bộ nhớ trên máy sản xuất của bạn tại thời điểm chạy truy vấn (ví dụ: bộ đệm RDBMS và bộ đệm hệ thống tệp sẽ không bị chặn bởi các truy vấn đồng thời và / hoặc hoạt động của hệ thống tệp) thì thời gian truy vấn bạn nhận được ở chế độ độc lập (ví dụ: pgAdminIII trên PC phát triển) sẽ là đại diện. Nếu có mâu thuẫn về hệ thống sản xuất, thời gian truy vấn sẽ giảm xuống tương ứng với tỷ lệ chi phí ước tính, vì truy vấn có chi phí thấp hơn không phụ thuộc nhiều vào bộ nhớ cache trong khi truy vấn có chi phí cao hơn sẽ truy cập lại cùng một dữ liệu (kích hoạt bổ sung I / O trong trường hợp không có bộ nhớ cache ổn định), ví dụ:

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

Đừng quên chạy ANALYZE livesmột lần sau khi tạo các chỉ số cần thiết.


Truy vấn số 1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

Truy vấn số 2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

Cập nhật 2013/01/29

Cuối cùng, kể từ phiên bản 8.4, Postgres hỗ trợ Chức năng Cửa sổ, nghĩa là bạn có thể viết một cái gì đó đơn giản và hiệu quả như:

Truy vấn số 3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );

Theo chỉ mục kết hợp trên (usr_id, trans_id, times_tamp), có phải ý bạn là "CREATE INDEX lives_blah_idx ON lives (usr_id, trans_id, time_stamp)" không? Hay tôi nên tạo ba chỉ mục riêng biệt cho mỗi cột? Tôi nên gắn bó với mặc định là "SỬ DỤNG btree", phải không?
Joshua Berry

1
Có cho lựa chọn đầu tiên: Ý tôi là TẠO INDEX lives_blah_idx TRÊN mạng (usr_id, trans_id, time_stamp). :) Chúc mừng.
vladr

Cảm ơn vì đã làm cả việc so sánh chi phí vladr! Câu trả lời rất đầy đủ!
Adam

@vladr Tôi vừa xem qua câu trả lời của bạn. Tôi hơi bối rối, như bạn nói truy vấn 1 có chi phí là 16k và truy vấn 2 có chi phí là 14k. Nhưng tiếp tục xuống trong bảng, bạn nói truy vấn 1 có chi phí là 5 nghìn và truy vấn 2 có chi phí là 50k. Vậy truy vấn nào được ưu tiên sử dụng? :) thanks
Houman

1
@Kave, bảng là một cặp truy vấn giả định để minh họa một ví dụ, không phải hai truy vấn của OP. Đổi tên để giảm nhầm lẫn.
vladr

77

Tôi sẽ đề xuất một phiên bản sạch dựa trên DISTINCT ON(xem tài liệu ):

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;

6
Đây là câu trả lời ngắn gọn và hợp lý. Cũng có một tài liệu tham khảo tốt! Đây phải là câu trả lời được chấp nhận.
Prakhar Agrawal

Điều này dường như hiệu quả với tôi trên ứng dụng hơi khác của tôi, nơi không có gì khác sẽ làm được. Chắc chắn nên được nâng lên để hiển thị nhiều hơn.
Jim Factor

8

Đây là một phương pháp khác, không sử dụng truy vấn con tương quan hoặc GROUP BY. Tôi không phải là chuyên gia về điều chỉnh hiệu suất PostgreSQL, vì vậy tôi khuyên bạn nên thử cả giải pháp này và các giải pháp do những người khác đưa ra để xem giải pháp nào phù hợp hơn với bạn.

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

Tôi giả định rằng đó trans_idlà duy nhất ít nhất trên bất kỳ giá trị nhất định nào của time_stamp.


4

Tôi thích kiểu câu trả lời của Mike Woodhouse trên trang khác mà bạn đã đề cập. Nó đặc biệt ngắn gọn khi thứ được tối đa hóa chỉ là một cột duy nhất, trong trường hợp này truy vấn con có thể chỉ sử dụng MAX(some_col)GROUP BYcác cột khác, nhưng trong trường hợp của bạn, bạn có số lượng 2 phần được tối đa hóa, bạn vẫn có thể làm như vậy bằng cách sử dụng ORDER BYcộng với LIMIT 1thay vào đó (như được thực hiện bởi Quassnoi):

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

Tôi thấy việc sử dụng cú pháp row-constructor WHERE (a, b, c) IN (subquery)rất hay vì nó cắt giảm lượng đường ngang cần thiết.


3

Thực sự có một giải pháp hacky cho vấn đề này. Giả sử bạn muốn chọn cây lớn nhất của mỗi khu rừng trong một vùng.

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

Khi bạn nhóm các cây theo các khu rừng, sẽ có một danh sách các cây chưa được sắp xếp và bạn cần tìm cây lớn nhất. Điều đầu tiên bạn nên làm là sắp xếp các hàng theo kích thước của chúng và chọn hàng đầu tiên trong danh sách của bạn. Nó có vẻ không hiệu quả nhưng nếu bạn có hàng triệu hàng, nó sẽ khá nhanh hơn các giải pháp bao gồm JOIN's và WHEREđiều kiện.

BTW, lưu ý rằng ORDER_BYfor array_aggđược giới thiệu trong Postgresql 9.0


Bạn có một lỗi. Bạn cần viết ORDER BY tree_size.size DESC. Ngoài ra, đối tác của tác giả mã sẽ trông như thế này: SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
alexkovelsky

2

Có một tùy chọn mới trong Postgressql 9.5 được gọi là DISTINCT ON

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Nó loại bỏ các hàng trùng lặp, chỉ để lại hàng đầu tiên như đã định nghĩa mệnh đề ORDER BY của tôi.

xem tài liệu chính thức


1
SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

Tạo một chỉ mục trên (usr_id, time_stamp, trans_id)sẽ cải thiện đáng kể truy vấn này.

Bạn nên luôn luôn có một số loại PRIMARY KEYtrong bảng của bạn.


0

Tôi nghĩ bạn có một vấn đề lớn ở đây: không có "bộ đếm" tăng đơn điệu nào để đảm bảo rằng một hàng nhất định xảy ra muộn hơn hàng khác. Lấy ví dụ sau:

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

Bạn không thể xác định từ dữ liệu này là mục nhập gần đây nhất. Nó là cái thứ hai hay cái cuối cùng? Không có hàm sắp xếp hoặc hàm max () nào mà bạn có thể áp dụng cho bất kỳ dữ liệu nào trong số này để cung cấp cho bạn câu trả lời chính xác.

Tăng độ phân giải của dấu thời gian sẽ là một trợ giúp rất lớn. Vì công cụ cơ sở dữ liệu sắp xếp các yêu cầu, với độ phân giải đủ, bạn có thể đảm bảo rằng không có hai dấu thời gian nào giống nhau.

Ngoài ra, hãy sử dụng một trans_id mà nó sẽ không thay đổi trong một thời gian rất dài. Có một trans_id cuộn qua có nghĩa là bạn không thể biết (đối với cùng một dấu thời gian) liệu trans_id 6 có mới hơn trans_id 1 hay không trừ khi bạn thực hiện một số phép toán phức tạp.


Có, lý tưởng là một cột trình tự (tự động tăng dần) theo thứ tự.
vladr

Giả định ở trên là đối với các gia số thời gian nhỏ, trans_id sẽ không chuyển sang. Tôi đồng ý rằng bảng cần một chỉ mục chính duy nhất - giống như một trans_id không lặp lại. (Tái bút, tôi rất vui vì giờ tôi đã có đủ nghiệp chướng / điểm danh tiếng để bình luận!)
Joshua Berry

Vlad nói rằng trans_id có chu kỳ khá ngắn và thường xuyên chuyển đổi. Ngay cả khi bạn chỉ xem xét hai hàng giữa từ bảng của tôi (trans_id = 6 và 1), bạn vẫn không thể biết đâu là hàng gần đây nhất. Do đó, việc sử dụng max (trans_id) cho một dấu thời gian nhất định sẽ không hoạt động.
Barry Brown

Đúng, tôi đang dựa vào sự đảm bảo của tác giả ứng dụng rằng tuple (time_stamp, trans_id) là duy nhất cho một người dùng nhất định. Nếu không phải như vậy thì "SELECT l1.usr_id, l1.lives_left, ... FROM ... WHERE ..." phải trở thành "SELECT l1.usr_id, MAX / MIN (l1.lives_left), ... FROM. .. ở ĐÂU ... GROUP BY l1.usr_id, ...
vladr

0

Một giải pháp khác mà bạn có thể thấy hữu ích.

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1
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.