Oracle
 sql >> Database >  >> RDS >> Oracle

Sql:ottimizzazione BETWEEN clausola

Questo è un problema abbastanza comune.

B-Tree semplice gli indici non vanno bene per le query come questa:

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

Un indice è utile per cercare i valori entro i limiti dati, come questo:

, ma non per cercare i limiti che contengono il valore dato, come questo:

Questo articolo nel mio blog spiega il problema in modo più dettagliato:

(il modello degli insiemi annidati si occupa del tipo simile di predicato).

Puoi fare l'indice su time , in questo modo gli intervals sarà all'inizio del join, il tempo con intervallo verrà utilizzato all'interno dei loop nidificati. Ciò richiederà l'ordinamento in base al time .

Puoi creare un indice spaziale su intervals (disponibile in MySQL utilizzando MyISAM storage) che includerebbe start e end in una colonna geometrica. In questo modo, measures può condurre nel join e non sarà necessario alcun ordinamento.

Gli indici spaziali, tuttavia, sono più lenti, quindi questo sarà efficiente solo se hai poche misure ma molti intervalli.

Dato che hai pochi intervalli ma molte misure, assicurati di avere un indice su measures.time :

CREATE INDEX ix_measures_time ON measures (time)

Aggiornamento:

Ecco uno script di esempio da testare:

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)
/

Questa domanda:

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
        )

utilizza NESTED LOOPS e ritorna in 1.7 secondi.

Questa domanda:

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
        )

utilizza MERGE JOIN e ho dovuto interromperlo dopo 5 minuti.

Aggiornamento 2:

Molto probabilmente dovrai forzare il motore a utilizzare l'ordine corretto delle tabelle nel join usando un suggerimento come questo:

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

L'Oracle L'ottimizzatore non è abbastanza intelligente da vedere che gli intervalli non si intersecano. Ecco perché molto probabilmente utilizzerà measures come tavolo principale (che sarebbe una decisione saggia se gli intervalli si intersecano).

Aggiornamento 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

Questa query suddivide l'asse temporale negli intervalli e utilizza un HASH JOIN per unire misure e timestamp sui valori dell'intervallo, con filtraggio fine in seguito.

Vedi questo articolo nel mio blog per spiegazioni più dettagliate su come funziona: