J'ai une table (dans PostgreSQL 9.4) qui ressemble à ceci:
CREATE TABLE dates_ranges (kind int, start_date date, end_date date);
INSERT INTO dates_ranges VALUES
(1, '2018-01-01', '2018-01-31'),
(1, '2018-01-01', '2018-01-05'),
(1, '2018-01-03', '2018-01-06'),
(2, '2018-01-01', '2018-01-01'),
(2, '2018-01-01', '2018-01-02'),
(3, '2018-01-02', '2018-01-08'),
(3, '2018-01-05', '2018-01-10');
Maintenant, je veux calculer pour les dates données et pour chaque type, en combien de lignes de dates_ranges
chaque date tombe. Des zéros pourraient éventuellement être omis.
Résultat désiré:
+-------+------------+----+
| kind | as_of_date | n |
+-------+------------+----+
| 1 | 2018-01-01 | 2 |
| 1 | 2018-01-02 | 2 |
| 1 | 2018-01-03 | 3 |
| 2 | 2018-01-01 | 2 |
| 2 | 2018-01-02 | 1 |
| 3 | 2018-01-02 | 1 |
| 3 | 2018-01-03 | 1 |
+-------+------------+----+
J'ai trouvé deux solutions, une avec LEFT JOIN
et GROUP BY
SELECT
kind, as_of_date, COUNT(*) n
FROM
(SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates
LEFT JOIN
dates_ranges ON dates.as_of_date BETWEEN start_date AND end_date
GROUP BY 1,2 ORDER BY 1,2
et un avec LATERAL
, qui est légèrement plus rapide:
SELECT
kind, as_of_date, n
FROM
(SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates,
LATERAL
(SELECT kind, COUNT(*) AS n FROM dates_ranges WHERE dates.as_of_date BETWEEN start_date AND end_date GROUP BY kind) ss
ORDER BY kind, as_of_date
Je me demande si c'est une meilleure façon d'écrire cette requête? Et comment inclure des paires date-kind avec 0 compte?
En réalité, il existe plusieurs types distincts, une période pouvant aller jusqu'à cinq ans (1800 dates) et ~ 30 000 lignes dans dates_ranges
table (mais elle pourrait augmenter considérablement).
Il n'y a pas d'index. Pour être précis dans mon cas, c'est le résultat d'une sous-requête, mais j'ai voulu limiter la question à un seul problème, donc c'est plus général.
La requête suivante fonctionne également si les "zéros manquants" sont OK:
select *
from (
select
kind,
generate_series(start_date, end_date, interval '1 day')::date as d,
count(*)
from dates_ranges
group by 1, 2
) x
where d between date '2018-01-01' and date '2018-01-03'
order by 1, 2;
mais ce n'est pas plus rapide que la version lateral
avec le petit ensemble de données. Cependant, il peut évoluer mieux, car aucune jointure n'est requise, mais la version ci-dessus agrège sur toutes les lignes, il peut donc y perdre à nouveau.
La requête suivante tente d'éviter un travail inutile en supprimant toutes les séries qui ne se chevauchent pas de toute façon:
select
kind,
generate_series(greatest(start_date, date '2018-01-01'), least(end_date, date '2018-01-03'), interval '1 day')::date as d,
count(*)
from dates_ranges
where (start_date, end_date + interval '1 day') overlaps (date '2018-01-01', date '2018-01-03' + interval '1 day')
group by 1, 2
order by 1, 2;
- et j'ai pu utiliser l'opérateur overlaps
! Notez que vous devez ajouter interval '1 day'
vers la droite car l'opérateur de chevauchement considère que les périodes sont ouvertes sur la droite (ce qui est assez logique car une date est souvent considérée comme un horodatage avec une composante horaire de minuit).
Et comment inclure des paires date-kind avec 0 compte?
Construisez une grille de toutes les combinaisons puis LATERAL
rejoignez votre table, comme ceci:
SELECT k.kind, d.as_of_date, c.n
FROM (SELECT DISTINCT kind FROM dates_ranges) k
CROSS JOIN (
SELECT d::date AS as_of_date
FROM generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
) d
CROSS JOIN LATERAL (
SELECT count(*)::int AS n
FROM dates_ranges
WHERE kind = k.kind
AND d.as_of_date BETWEEN start_date AND end_date
) c
ORDER BY k.kind, d.as_of_date;
Devrait également être aussi rapide que possible.
J'avais LEFT JOIN LATERAL ... on true
Au début, mais il y a un agrégat dans la sous-requête c
, donc nous toujours obtenons une ligne et peut également utiliser CROSS JOIN
. Aucune différence de performance.
Si vous avez une table contenant tous les types pertinents , utilisez-le au lieu de générer la liste avec la sous-requête k
.
La conversion en integer
est facultative. Sinon, vous obtenez bigint
.
Les index seraient utiles, en particulier un index multicolonne sur (kind, start_date, end_date)
. Étant donné que vous construisez sur une sous-requête, cela peut être possible ou non.
L'utilisation de fonctions renvoyant un ensemble comme generate_series()
dans la liste SELECT
est généralement déconseillée dans les versions Postgres antérieures à 10 ( sauf si vous savez exactement ce que vous faites). Voir:
Si vous avez beaucoup de combinaisons avec peu ou pas de lignes, ce formulaire équivalent peut être plus rapide:
SELECT k.kind, d.as_of_date, count(dr.kind)::int AS n
FROM (SELECT DISTINCT kind FROM dates_ranges) k
CROSS JOIN (
SELECT d::date AS as_of_date
FROM generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
) d
LEFT JOIN dates_ranges dr ON dr.kind = k.kind
AND d.as_of_date BETWEEN dr.start_date AND dr.end_date
GROUP BY 1, 2
ORDER BY 1, 2;
daterange
PostgreSQL a un daterange
. Son utilisation est assez simple. À partir de vos exemples de données, nous allons utiliser le type sur la table.
BEGIN;
ALTER TABLE dates_ranges ADD COLUMN myrange daterange;
UPDATE dates_ranges
SET myrange = daterange(start_date, end_date, '[]');
ALTER TABLE dates_ranges
DROP COLUMN start_date,
DROP COLUMN end_date;
COMMIT;
-- Now you can create Gist index on it...
CREATE INDEX ON dates_ranges USING Gist (myrange);
TABLE dates_ranges;
kind | myrange
------+-------------------------
1 | [2018-01-01,2018-02-01)
1 | [2018-01-01,2018-01-06)
1 | [2018-01-03,2018-01-07)
2 | [2018-01-01,2018-01-02)
2 | [2018-01-01,2018-01-03)
3 | [2018-01-02,2018-01-09)
3 | [2018-01-05,2018-01-11)
(7 rows)
Je veux calculer pour les dates données et pour chaque type, en combien de lignes de dates_ranges chaque date tombe.
Maintenant, pour l'interroger, nous inversons la procédure et générer une série de dates mais voici le crochet que la requête elle-même peut utiliser le confinement (@>
) pour vérifier que les dates sont dans la plage, à l'aide d'un index.
Notez que nous utilisons timestamp without time zone
(pour arrêter les risques DST)
SELECT d1.kind, day::date, count(d2.kind)
FROM dates_ranges AS d1
CROSS JOIN LATERAL generate_series(
lower(myrange)::timestamp without time zone,
upper(myrange)::timestamp without time zone,
'1 day'
) AS gs(day)
INNER JOIN dates_ranges AS d2
ON d2.myrange @> day::date
GROUP BY d1.kind, day;
Quels sont les jours-chevauchements détaillés sur l'indice.
En bonus, avec le type daterange, vous pouvez arrêter insertions de plages qui se chevauchent avec d'autres en utilisant un EXCLUDE CONSTRAINT