sql >> Databasteknik >  >> RDS >> Oracle

SQL:Optimera BETWEEN-satsen

Detta är ett ganska vanligt problem.

Vanligt B-Tree index är inte bra för frågor som denna:

SELECT  measures.measure as measure,
        measures.time as time,
        intervals.entry_time as entry_time,
        intervals.exit_time as exit_time
FROM    intervals
JOIN    measures
ON      measures.time BETWEEN intervals.entry_time AND intervals.exit_time
ORDER BY
        time ASC

Ett index är bra för att söka efter värden inom de givna gränserna, så här:

, men inte för att söka efter gränserna som innehåller det givna värdet, så här:

Den här artikeln i min blogg förklarar problemet mer detaljerat:

(modellen med kapslade uppsättningar behandlar liknande typ av predikat).

Du kan göra indexet på time , på detta sätt intervals kommer att vara ledande i sammanfogningen, kommer tidsintervallet att användas inuti de kapslade looparna. Detta kräver sortering på time .

Du kan skapa ett rumsligt index på intervals (tillgänglig i MySQL med MyISAM lagring) som skulle inkludera start och end i en geometrikolumn. På det här sättet measures kan leda i sammanfogningen och ingen sortering kommer att behövas.

De rumsliga indexen är dock långsammare, så detta kommer bara att vara effektivt om du har få mått men många intervall.

Eftersom du har få intervall men många mått, se bara till att du har ett index på measures.time :

CREATE INDEX ix_measures_time ON measures (time)

Uppdatering:

Här är ett exempelskript att testa:

BEGIN
        DBMS_RANDOM.seed(20091223);
END;
/

CREATE TABLE intervals (
        entry_time NOT NULL,
        exit_time NOT NULL
)
AS
SELECT  TO_DATE('23.12.2009', 'dd.mm.yyyy') - level,
        TO_DATE('23.12.2009', 'dd.mm.yyyy') - level + DBMS_RANDOM.value
FROM    dual
CONNECT BY
        level <= 1500
/

CREATE UNIQUE INDEX ux_intervals_entry ON intervals (entry_time)
/

CREATE TABLE measures (
        time NOT NULL,
        measure NOT NULL
)
AS
SELECT  TO_DATE('23.12.2009', 'dd.mm.yyyy') - level / 720,
        CAST(DBMS_RANDOM.value * 10000 AS NUMBER(18, 2))
FROM    dual
CONNECT BY
        level <= 1080000
/

ALTER TABLE measures ADD CONSTRAINT pk_measures_time PRIMARY KEY (time)
/

CREATE INDEX ix_measures_time_measure ON measures (time, measure)
/

Denna fråga:

SELECT  SUM(measure), AVG(time - TO_DATE('23.12.2009', 'dd.mm.yyyy'))
FROM    (
        SELECT  *
        FROM    (
                SELECT  /*+ ORDERED USE_NL(intervals measures) */
                        *
                FROM    intervals
                JOIN    measures
                ON      measures.time BETWEEN intervals.entry_time AND intervals.exit_time
                ORDER BY
                        time
                )
        WHERE   rownum <= 500000
        )

använder NESTED LOOPS och returnerar i 1.7 sekunder.

Denna fråga:

SELECT  SUM(measure), AVG(time - TO_DATE('23.12.2009', 'dd.mm.yyyy'))
FROM    (
        SELECT  *
        FROM    (
                SELECT  /*+ ORDERED USE_MERGE(intervals measures) */
                        *
                FROM    intervals
                JOIN    measures
                ON      measures.time BETWEEN intervals.entry_time AND intervals.exit_time
                ORDER BY
                        time
                )
        WHERE   rownum <= 500000
        )

använder MERGE JOIN och jag var tvungen att stoppa det efter 5 minuter.

Uppdatering 2:

Du kommer med största sannolikhet att behöva tvinga motorn att använda rätt tabellordning i joinningen med hjälp av ett tips så här:

SELECT  /*+ LEADING (intervals) USE_NL(intervals, measures) */
        measures.measure as measure,
        measures.time as time,
        intervals.entry_time as entry_time,
        intervals.exit_time as exit_time
FROM    intervals
JOIN    measures
ON      measures.time BETWEEN intervals.entry_time AND intervals.exit_time
ORDER BY
        time ASC

Oracle s optimizer är inte tillräckligt smart för att se att intervallen inte skär varandra. Det är därför den med största sannolikhet kommer att använda measures som en ledande tabell (vilket skulle vara ett klokt beslut om intervallen skär varandra).

Uppdatering 3:

WITH    splits AS
        (
        SELECT  /*+ MATERIALIZE */
                entry_range, exit_range,
                exit_range - entry_range + 1 AS range_span,
                entry_time, exit_time
        FROM    (
                SELECT  TRUNC((entry_time - TO_DATE(1, 'J')) * 2) AS entry_range,
                        TRUNC((exit_time - TO_DATE(1, 'J')) * 2) AS exit_range,
                        entry_time,
                        exit_time
                FROM    intervals
                )
        ),
        upper AS
        (
        SELECT  /*+ MATERIALIZE */
                MAX(range_span) AS max_range
        FROM    splits
        ),
        ranges AS
        (
        SELECT  /*+ MATERIALIZE */
                level AS chunk
        FROM    upper
        CONNECT BY
                level <= max_range
        ),
        tiles AS
        (
        SELECT  /*+ MATERIALIZE USE_MERGE (r s) */
                entry_range + chunk - 1 AS tile,
                entry_time,
                exit_time
        FROM    ranges r
        JOIN    splits s
        ON      chunk <= range_span
        )
SELECT  /*+ LEADING(t) USE_HASH(m t) */
        SUM(LENGTH(stuffing))
FROM    tiles t
JOIN    measures m
ON      TRUNC((m.time - TO_DATE(1, 'J')) * 2) = tile
        AND m.time BETWEEN t.entry_time AND t.exit_time

Den här frågan delar upp tidsaxeln i intervallen och använder en HASH JOIN för att sammanfoga måtten och tidsstämplarna på intervallvärdena, med finfiltrering senare.

Se den här artikeln i min blogg för mer detaljerade förklaringar om hur det fungerar:



  1. MySQL-felkod:1193. Okänd systemvariabel

  2. mysql proxy strumpor

  3. Olaglig blandning av sammanställningar (utf8mb4_unicode_ci,IMPLICIT) och (utf8mb4_general_ci,IMPLICIT) för operation '='

  4. Hur man uppdaterar från select med en Join