PostgreSQL: Tạo một chuỗi ngày cho mỗi nhóm trong một bảng


7

Tôi có một balancesbảng trong PostgreSQL 9.3 trông như thế này:

CREATE TABLE balances (
  user_id INT
, balance INT
, as_of_date DATE
);

INSERT INTO balances (user_id, balance, as_of_date) VALUES
  (1, 100, '2016-01-03')
, (1,  50, '2016-01-02')
, (1,  10, '2016-01-01')
, (2, 200, '2016-01-01')
, (3,  30, '2016-01-03');

Nó chỉ chứa số dư cho những ngày mà người dùng đã thực hiện giao dịch. Tôi cần nó để chứa một hàng cho mỗi người dùng với số dư của họ vào mỗi ngày trong một phạm vi ngày nhất định.

  • Nếu người dùng không có một hàng cho một ngày nhất định trong phạm vi, tôi cần sử dụng số dư của họ từ ngày hôm trước.
  • Nếu người dùng tạo tài khoản của họ sau một ngày nhất định trong phạm vi, tôi cần tránh tạo một hàng cho kết hợp người dùng / ngày đó.

Tôi có thể tham khảo một accountsbảng để có được người dùng create_date:

CREATE TABLE accounts (
  user_id INT
, create_date DATE
);

INSERT INTO accounts (user_id, create_date) VALUES
  (1, '2015-12-01')
, (2, '2015-12-31')
, (3, '2016-01-03');

Kết quả mong muốn của tôi trông như thế này:

+---------+---------+--------------------------+
| user_id | balance |        as_of_date        |
+---------+---------+--------------------------+
|       1 |     100 | 2016-01-03T00:00:00.000Z |
|       1 |      50 | 2016-01-02T00:00:00.000Z |
|       1 |      10 | 2016-01-01T00:00:00.000Z |
|       2 |     200 | 2016-01-03T00:00:00.000Z |
|       2 |     200 | 2016-01-02T00:00:00.000Z |
|       2 |     200 | 2016-01-01T00:00:00.000Z |
|       3 |      30 | 2016-01-03T00:00:00.000Z |
+---------+---------+--------------------------+

Lưu ý rằng các hàng đã được thêm cho người dùng 2 cho 2016-01-022016-01-03, mang theo số dư trước đó từ 2016-01-01; và không có hàng nào được thêm cho người dùng 3, người được tạo trên đó 2016-01-03.

Để tạo một chuỗi ngày trong phạm vi ngày, tôi biết tôi có thể sử dụng:

SELECT d.date FROM GENERATE_SERIES('2016-01-01', '2016-01-03', '1 day'::INTERVAL) d

... nhưng tôi đang vật lộn với LEFT JOINchuỗi đó với từng nhóm hàng được nhóm theo user_id.


Còn những người dùng có giao dịch đầu tiên muộn hơn họ created_atthì sao? Liệt kê chúng với số dư 0 cho những ngày đầu tiên? Hay với NULL? Hoặc không liệt kê cho đến khi giao dịch đầu tiên? Hay là không thể?
Erwin Brandstetter

Bạn đã đúng, số dư bằng 0 sẽ phù hợp trong trường hợp đó.
Shaun Scovil

Câu trả lời:


5

1. CROSS JOIN, LEFT JOIN LATERALđể truy vấn

SELECT a.user_id, COALESCE(b.balance, 0) AS balance, d.as_of_date
FROM   (
   SELECT d::date AS as_of_date  -- cast to date right away
   FROM   generate_series(timestamp '2016-01-01', '2016-01-03', interval '1 day') d
   ) d
JOIN   accounts a ON a.create_date <= d.as_of_date
LEFT   JOIN LATERAL (
   SELECT balance
   FROM   balances
   WHERE  user_id = a.user_id
   AND    as_of_date <= d.as_of_date
   ORDER  BY as_of_date DESC
   LIMIT  1
   ) b ON true
ORDER  BY a.user_id, d.as_of_date;

Trả về kết quả mong muốn của bạn - ngoại trừ đó as_of_datelà một thực tế date, không timestampgiống như trong ví dụ của bạn. Điều đó nên thích hợp hơn.

Người dùng đã được tạo, nhưng chưa có bất kỳ giao dịch nào, được liệt kê với số dư bằng 0. Bạn không xác định cách xử lý trường hợp góc.

Thay vì sử dụng timestampđầu vào cho generate_series():

Điều quan trọng đối với hiệu suất là bạn sao lưu điều này với chỉ mục nhiều màu:

CREATE INDEX balances_multi_idx ON balances (user_id, as_of_date DESC, balance);

Chúng tôi đã có một trường hợp tương tự trên SO chỉ trong tuần này:

Tìm thêm lời giải thích ở đó.

2. CROSS JOIN,, LEFT JOINchức năng cửa sổ

SELECT user_id
     , COALESCE(max(balance) OVER (PARTITION BY user_id, grp
                                   ORDER BY as_of_date), 0) AS balance
     , as_of_date
FROM  (
   SELECT a.user_id, b.balance, d.as_of_date
        , count(b.user_id) OVER (PARTITION BY user_id ORDER BY as_of_date) AS grp
   FROM   (
      SELECT d::date AS as_of_date  -- cast to date right away
      FROM   generate_series(timestamp '2016-01-01', '2016-01-03', interval '1 day') d
      ) d
   JOIN   accounts a ON a.create_date <= d.as_of_date
   LEFT   JOIN balances b USING (user_id, as_of_date)
   ) sub
ORDER  BY user_id, as_of_date;

Cùng một kết quả. Nếu bạn có chỉ mục nhiều màu được đề cập ở trên và có thể quét chỉ mục từ nó, thì giải pháp đầu tiên có lẽ là nhanh hơn.

Tính năng chính là số lượng giá trị đang chạy để tạo thành các nhóm. vì Count () không tính các giá trị NULL, tất cả các ngày không có số dư đều thuộc cùng một nhóm ( grp) làm số dư gần đây nhất. Sau đó sử dụng một đơn giản max()trên cùng một khung cửa sổ được mở rộng bằng cách grpsao chép số dư cuối cùng cho các khoảng trống lơ lửng.

Liên quan:


Tuyệt vời cảm ơn bạn. Sẽ kiểm tra nó vào thứ hai.
Shaun Scovil

1

Nếu sự cân bằng là đơn điệu tăng một cái gì đó như:

SELECT b.user_id, max(b.balance) as balance, d.as_of_date 
FROM GENERATE_SERIES('2016-01-01', '2016-01-03', '1 day'::INTERVAL) d (as_of_date)
LEFT JOIN balances b
    on b.as_of_date <= d.as_of_date
GROUP BY b.user_id, d.as_of_date    
ORDER BY b.user_id, d.as_of_date desc

nên làm. Vấn đề có lẽ dễ dàng hơn một chút đối với trường hợp chung nếu bạn có quyền truy cập vào các giao dịch riêng lẻ thay vì số dư mỗi ngày.


Tôi thực sự đang thực hiện một số bước khác để đến bàn trong ví dụ của mình, đại diện cho số dư cuối ngày cho mỗi tài khoản, vào mỗi ngày có một giao dịch. Không thể dựa vào số dư tăng dần theo thời gian hoặc bất cứ điều gì tương tự; nghĩ rằng số dư ngân hàng cho hàng ngàn người dùng.
Shaun Scovil
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.