'Creating queries using between for year, month and day in separate fields

I own a table with year, month and day in separate fields in Postgresql, but I need to make a query using 'between' these fields. I've tried some crazy things but nothing worked ... Does anyone know help me?

Ex table:

CREATE TABLE my_table
(
  id serial NOT NULL,
  day integer NOT NULL,
  month integer NOT NULL,
  year integer NOT NULL,
)


Solution 1:[1]

Cheat. You aren't validating these "dates", you are just sorting them. Validation is a separate task, and the only sane solution is a schema change.

CREATE INDEX my_table__date__idx ON test1 (year * 10000 + month * 100 + day);

SELECT *
FROM my_table
WHERE year * 10000 + month * 100 + day BETWEEN 20130101 AND 20131231;

If you don't want huge gaps, use year*12*31 + month*31 + day

Solution 2:[2]

Assistance for the dark side

While stuck with your design, this should be simpler and faster than any of the solutions proposed so far: ad-hoc row types:

SELECT *
FROM   tbl
WHERE  (year, month, day) BETWEEN (2013,7,3)
                          AND     (2013,7,5);

db<>fiddle here
Old sqlfiddle

Proper solution

But, really, you should use a proper table design to begin with. Save the date as date, not as three integer columns. That enforces valid dates, as collateral benefit.

CREATE TABLE tbl (
  tbl_id  serial PRIMARY KEY
, thedate date NOT NULL
);

thedate includes all three columns: day, month, year and needs 4 bytes like an integer.

Then the task is trivial:
To get day, month, year - or any sub-unit in any form, use to_char(), extract(), or date_trunc(). These functions are very fast and versatile.

You could create a VIEW that looks exactly like the table you have now:

CREATE view old_table AS
SELECT tbl_id
     , EXTRACT(day   FROM thedate)::int AS day
     , EXTRACT(month FROM thedate)::int AS month
     , EXTRACT(year  FROM thedate)::int AS year
FROM   my_table;

Would still be at least as fast as your table because the source is smaller.

Solution 3:[3]

WHERE
  (
       ((day >= :first_day) AND (month  = :first_month) AND (year  = :first_year))
    OR (                        (month >  :first_month) AND (year  = :first_year))
    OR (                                                    (year >  :first_year))
  )
  AND
  (
       ((day <= :final_day) AND (month  = :final_month) AND (year  = :final_year))
    OR (                        (month <  :final_month) AND (year  = :final_year))
    OR (                                                    (year <  :final_year))
  )

Which is really going to destroy any chance of an index seek. You will likely scan the whole table every time.

You're a million times better off looking to use a real date field.

I would never want to release the above code, never mind have a peer review it. I'd be mortified.

Solution 4:[4]

If you can't change the table structure to use proper dates, you can make dates from the fields for comparison, something like:

SELECT *
FROM Table
WHERE to_date(Year||' '||Day||' '||Month, 'YYYY DD MM') BETWEEN date1 AND date2

If they're all Integers you'll have to convert each to string, I believe ::VARCHAR() works, or CAST():

SELECT *
FROM Table
WHERE to_date(Year::varchar(4)||' '||Day::varchar(2)||' '||Month::varchar(2), 'YYYY DD MM') BETWEEN date1 AND date2

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 MatBailie
Solution 4