गिनती करने के लिए सबसे तेज़ तरीका है कि श्रृंखला से प्रत्येक तिथि कितनी तारीखों को कवर करती है


12

मेरे पास एक टेबल है (PostgreSQL 9.4 में) जो इस तरह दिखता है:

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');

अब मैं दी गई तारीखों के लिए और हर तरह के लिए गणना करना चाहता हूं कि dates_rangesप्रत्येक तिथि से कितनी पंक्तियां आती हैं। शून्य को संभवतः छोड़ा जा सकता है।

वांछित परिणाम:

+-------+------------+----+
|  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 |
+-------+------------+----+

मैं दो समाधान के साथ आया हूं, एक के साथ LEFT JOINऔर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

और एक LATERAL, जो थोड़ा तेज है:

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

मुझे आश्चर्य है कि क्या यह क्वेरी लिखने का कोई बेहतर तरीका है? और 0 गिनती के साथ जोड़े को किस तरह से शामिल किया जाए?

वास्तव में कुछ अलग प्रकार के होते हैं, पाँच साल (1800 तारीख) की अवधि, और ~ 30k पंक्तियों की dates_rangesतालिका में (लेकिन यह काफी बढ़ सकता है)।

कोई इंडेक्स नहीं हैं। मेरे मामले में सटीक होना यह सबक्वेरी का परिणाम है, लेकिन मैं प्रश्न को एक मुद्दे पर सीमित करना चाहता हूं, इसलिए यह अधिक सामान्य है।


यदि आप तालिका में श्रेणियाँ गैर-अतिव्यापी या स्पर्श कर रहे हैं तो आप क्या करते हैं। उदाहरण के लिए यदि आपके पास एक सीमा है जहाँ (प्रकार, प्रारंभ, अंत) = (1,2018-01-01,2018-01-15)और (1,2018-01-20,2018-01-25)क्या आप यह निर्धारित करना चाहते हैं कि आपके पास कितनी ओवरलैपिंग तिथियां हैं?
इवान कैरोल

मैं भी उलझन में हूं कि आपकी टेबल छोटी क्यों है? क्यों नहीं है 2018-01-31या 2018-01-30या 2018-01-29पहली श्रेणी उन सभी को है जब यह में?
इवान कैरोल

@EvanCarroll की तारीखें generate_seriesबाहरी पैरामीटर हैं - वे जरूरी नहीं कि सभी श्रेणियों को dates_rangesतालिका में शामिल करें। पहले सवाल के रूप में मुझे लगता है मैं इसे नहीं समझता - पंक्तियाँ dates_rangesस्वतंत्र हैं, मैं ओवरलैपिंग निर्धारित नहीं करना चाहता।
बार्टचेक

जवाबों:


4

निम्नलिखित क्वेरी भी काम करती है यदि "लापता शून्य" ठीक हैं:

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;

लेकिन यह lateralछोटे डेटासेट वाले संस्करण की तुलना में तेज़ नहीं है । यह बेहतर पैमाने पर हो सकता है, क्योंकि इसमें शामिल होने की आवश्यकता नहीं है, लेकिन उपरोक्त संस्करण सभी पंक्तियों पर एकत्रित होता है, इसलिए यह फिर से खो सकता है।

निम्नलिखित क्वेरी किसी भी तरह से ओवरलैप नहीं होने वाली किसी भी श्रृंखला को हटाकर अनावश्यक काम से बचने की कोशिश करती है:

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;

- और मैं overlapsऑपरेटर का उपयोग करने के लिए मिला ! ध्यान दें कि आपको interval '1 day'दाईं ओर जोड़ना है क्योंकि ओवरलैप्स ऑपरेटर समय अवधि को दाईं ओर खोलने पर विचार करता है (जो कि काफी तार्किक है क्योंकि एक तिथि को अक्सर मध्यरात्रि के समय घटक के साथ टाइमस्टैम्प माना जाता है)।


अच्छा, मुझे नहीं पता था कि generate_seriesइसका इस्तेमाल किया जा सकता है। कुछ परीक्षणों के बाद मेरे पास निम्नलिखित अवलोकन हैं। आपकी क्वेरी वास्तव में चयनित सीमा लंबाई के साथ वास्तव में अच्छी तरह से तराजू है - थेरेपी व्यावहारिक रूप से 3 साल और 10 साल की अवधि के बीच कोई अंतर नहीं है। हालांकि छोटी अवधि (1 वर्ष) के लिए मेरे समाधान तेज हैं - मैं अनुमान लगा रहा हूं कि इसका कारण यह है कि dates_ranges(2010-2100 की तरह) कुछ लंबी रेंज हैं, जो आपकी क्वेरी को धीमा कर रहे हैं। आंतरिक क्वेरी को सीमित करना start_dateऔर उसके end_dateअंदर मदद करना चाहिए। मुझे कुछ और परीक्षण करने की आवश्यकता है।
बार्टेक

6

और 0 गिनती के साथ जोड़े को किस तरह से शामिल किया जाए?

सभी संयोजनों की एक ग्रिड बनाएं, फिर LATERAL अपनी तालिका में शामिल हों, जैसे:

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;

जितना संभव हो उतना तेज होना चाहिए।

मेरे पास LEFT JOIN LATERAL ... on trueपहले से था , लेकिन उपश्रेणी में एक समुच्चय है c, इसलिए हमें हमेशा एक पंक्ति मिलती है और CROSS JOINसाथ ही उपयोग कर सकते हैं । प्रदर्शन में कोई अंतर नहीं।

यदि आपके पास सभी प्रासंगिक प्रकारों को रखने वाली तालिका है , तो उप-सूची के साथ सूची बनाने के बजाय उसका उपयोग करें k

कास्ट integerवैकल्पिक है। आपको मिलता है bigint

सूचकांक में मदद मिलेगी, विशेष रूप से एक बहुरंगी सूचकांक पर (kind, start_date, end_date)। जब से आप एक उपमहाद्वीप पर निर्माण कर रहे हैं, यह प्राप्त करना संभव है या नहीं हो सकता है।

सूची generate_series()में सेट-रिटर्निंग फ़ंक्शंस का उपयोग करना SELECTआमतौर पर 10 से पहले पोस्टग्रेज संस्करणों में उचित नहीं है (जब तक कि आपको पता नहीं है कि आप क्या कर रहे हैं)। देख:

यदि आपके पास कुछ या कुछ पंक्तियों के साथ बहुत सारे संयोजन हैं, तो यह समकक्ष रूप तेज़ हो सकता है:

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;

SELECTसूची में सेट-रिटर्न फ़ंक्शंस के लिए - मैंने पढ़ा है कि यह उचित नहीं है, हालांकि ऐसा लगता है कि यह ठीक काम करता है, अगर ऐसा केवल एक ही फ़ंक्शन है। अगर मुझे यकीन है कि केवल एक ही होगा, तो क्या कुछ गलत हो सकता है?
बार्टेकच

@BartekCh: SELECTसूची में एक एकल एसआरएफ उम्मीद के मुताबिक काम करता है। शायद दूसरे को जोड़ने के खिलाफ चेतावनी देने के लिए एक टिप्पणी जोड़ें। या FROMपोस्टग्रेज के पुराने संस्करणों के साथ शुरू करने के लिए इसे सूची में ले जाएं। जटिलताओं का जोखिम क्यों? (यह भी मानक एसक्यूएल है और अन्य RDBMS से आने वाले लोगों को भ्रमित नहीं करेगा।)
इरविन ब्रान्डस्टेट्टर

1

daterangeप्रकार का उपयोग करना

PostgreSQL में ए daterange। इसका उपयोग करना बहुत सरल है। आपके नमूना डेटा के साथ शुरू करके हम टेबल पर प्रकार का उपयोग करने के लिए आगे बढ़ते हैं।

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)

मैं दी गई तारीखों के लिए और हर तरह की तारीखों के लिए गणना करना चाहता हूं कि हर तारीख में तारीखों से कितनी पंक्तियां आती हैं।

अब इसे क्वेरी करने के लिए हम प्रक्रिया को उल्टा करते हैं, और एक दिनांक श्रृंखला उत्पन्न करते हैं , लेकिन यहाँ क्वेरी को पकड़ने के लिए ( @>) ऑपरेटर ऑपरेटर का उपयोग करके जाँच कर सकता है कि दिनांक एक सीमा का उपयोग कर रहे हैं

ध्यान दें कि हम timestamp without time zone(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;

इंडेक्स पर आइटम-डे-ओवरलैप्स है।

एक साइड बोनस के रूप में, डॉटरेंज प्रकार के साथ आप उन सीमाओं के सम्मिलन को रोक सकते हैं जो एक का उपयोग करके दूसरों के साथ ओवरलैप करते हैंEXCLUDE CONSTRAINT


आपकी क्वेरी में कुछ गड़बड़ है, ऐसा लगता है कि यह कई बार पंक्तियों की गिनती कर रहा है, एक JOINमुझे बहुत ज्यादा लगता है।
बार्टचेक

@BartekCh नहीं, आपके पास ओवरलैपिंग पंक्तियाँ हैं, आप ओवरलैपिंग रेंज (सुझाए गए) या उपयोग करके इसे चारों ओर प्राप्त कर सकते हैंcount(DISTINCT kind)
इवान कैरोल

लेकिन मैं ओवरलैपिंग पंक्तियों को चाहता हूं। उदाहरण के लिए दयालु 1तिथि 2018-01-01से पहले दो पंक्तियों के भीतर है dates_ranges, लेकिन आपकी क्वेरी देता है 8
बार्टेकच

याcount(DISTINCT kind) क्या आपने DISTINCTवहां कीवर्ड जोड़ा ?
इवान कैरोल

दुर्भाग्यवश DISTINCTकीवर्ड के साथ यह अभी भी अपेक्षा के अनुरूप काम नहीं करता है। यह हर तिथि के लिए अलग प्रकार की गणना करता है, लेकिन मैं हर तिथि के लिए प्रत्येक प्रकार की सभी पंक्तियों को गिनना चाहता हूं।
बार्टचेक
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.