PostgreSQL
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> PostgreSQL

Làm thế nào để bạn làm toán ngày mà bỏ qua năm?

Nếu bạn không quan tâm đến lời giải thích và chi tiết, hãy sử dụng "Phiên bản ma thuật đen" bên dưới.

Tất cả các truy vấn được trình bày trong các câu trả lời khác cho đến nay đều hoạt động với các điều kiện không thể phân tích - họ không thể sử dụng một chỉ mục và phải tính một biểu thức cho mọi hàng đơn lẻ trong bảng cơ sở để tìm các hàng phù hợp. Không thành vấn đề với bàn nhỏ. Các vấn đề ( rất nhiều ) với các bảng lớn.

Cho bảng đơn giản sau:

CREATE TABLE event (
  event_id   serial PRIMARY KEY
, event_date date
);

Truy vấn

Phiên bản 1. và 2. dưới đây có thể sử dụng một chỉ mục đơn giản của biểu mẫu:

CREATE INDEX event_event_date_idx ON event(event_date);

Nhưng tất cả các giải pháp sau đây thậm chí còn nhanh hơn mà không cần lập chỉ mục .

1. Phiên bản đơn giản

SELECT *
FROM  (
   SELECT ((current_date + d) - interval '1 year' * y)::date AS event_date
   FROM       generate_series( 0,  14) d
   CROSS JOIN generate_series(13, 113) y
   ) x
JOIN  event USING (event_date);

Truy vấn con x tính toán tất cả các ngày có thể có trong một phạm vi năm nhất định từ CROSS JOIN trong số hai generate_series() cuộc gọi. Việc lựa chọn được thực hiện với phép nối đơn giản cuối cùng.

2. Phiên bản nâng cao

WITH val AS (
   SELECT extract(year FROM age(current_date + 14, min(event_date)))::int AS max_y
        , extract(year FROM age(current_date,      max(event_date)))::int AS min_y
   FROM   event
   )
SELECT e.*
FROM  (
   SELECT ((current_date + d.d) - interval '1 year' * y.y)::date AS event_date
   FROM   generate_series(0, 14) d
        ,(SELECT generate_series(min_y, max_y) AS y FROM val) y
   ) x
JOIN  event e USING (event_date);

Phạm vi năm được suy ra từ bảng tự động - do đó giảm thiểu số năm được tạo.
Bạn có thể tiến thêm một bước nữa và chắt lọc danh sách các năm hiện có nếu có khoảng cách.

Hiệu quả phụ thuộc vào sự phân bố của ngày tháng. Vài năm với nhiều hàng mỗi làm cho giải pháp này hữu ích hơn. Nhiều năm, mỗi hàng có ít hàng khiến nó trở nên kém hữu ích hơn.

SQL Fiddle đơn giản để chơi với.

3. Phiên bản ma thuật đen

Đã cập nhật năm 2016 để xóa "cột đã tạo", cột này sẽ chặn H.O.T. các bản cập nhật; chức năng đơn giản và nhanh hơn.
Cập nhật năm 2018 để tính MMDD với IMMUTABLE biểu thức cho phép nội dòng hàm.

Tạo một hàm SQL đơn giản để tính toán một số nguyên integer từ mẫu 'MMDD' :

CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';

Tôi đã có to_char(time, 'MMDD') lúc đầu, nhưng đã chuyển sang biểu thức trên, được chứng minh là nhanh nhất trong các thử nghiệm mới trên Postgres 9.6 và 10:

db <> fiddle here

Nó cho phép nội dòng hàm vì EXTRACT (xyz FROM date) được triển khai với IMMUTABLE function date_part(text, date) trong nội bộ. Và nó phải là IMMUTABLE để cho phép sử dụng nó trong chỉ mục biểu thức đa cột cần thiết sau:

CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);

Đa cột vì một số lý do:
Có thể trợ giúp với ORDER BY hoặc với sự lựa chọn từ những năm nhất định. Đọc ở đây. Hầu như không có chi phí bổ sung cho chỉ mục. Một date vừa với 4 byte mà nếu không sẽ bị mất phần đệm do căn chỉnh dữ liệu. Đọc ở đây.
Ngoài ra, vì cả hai cột chỉ mục đều tham chiếu đến cùng một cột trong bảng, không có nhược điểm nào đối với H.O.T. các bản cập nhật. Đọc ở đây.

Một hàm bảng PL / pgSQL để thống trị tất cả chúng

Chuyển sang một trong hai truy vấn để thực hiện đầu năm:

CREATE OR REPLACE FUNCTION f_anniversary(date = current_date, int = 14)
  RETURNS SETOF event AS
$func$
DECLARE
   d  int := f_mmdd($1);
   d1 int := f_mmdd($1 + $2 - 1);  -- fix off-by-1 from upper bound
BEGIN
   IF d1 > d THEN
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) BETWEEN d AND d1
      ORDER  BY f_mmdd(e.event_date), e.event_date;

   ELSE  -- wrap around end of year
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) >= d OR
             f_mmdd(e.event_date) <= d1
      ORDER  BY (f_mmdd(e.event_date) >= d) DESC, f_mmdd(e.event_date), event_date;
      -- chronological across turn of the year
   END IF;
END
$func$  LANGUAGE plpgsql;

Gọi sử dụng mặc định:14 ngày bắt đầu từ "hôm nay":

SELECT * FROM f_anniversary();

Gọi trong 7 ngày bắt đầu từ '2014-08-23':

SELECT * FROM f_anniversary(date '2014-08-23', 7);

SQL Fiddle so sánh EXPLAIN ANALYZE .

Ngày 29 tháng 2

Khi xử lý các ngày kỷ niệm hoặc "sinh nhật", bạn cần xác định cách xử lý trường hợp đặc biệt "ngày 29 tháng 2" trong các năm nhuận.

Khi kiểm tra phạm vi ngày, Feb 29 thường được đưa vào tự động, ngay cả khi năm hiện tại không phải năm nhuận . Phạm vi ngày được kéo dài thêm 1 ngày trở về trước khi nó bao gồm ngày này.
Mặt khác, nếu năm hiện tại là năm nhuận và bạn muốn tìm kiếm trong 15 ngày, bạn có thể nhận được kết quả cho 14 ngày trong năm nhuận nếu dữ liệu của bạn là từ những năm không nhuận.

Giả sử, Bob sinh vào ngày 29 tháng 2:
Truy vấn 1. và 2. của tôi chỉ bao gồm ngày 29 tháng 2 trong những năm nhuận. Bob chỉ có sinh nhật ~ 4 năm một lần.
Truy vấn của tôi 3. bao gồm ngày 29 tháng 2 trong phạm vi. Bob có sinh nhật hàng năm.

Không có giải pháp kỳ diệu nào. Bạn phải xác định những gì bạn muốn cho mọi trường hợp.

Kiểm tra

Để chứng minh quan điểm của mình, tôi đã chạy một thử nghiệm mở rộng với tất cả các giải pháp được trình bày. Tôi đã điều chỉnh từng truy vấn cho phù hợp với bảng nhất định và để mang lại kết quả giống hệt nhau mà không cần ORDER BY .

Tin tốt là tất cả chúng đều chính xác và mang lại kết quả tương tự - ngoại trừ truy vấn của Gordon có lỗi cú pháp và truy vấn của @ wildplasser không thành công khi hết năm (dễ sửa).

Chèn 108000 hàng với các ngày ngẫu nhiên từ thế kỷ 20, tương tự như một bảng gồm những người sống (13 tuổi trở lên).

INSERT INTO  event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM   generate_series (1, 108000);

Xóa ~ 8% để tạo một số bộ giá trị chết và làm cho bảng trở nên "đời thực" hơn.

DELETE FROM event WHERE random() < 0.08;
ANALYZE event;

Trường hợp thử nghiệm của tôi có 99289 hàng, 4012 lần truy cập.

C - Catcall

WITH anniversaries as (
   SELECT event_id, event_date
         ,(event_date + (n || ' years')::interval)::date anniversary
   FROM   event, generate_series(13, 113) n
   )
SELECT event_id, event_date -- count(*)   --
FROM   anniversaries
WHERE  anniversary BETWEEN current_date AND current_date + interval '14' day;

C1 - Ý tưởng của Catcall được viết lại

Ngoài những tối ưu hóa nhỏ, sự khác biệt chính là thêm chỉ số năm chính xác date_trunc('year', age(current_date + 14, event_date)) để có được lễ kỷ niệm năm nay, điều này tránh hoàn toàn nhu cầu về CTE:

SELECT event_id, event_date
FROM   event
WHERE (event_date + date_trunc('year', age(current_date + 14, event_date)))::date
       BETWEEN current_date AND current_date + 14;

D - Daniel

SELECT *   -- count(*)   -- 
FROM   event
WHERE  extract(month FROM age(current_date + 14, event_date))  = 0
AND    extract(day   FROM age(current_date + 14, event_date)) <= 14;

E1 - Erwin 1

Xem "1. Phiên bản đơn giản" ở trên.

E2 - Erwin 2

Xem "2. Phiên bản nâng cao" ở trên.

E3 - Erwin 3

Xem "3. Phiên bản ma thuật đen" ở trên.

G - Gordon

SELECT * -- count(*)   
FROM  (SELECT *, to_char(event_date, 'MM-DD') AS mmdd FROM event) e
WHERE  to_date(to_char(now(), 'YYYY') || '-'
                 || (CASE WHEN mmdd = '02-29' THEN '02-28' ELSE mmdd END)
              ,'YYYY-MM-DD') BETWEEN date(now()) and date(now()) + 14;

H - a_horse_with_no_name

WITH upcoming as (
   SELECT event_id, event_date
         ,CASE 
            WHEN date_trunc('year', age(event_date)) = age(event_date)
                 THEN current_date
            ELSE cast(event_date + ((extract(year FROM age(event_date)) + 1)
                      * interval '1' year) AS date) 
          END AS next_event
   FROM event
   )
SELECT event_id, event_date
FROM   upcoming
WHERE  next_event - current_date  <= 14;

W - bộ phân vùng đại diện

CREATE OR REPLACE FUNCTION this_years_birthday(_dut date) RETURNS date AS
$func$
DECLARE
    ret date;
BEGIN
    ret :=
    date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
         - date_trunc( 'year' , _dut));
    RETURN ret;
END
$func$ LANGUAGE plpgsql;

Đơn giản hóa để trả về giống như tất cả những cái khác:

SELECT *
FROM   event e
WHERE  this_years_birthday( e.event_date::date )
        BETWEEN current_date
        AND     current_date + '2weeks'::interval;

W1 - truy vấn của wildplasser được viết lại

Ở trên mắc phải một số chi tiết không hiệu quả (ngoài phạm vi của bài đăng đã khá lớn này). Phiên bản viết lại là nhiều nhanh hơn:

CREATE OR REPLACE FUNCTION this_years_birthday(_dut INOUT date) AS
$func$
SELECT (date_trunc('year', now()) + ($1 - date_trunc('year', $1)))::date
$func$ LANGUAGE sql;

SELECT *
FROM   event e
WHERE  this_years_birthday(e.event_date)
        BETWEEN current_date
        AND    (current_date + 14);

Kết quả kiểm tra

Tôi đã chạy thử nghiệm này với một bảng tạm thời trên PostgreSQL 9.1.7. Kết quả được thu thập bằng EXPLAIN ANALYZE , tốt nhất trong số 5. ​​

Kết quả

Without index
C:  Total runtime: 76714.723 ms
C1: Total runtime:   307.987 ms  -- !
D:  Total runtime:   325.549 ms
E1: Total runtime:   253.671 ms  -- !
E2: Total runtime:   484.698 ms  -- min() & max() expensive without index
E3: Total runtime:   213.805 ms  -- !
G:  Total runtime:   984.788 ms
H:  Total runtime:   977.297 ms
W:  Total runtime:  2668.092 ms
W1: Total runtime:   596.849 ms  -- !

With index
E1: Total runtime:    37.939 ms  --!!
E2: Total runtime:    38.097 ms  --!!

With index on expression
E3: Total runtime:    11.837 ms  --!!

Tất cả các truy vấn khác hoạt động giống nhau khi có hoặc không có chỉ mục bởi vì chúng sử dụng không phân chia được biểu thức.

Conclusio

  • Cho đến nay, truy vấn của @ Daniel là nhanh nhất.

  • Phương pháp @wildplassers (được viết lại) cũng hoạt động ở mức có thể chấp nhận được.

  • Phiên bản của @ Catcall giống như cách tiếp cận ngược của tôi. Tuy nhiên, hiệu suất nhanh chóng vượt khỏi tầm tay với các bảng lớn hơn.
    Tuy nhiên, phiên bản viết lại hoạt động khá tốt. Biểu thức tôi sử dụng giống như một phiên bản đơn giản hơn của this_years_birthday() của @ wildplassser chức năng.

  • "Phiên bản đơn giản" của tôi nhanh hơn ngay cả khi không có chỉ mục , bởi vì nó cần ít tính toán hơn.

  • Với chỉ mục, "phiên bản nâng cao" nhanh bằng "phiên bản đơn giản", vì min()max() trở nên rất rẻ với một chỉ số. Cả hai đều nhanh hơn đáng kể so với phần còn lại không thể sử dụng chỉ mục.

  • "Phiên bản ma thuật đen" của tôi nhanh nhất có hoặc không có chỉ mục . Và nó rất đơn giản để gọi.

  • Với bảng ngoài đời thực, một chỉ mục sẽ làm cho thậm chí còn tuyệt vời hơn Sự khác biệt. Nhiều cột hơn làm cho bảng lớn hơn và quét tuần tự đắt hơn, trong khi kích thước chỉ mục vẫn giữ nguyên.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Khung khả dụng cao PostgreSQL tốt nhất là gì? PAF so với repmgr và Patroni Infographic

  2. PostgreSql CHÈN TỪ CHỌN ID QUAY LẠI

  3. ĐẶT HÀNG ĐƯỢC lập chỉ mục theo LIMIT 1

  4. Định dạng cho chuỗi / URL kết nối PostgreSQL là gì?

  5. sê-ri trong postgres đang được tăng lên mặc dù tôi đã thêm vào cuộc xung đột không làm gì cả