diff --git a/api/internal/db/measurement.manual.go b/api/internal/db/measurement.manual.go index b5b67073..6e8d74c1 100644 --- a/api/internal/db/measurement.manual.go +++ b/api/internal/db/measurement.manual.go @@ -49,47 +49,6 @@ func (q *Queries) TimeseriesMeasurementCollectionGetForRange(ctx context.Context return mc, nil } -// TimeseriesMeasurementCollectionGetForRangeEXPERIMENTAL MinMax downsampling in SQL and LTTB in Go to avoid sending large amounts of data to the api -func (q *Queries) TimeseriesMeasurementCollectionGetForRangeEXPERIMENTAL(ctx context.Context, arg TimeseriesMeasurementCollectionGetForRangeParams) (MeasurementCollection, error) { - var mc MeasurementCollection - limitSql := "" - param := []any{arg.TimeseriesID, arg.EncodedTimeWindow, arg.Threshold} - if arg.Limit != 0 { - limitSql = " limit $4" - param = append(param, arg.Limit) - } - timeseriesMeasurementListForRangeEXPERIMENTAL := ` - select - ts.id timeseries_id, - t.mmt measurements - from timeseries ts - left join lateral ( - select minmax_downsample_jsonb_agg( - array( - select row("time", "value")::ts_measurement - from timeseries_measurement - where timeseries_id=ts.id - and "time" <@ $2::tstzrange - order by "time" asc - %s - ), - $3 - ) mmt ) t on true - where ts.id = $1 - ` + limitSql - rows, err := q.db.Query(ctx, timeseriesMeasurementListForRangeEXPERIMENTAL) - if err != nil { - return mc, err - } - mm, err := pgx.CollectRows[Measurement](rows, pgx.RowToStructByNameLax) - if err != nil { - return mc, err - } - mc.TimeseriesID = arg.TimeseriesID - mc.Items = LTTB(mm, arg.Threshold) - return mc, nil -} - type MeasurementGetter interface { getTime() time.Time getValue() float64 diff --git a/api/internal/db/timeseries_process.manual.go b/api/internal/db/timeseries_process.manual.go index 70ffa8ec..bad3f0e2 100644 --- a/api/internal/db/timeseries_process.manual.go +++ b/api/internal/db/timeseries_process.manual.go @@ -12,6 +12,7 @@ import ( "github.com/Knetic/govaluate" "github.com/USACE/instrumentation-api/api/v4/internal/timewindow" "github.com/google/uuid" + "github.com/jackc/pgx/v5" "github.com/tidwall/btree" ) @@ -213,25 +214,6 @@ func (q *Queries) ProcessMeasurementListDynamic(ctx context.Context, f ProcessMe return tss, nil } -// ProcessMeasurementListDynamicEXPERIMENTAL returns measurements for the timeseries specified in the filter downsampled using the MinMaxLTTB algorithm -func (q *Queries) ProcessMeasurementListDynamicEXPERIMENTAL(ctx context.Context, f ProcessMeasurementFilter, threshold int) (ProcessTimeseriesResponseCollection, error) { - tss, err := queryTimeseriesMeasurementsEXPERIMENTAL(ctx, q, f, threshold) - if err != nil { - return tss, err - } - var order locfOrder - if f.SortDesc { - order = locfOrderDesc - } else { - order = locfOrderAsc - } - tss, err = processLOCF(tss, order) - if err != nil { - return tss, err - } - return tss, nil -} - // collectAggregate creates a btree of all sorted times (key) and measurements (value; as variable map) from an array of Timeseries func collectAggregate(tss *ProcessTimeseriesResponseCollection) *btree.BTreeG[BTreeNode] { // Get unique set of all measurement times of timeseries dependencies for non-regularized values @@ -380,7 +362,16 @@ func processLOCF(tss ProcessTimeseriesResponseCollection, order locfOrder) (Proc } // queryTimeseriesMeasurements selects stored measurements and dependencies for computed measurements +// NOTE: this will soon be deprecated in favor of expressions which are incrementally computed and saved func queryTimeseriesMeasurements(ctx context.Context, q *Queries, f ProcessMeasurementFilter) (ProcessTimeseriesResponseCollection, error) { + tw := f.TimeWindow.Encode() + if f.TimeWindow.Lower == nil { + f.TimeWindow.Lower = &time.Time{} + } + if f.TimeWindow.Upper == nil { + tmp := time.Now().UTC() + f.TimeWindow.Upper = &tmp + } var filterSQL string var filterArg any // short circuiting before executing SQL query greatly improves query perfomance, @@ -393,11 +384,10 @@ func queryTimeseriesMeasurements(ctx context.Context, q *Queries, f ProcessMeasu filterSQL = `instrument_id=$1` filterArg = f.InstrumentID case f.InstrumentGroupID != nil: - filterSQL = ` - instrument_id = any( - SELECT instrument_id - FROM instrument_group_instruments - WHERE instrument_group_id=$1 + filterSQL = `instrument_id = any( + select igi.instrument_id + from instrument_group_instruments igi + where igi.instrument_group_id=$1 )` filterArg = f.InstrumentGroupID case len(f.InstrumentIDs) > 0: @@ -409,19 +399,6 @@ func queryTimeseriesMeasurements(ctx context.Context, q *Queries, f ProcessMeasu default: return nil, fmt.Errorf("must supply valid filter for timeseries_measurement query") } - params := []any{filterArg, f.TimeWindow.Encode()} - - var limitSql string - if f.Limit != 0 { - limitSql = " limit $3" - params = append(params, f.Limit) - } - - sortSql := "asc" - if f.SortDesc { - sortSql = "desc" - } - listTimeseriesMeasurments := ` with required_timeseries as ( ( @@ -435,24 +412,6 @@ func queryTimeseriesMeasurements(ctx context.Context, q *Queries, f ProcessMeasu from v_timeseries_dependency where ` + filterSQL + ` ) - ), next_low as ( - select nlm.timeseries_id as timeseries_id, json_build_object('time', nlm.time, 'value', m1.value)::text measurement - from ( - select timeseries_id, max("time") "time" - from timeseries_measurement - where timeseries_id in (select id from required_timeseries) and "time" < lower($2::tstzrange) - group by timeseries_id - ) nlm - inner join timeseries_measurement m1 on m1.time = nlm.time and m1.timeseries_id = nlm.timeseries_id - ), next_high as ( - select nhm.timeseries_id as timeseries_id, json_build_object('time', nhm.time, 'value', m2.value)::text measurement - from ( - select timeseries_id, min("time") "time" - from timeseries_measurement - where timeseries_id in (select id from required_timeseries) and "time" > upper($2::tstzrange) - group by timeseries_id - ) nhm - inner join timeseries_measurement m2 on m2.time = nhm.time and m2.timeseries_id = nhm.timeseries_id ) ( select @@ -460,20 +419,10 @@ func queryTimeseriesMeasurements(ctx context.Context, q *Queries, f ProcessMeasu ts.instrument_id, i.slug || '.' || ts.slug variable, false is_computed, - null formula, - coalesce(( - select json_agg(json_build_object('time', "time", 'value', value) order by time ` + sortSql + `)::text - from timeseries_measurement - where timeseries_id = rt.id and "time" <@ $2::tstzrange - ` + limitSql + ` - ), '[]'::text) measurements, - nl.measurement next_measurement_low, - nh.measurement next_measurement_high + null formula from required_timeseries rt inner join timeseries ts on ts.id = rt.id inner join instrument i on i.id = ts.instrument_id - left join next_low nl on nl.timeseries_id = rt.id - left join next_high nh on nh.timeseries_id = rt.id ) union all ( @@ -482,10 +431,7 @@ func queryTimeseriesMeasurements(ctx context.Context, q *Queries, f ProcessMeasu instrument_id, slug variable, true is_computed, - contents formula, - '[]'::text measurements, - null next_measurement_low, - null next_measurement_high + contents formula from v_timeseries_computed where ` + filterSQL + ` and contents is not null @@ -493,209 +439,179 @@ func queryTimeseriesMeasurements(ctx context.Context, q *Queries, f ProcessMeasu order by is_computed ` - rows, err := q.db.Query(ctx, listTimeseriesMeasurments, params...) + rows, err := q.db.Query(ctx, listTimeseriesMeasurments, filterArg) if err != nil { return make(ProcessTimeseriesResponseCollection, 0), fmt.Errorf("queryTimeseriesMeasurements %w", err) } - defer rows.Close() - tt := make([]ProcessTimeseries, 0) + arg1 := make([]MeasurementListBatchForTimeseriesRangeOrderParams, 0) + arg2 := make([]MeasurementListBatchNeighborsForTimeseriesRangeParams, 0) + for rows.Next() { t := ProcessTimeseries{ - Measurements: make([]ProcessMeasurement, 0), - TimeWindow: f.TimeWindow, + TimeWindow: f.TimeWindow, } - var mmStr string - var nlStr, nhStr *string - if err := rows.Scan( &t.TimeseriesID, &t.InstrumentID, &t.Variable, &t.IsComputed, &t.Formula, - &mmStr, - &nlStr, - &nhStr, ); err != nil { return nil, err } - if err := json.Unmarshal([]byte(mmStr), &t.Measurements); err != nil { - return nil, err + tt = append(tt, t) + if !t.IsComputed { + arg1 = append(arg1, MeasurementListBatchForTimeseriesRangeOrderParams{ + TimeseriesID: t.TimeseriesID, + TimeWindow: tw, + }) + arg2 = append(arg2, MeasurementListBatchNeighborsForTimeseriesRangeParams{ + TimeseriesID: t.TimeseriesID, + MinTime: *f.TimeWindow.Lower, + MaxTime: *f.TimeWindow.Upper, + }) } - if nlStr != nil { - if err := json.Unmarshal([]byte(*nlStr), &t.NextMeasurementLow); err != nil { - return nil, err - } + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("queryTimeseriesMeasurements %w", err) + } + + mmMap := make(map[uuid.UUID][]MeasurementListBatchForTimeseriesRangeOrderRow, len(arg1)) + q.MeasurementListBatchForTimeseriesRangeOrder(ctx, arg1, f.SortDesc).Query(func(i int, rr []MeasurementListBatchForTimeseriesRangeOrderRow, qerr error) { + if qerr != nil { + err = qerr + return } - if nhStr != nil { - if err := json.Unmarshal([]byte(*nhStr), &t.NextMeasurementHigh); err != nil { - return nil, err + mmMap[arg1[i].TimeseriesID] = rr + }) + if err != nil { + return nil, err + } + + nnMap := make(map[uuid.UUID][]MeasurementListBatchNeighborsForTimeseriesRangeRow, len(arg1)) + q.MeasurementListBatchNeighborsForTimeseriesRange(ctx, arg2).Query(func(i int, rr []MeasurementListBatchNeighborsForTimeseriesRangeRow, qerr error) { + if qerr != nil { + err = qerr + return + } + nnMap[arg2[i].TimeseriesID] = rr + }) + if err != nil { + return nil, err + } + + for i := range tt { + if tt[i].IsComputed { + continue + } + rawMm, exists := mmMap[tt[i].TimeseriesID] + if !exists { + continue + } + mm := make([]ProcessMeasurement, 0, len(rawMm)) + for _, m := range rawMm { + mm = append(mm, ProcessMeasurement{ + Time: m.Time, + Value: FloatNanInf(m.Value), + }) + } + tt[i].Measurements = mm + rawNn, exists := nnMap[tt[i].TimeseriesID] + if !exists { + continue + } + for _, n := range rawNn { + m := ProcessMeasurement{Time: n.Time, Value: FloatNanInf(n.Value)} + switch n.Kind { + case "last": + tt[i].NextMeasurementLow = &m + case "next": + tt[i].NextMeasurementHigh = &m } } - tt = append(tt, t) } return tt, nil } -// queryTimeseriesMeasurementsEXPERIMENTAL selects stored measurements and dependencies for computed measurements -func queryTimeseriesMeasurementsEXPERIMENTAL(ctx context.Context, q *Queries, f ProcessMeasurementFilter, threshold int) (ProcessTimeseriesResponseCollection, error) { - var filterSQL string - var filterArg any - // short circuiting before executing SQL query greatly improves query perfomance, - // rather than adding all parameters to the query with logical OR - switch { - case f.TimeseriesID != nil: - filterSQL = `id=$1` - filterArg = f.TimeseriesID - case f.InstrumentID != nil: - filterSQL = `instrument_id=$1` - filterArg = f.InstrumentID - case f.InstrumentGroupID != nil: - filterSQL = ` - instrument_id = any( - SELECT instrument_id - FROM instrument_group_instruments - WHERE instrument_group_id=$1 - )` - filterArg = f.InstrumentGroupID - case len(f.InstrumentIDs) > 0: - filterSQL = `instrument_id = any($1)` - filterArg = f.InstrumentIDs - case len(f.TimeseriesIDs) > 0: - filterSQL = `id = any($1)` - filterArg = f.TimeseriesIDs - default: - return nil, fmt.Errorf("must supply valid filter for timeseries_measurement query") - } +const measurementListBatchForTimeseriesRangeOrder = ` +select time, value +from timeseries_measurement +where timeseries_id = $1::uuid +and time <@ $2::tstzrange +` + +type MeasurementListBatchForTimeseriesRangeOrderBatchResults struct { + br pgx.BatchResults + tot int + closed bool +} - var limitSql string - params := []any{filterArg, f.TimeWindow.Encode(), threshold} - if f.Limit != 0 { - limitSql = " limit $4" - params = append(params, f.Limit) - } +type MeasurementListBatchForTimeseriesRangeOrderParams struct { + TimeseriesID uuid.UUID `json:"timeseries_id"` + TimeWindow string `json:"time_window"` +} - sortSql := "asc" - if f.SortDesc { - sortSql = "desc" - } +type MeasurementListBatchForTimeseriesRangeOrderRow struct { + Time time.Time `json:"time"` + Value float64 `json:"value"` +} - listTimeseriesMeasurments := ` - with required_timeseries as ( - ( - select id - from v_timeseries_stored - where ` + filterSQL + ` - ) - union all - ( - select dependency_timeseries_id as id - from v_timeseries_dependency - where ` + filterSQL + ` - ) - ), next_low as ( - select nlm.timeseries_id as timeseries_id, json_build_object('time', nlm.time, 'value', m1.value)::text measurement - from ( - select timeseries_id, max("time") "time" - from timeseries_measurement - where timeseries_id in (select id from required_timeseries) and "time" < $2 - group by timeseries_id - ) nlm - inner join timeseries_measurement m1 on m1.time = nlm.time and m1.timeseries_id = nlm.timeseries_id - ), next_high as ( - select nhm.timeseries_id as timeseries_id, json_build_object('time', nhm.time, 'value', m2.value)::text measurement - from ( - select timeseries_id, min("time") "time" - from timeseries_measurement - where timeseries_id in (select id from required_timeseries) and "time" > $3 - group by timeseries_id - ) nhm - inner join timeseries_measurement m2 on m2.time = nhm.time and m2.timeseries_id = nhm.timeseries_id - ) - ( - select - rt.id timeseries_id, - ts.instrument_id, - i.slug || '.' || ts.slug variable, - false is_computed, - null formula, - coalesce(minmax_downsample_jsonb_agg(array( - select row("time", "value")::ts_measurement - from timeseries_measurement - where timeseries_id = rt.id and "time" <@ $2::tstzrange - order by "time" ` + sortSql + ` - ` + limitSql + ` - ), $3)::text, '[]'::text) measurements, - nl.measurement next_measurement_low, - nh.measurement next_measurement_high - from required_timeseries rt - inner join timeseries ts on ts.id = rt.id - inner join instrument i on i.id = ts.instrument_id - left join next_low nl on nl.timeseries_id = rt.id - left join next_high nh on nh.timeseries_id = rt.id - ) - union all - ( - select - id timeseries_id, - instrument_id, - slug variable, - true is_computed, - contents formula, - '[]'::text measurements, - null next_measurement_low, - null next_measurement_high - from v_timeseries_computed - where ` + filterSQL + ` - and contents is not null - ) - order by is_computed +func (q *Queries) MeasurementListBatchForTimeseriesRangeOrder(ctx context.Context, arg []MeasurementListBatchForTimeseriesRangeOrderParams, desc bool) *MeasurementListBatchForTimeseriesRangeOrderBatchResults { + orderBy := ` + order by time asc ` - - rows, err := q.db.Query(ctx, listTimeseriesMeasurments, params...) - if err != nil { - return make(ProcessTimeseriesResponseCollection, 0), err + if desc { + orderBy = ` + order by time desc + ` } - defer rows.Close() - - tt := make([]ProcessTimeseries, 0) - for rows.Next() { - t := ProcessTimeseries{ - Measurements: make([]ProcessMeasurement, 0), - TimeWindow: f.TimeWindow, + batch := &pgx.Batch{} + for _, a := range arg { + vals := []any{ + a.TimeseriesID, + a.TimeWindow, } - var mmStr string - var nlStr, nhStr *string + batch.Queue(measurementListBatchForTimeseriesRangeOrder+orderBy, vals...) + } + br := q.db.SendBatch(ctx, batch) + return &MeasurementListBatchForTimeseriesRangeOrderBatchResults{br, len(arg), false} +} - if err := rows.Scan( - &t.TimeseriesID, - &t.InstrumentID, - &t.Variable, - &t.IsComputed, - &t.Formula, - &mmStr, - &nlStr, - &nhStr, - ); err != nil { - return nil, err - } - if err := json.Unmarshal([]byte(mmStr), &t.Measurements); err != nil { - return nil, err - } - if nlStr != nil { - if err := json.Unmarshal([]byte(*nlStr), &t.NextMeasurementLow); err != nil { - return nil, err +func (b *MeasurementListBatchForTimeseriesRangeOrderBatchResults) Query(f func(int, []MeasurementListBatchForTimeseriesRangeOrderRow, error)) { + defer b.br.Close() + for t := 0; t < b.tot; t++ { + items := []MeasurementListBatchForTimeseriesRangeOrderRow{} + if b.closed { + if f != nil { + f(t, items, ErrBatchAlreadyClosed) } + continue } - if nhStr != nil { - if err := json.Unmarshal([]byte(*nhStr), &t.NextMeasurementHigh); err != nil { - return nil, err + err := func() error { + rows, err := b.br.Query() + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var i MeasurementListBatchForTimeseriesRangeOrderRow + if err := rows.Scan(&i.Time, &i.Value); err != nil { + return err + } + items = append(items, i) } + return rows.Err() + }() + if f != nil { + f(t, items, err) } - tt = append(tt, t) } +} - return tt, nil +func (b *MeasurementListBatchForTimeseriesRangeOrderBatchResults) Close() error { + b.closed = true + return b.br.Close() } diff --git a/api/internal/handler/timeseries_process.go b/api/internal/handler/timeseries_process.go index 2f2a5fe5..16aedb3e 100644 --- a/api/internal/handler/timeseries_process.go +++ b/api/internal/handler/timeseries_process.go @@ -12,8 +12,7 @@ import ( ) type MeasurementListQueryParams struct { - Experimental bool `query:"expertimental"` - Threshold int `query:"threshold"` + Threshold int `query:"threshold"` TimeWindowQueryParams } @@ -70,7 +69,7 @@ func (h *ApiHandler) RegisterTimeseriesProcess(api huma.API) { Limit: input.Limit, CursorTime: cursorTime, SortDesc: input.SortDesc, - }, input.Experimental) + }) if err != nil { return nil, httperr.InternalServerError(fmt.Errorf("measurementListStored %w", err)) } @@ -79,7 +78,7 @@ func (h *ApiHandler) RegisterTimeseriesProcess(api huma.API) { TimeseriesID: &input.TimeseriesID.UUID, TimeWindow: tw, } - mrc, err := h.measurementListComputed(ctx, f, input.Threshold, input.Experimental) + mrc, err := h.measurementListComputed(ctx, f) if err != nil { return nil, httperr.InternalServerError(fmt.Errorf("measurementListComputed %w", err)) } @@ -183,7 +182,7 @@ func (h *ApiHandler) RegisterTimeseriesProcess(api huma.API) { InstrumentID: &input.InstrumentID.UUID, TimeWindow: input.TimeWindow, } - mm, err := h.measurementListComputed(ctx, f, input.Threshold, input.Experimental) + mm, err := h.measurementListComputed(ctx, f) if err != nil { return nil, httperr.InternalServerError(err) } @@ -209,7 +208,7 @@ func (h *ApiHandler) RegisterTimeseriesProcess(api huma.API) { InstrumentGroupID: &input.InstrumentGroupID.UUID, TimeWindow: input.TimeWindow, } - mm, err := h.measurementListComputed(ctx, f, input.Threshold, input.Experimental) + mm, err := h.measurementListComputed(ctx, f) if err != nil { return nil, httperr.InternalServerError(err) } @@ -235,7 +234,7 @@ func (h *ApiHandler) RegisterTimeseriesProcess(api huma.API) { InstrumentIDs: input.Body, TimeWindow: input.TimeWindow, } - mm, err := h.measurementListComputed(ctx, f, input.Threshold, input.Experimental) + mm, err := h.measurementListComputed(ctx, f) if err != nil { return nil, httperr.InternalServerError(err) } @@ -247,36 +246,18 @@ func (h *ApiHandler) RegisterTimeseriesProcess(api huma.API) { }) } -func (h *ApiHandler) measurementListStored(ctx context.Context, arg db.TimeseriesMeasurementCollectionGetForRangeParams, experimental bool) (db.MeasurementCollection, error) { - var mc db.MeasurementCollection - var err error - if experimental { - mc, err = h.DBService.TimeseriesMeasurementCollectionGetForRangeEXPERIMENTAL(ctx, arg) - if err != nil { - return mc, err - } - return mc, nil - } - mc, err = h.DBService.TimeseriesMeasurementCollectionGetForRange(ctx, arg) +func (h *ApiHandler) measurementListStored(ctx context.Context, arg db.TimeseriesMeasurementCollectionGetForRangeParams) (db.MeasurementCollection, error) { + mc, err := h.DBService.TimeseriesMeasurementCollectionGetForRange(ctx, arg) if err != nil { return mc, err } return mc, nil } -func (h *ApiHandler) measurementListComputed(ctx context.Context, arg db.ProcessMeasurementFilter, threshold int, experimental bool) (db.ProcessTimeseriesResponseCollection, error) { - var mrc db.ProcessTimeseriesResponseCollection - var err error - if experimental { - mrc, err = h.DBService.ProcessMeasurementListDynamicEXPERIMENTAL(ctx, arg, threshold) - if err != nil { - return mrc, err - } - } else { - mrc, err = h.DBService.ProcessMeasurementListDynamic(ctx, arg) - if err != nil { - return mrc, err - } +func (h *ApiHandler) measurementListComputed(ctx context.Context, arg db.ProcessMeasurementFilter) (db.ProcessTimeseriesResponseCollection, error) { + mrc, err := h.DBService.ProcessMeasurementListDynamic(ctx, arg) + if err != nil { + return mrc, err } return mrc, nil } diff --git a/api/migrations/schema/V1.54.00__add_indexes.sql b/api/migrations/schema/V1.54.00__add_indexes.sql new file mode 100644 index 00000000..6bba64a4 --- /dev/null +++ b/api/migrations/schema/V1.54.00__add_indexes.sql @@ -0,0 +1,88 @@ +create index if not exists idx_saa_opts_instrument_id on saa_opts (instrument_id); +create index if not exists idx_saa_segment_length_timeseries_id on saa_segment (length_timeseries_id); +create index if not exists idx_saa_segment_temp_timeseries_id on saa_segment (temp_timeseries_id); +create index if not exists idx_saa_segment_x_timeseries_id on saa_segment (x_timeseries_id); +create index if not exists idx_saa_segment_y_timeseries_id on saa_segment (y_timeseries_id); +create index if not exists idx_saa_segment_z_timeseries_id on saa_segment (z_timeseries_id); +create index if not exists idx_seis_opts_instrument_id on seis_opts (instrument_id); +create index if not exists idx_submittal_submittal_status_id on submittal (submittal_status_id); +create index if not exists idx_survey123_created_by on survey123 (created_by); +create index if not exists idx_survey123_project_id on survey123 (project_id); +create index if not exists idx_survey123_updated_by on survey123 (updated_by); +create index if not exists idx_survey123_equivalency_table_timeseries_id on survey123_equivalency_table (timeseries_id); +create index if not exists idx_survey123_payload_error_survey123_id on survey123_payload_error (survey123_id); +create index if not exists idx_timeseries_parameter_id on timeseries (parameter_id); +create index if not exists idx_timeseries_unit_id on timeseries (unit_id); +create index if not exists idx_timeseries_alert_status_alert_status_id on timeseries_alert_status (alert_status_id); +create index if not exists idx_timeseries_alert_status_created_by on timeseries_alert_status (created_by); +create index if not exists idx_unit_measure_id on unit (measure_id); +create index if not exists idx_unit_unit_family_id on unit (unit_family_id); +create index if not exists idx_uploader_config_created_by on uploader_config (created_by); +create index if not exists idx_uploader_config_project_id on uploader_config (project_id); +create index if not exists idx_uploader_config_updated_by on uploader_config (updated_by); +create index if not exists idx_uploader_config_mapping_timeseries_id on uploader_config_mapping (timeseries_id); +create index if not exists idx_plot_configuration_updated_by on plot_configuration (updated_by); +create index if not exists idx_alert_alert_status_id on alert (alert_status_id); +create index if not exists idx_alert_config_created_by on alert_config (created_by); +create index if not exists idx_alert_config_project_id on alert_config (project_id); +create index if not exists idx_alert_config_updated_by on alert_config (updated_by); +create index if not exists idx_alert_config_change_alert_config_id on alert_config_change (alert_config_id); +create index if not exists idx_alert_config_schedule_alert_config_id on alert_config_schedule (alert_config_id); +create index if not exists idx_alert_config_threshold_alert_config_id on alert_config_threshold (alert_config_id); +create index if not exists idx_aware_parameter_parameter_id on aware_parameter (parameter_id); +create index if not exists idx_aware_parameter_unit_id on aware_parameter (unit_id); +create index if not exists idx_aware_platform_instrument_id on aware_platform (instrument_id); +create index if not exists idx_collection_group_created_by on collection_group (created_by); +create index if not exists idx_collection_group_updated_by on collection_group (updated_by); +create index if not exists idx_datalogger_created_by on datalogger (created_by); +create index if not exists idx_datalogger_project_id on datalogger (project_id); +create index if not exists idx_datalogger_updated_by on datalogger (updated_by); +create index if not exists idx_datalogger_equivalency_table_instrument_id on datalogger_equivalency_table (instrument_id); +create index if not exists idx_datalogger_equivalency_table_datalogger_id on datalogger_equivalency_table (datalogger_id); +create index if not exists idx_datalogger_error_datalogger_id on datalogger_error (datalogger_id); +create index if not exists idx_datalogger_error_datalogger_table_id on datalogger_error (datalogger_table_id); +create index if not exists idx_district_division_id on district (division_id); +create index if not exists idx_division_agency_id on division (agency_id); +create index if not exists idx_evaluation_created_by on evaluation (created_by); +create index if not exists idx_evaluation_project_id on evaluation (project_id); +create index if not exists idx_evaluation_submittal_id on evaluation (submittal_id); +create index if not exists idx_evaluation_updated_by on evaluation (updated_by); +create index if not exists idx_evaluation_instrument_evaluation_id on evaluation_instrument (evaluation_id); +create index if not exists idx_evaluation_instrument_instrument_id on evaluation_instrument (instrument_id); +create index if not exists idx_incl_opts_instrument_id on incl_opts (instrument_id); +create index if not exists idx_incl_segment_a0_timeseries_id on incl_segment (a0_timeseries_id); +create index if not exists idx_incl_segment_a180_timeseries_id on incl_segment (a180_timeseries_id); +create index if not exists idx_incl_segment_b0_timeseries_id on incl_segment (b0_timeseries_id); +create index if not exists idx_incl_segment_b180_timeseries_id on incl_segment (b180_timeseries_id); +create index if not exists idx_incl_segment_depth_timeseries_id on incl_segment (depth_timeseries_id); +create index if not exists idx_instrument_created_by on instrument (created_by); +create index if not exists idx_instrument_type_id on instrument (type_id); +create index if not exists idx_instrument_updated_by on instrument (updated_by); +create index if not exists idx_instrument_group_created_by on instrument_group (created_by); +create index if not exists idx_instrument_group_updated_by on instrument_group (updated_by); +create index if not exists idx_instrument_note_created_by on instrument_note (created_by); +create index if not exists idx_instrument_note_instrument_id on instrument_note (instrument_id); +create index if not exists idx_instrument_note_updated_by on instrument_note (updated_by); +create index if not exists idx_instrument_status_status_id on instrument_status (status_id); +create index if not exists idx_instrument_telemetry_telemetry_type_id on instrument_telemetry (telemetry_type_id); +create index if not exists idx_ipi_opts_instrument_id on ipi_opts (instrument_id); +create index if not exists idx_ipi_segment_inc_dev_timeseries_id on ipi_segment (inc_dev_timeseries_id); +create index if not exists idx_ipi_segment_length_timeseries_id on ipi_segment (length_timeseries_id); +create index if not exists idx_ipi_segment_temp_timeseries_id on ipi_segment (temp_timeseries_id); +create index if not exists idx_ipi_segment_tilt_timeseries_id on ipi_segment (tilt_timeseries_id); +create index if not exists idx_plot_bullseye_config_x_axis_timeseries_id on plot_bullseye_config (x_axis_timeseries_id); +create index if not exists idx_plot_bullseye_config_y_axis_timeseries_id on plot_bullseye_config (y_axis_timeseries_id); +create index if not exists idx_plot_configuration_created_by on plot_configuration (created_by); +create index if not exists idx_plot_configuration_custom_shape_plot_configuration_id on plot_configuration_custom_shape (plot_configuration_id); +create index if not exists idx_plot_profile_config_instrument_id on plot_profile_config (instrument_id); +create index if not exists idx_profile_login_activity_profile_id on profile_login_activity (profile_id); +create index if not exists idx_profile_project_roles_granted_by on profile_project_roles (granted_by); +create index if not exists idx_profile_token_profile_id on profile_token (profile_id); +create index if not exists idx_project_created_by on project (created_by); +create index if not exists idx_project_district_id on project (district_id); +create index if not exists idx_project_updated_by on project (updated_by); +create index if not exists idx_report_config_created_by on report_config (created_by); +create index if not exists idx_report_config_project_id on report_config (project_id); +create index if not exists idx_report_config_updated_by on report_config (updated_by); +create index if not exists idx_report_download_job_created_by on report_download_job (created_by); +create index if not exists idx_report_download_job_report_config_id on report_download_job (report_config_id);