Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision cdd986ac

Von G. Richardson vor fast 5 Jahren hinzugefügt

  • ID cdd986acf8d8575920edf3c4c5351dbc561a8e64
  • Vorgänger 543d7822
  • Nachfolger 52683706

Inventory Controller - Datenbankoptimierungen für mini_journal

Aus Datenbanksicht war das Inventory mini-journal eine Katastrophe.

Die trans_id Abfrage führte zu einem ersten Seq Scan auf der Tabelle inventory.
my $query = 'SELECT trans_id FROM inventory GROUP BY trans_id ORDER BY max(itime) DESC LIMIT 10';

Die Rose Manager Abfrage führte dann zu einem zweiten Seq Scan auf der Tabelle inventory:
$objs = SL::DB::Manager::Inventory->get_all(query => [ trans_id => \@ids ]) if @ids;

Das sind zwei Seq Scans auf eine Tabelle die in kivitendo recht groß werden
kann. Außerdem könnte in der Zwischenzeit ein neuer inventory-Eintrag
dazugekommen sein, der damit ignoriert würde.

Im Template wurde dann auf die Rose-Objekte von part, transfer_type, bin und
warehouse zugegriffen, und da diese noch nicht geladen wurden sorgt das im
Extremfall für 40 weitere Datenbankzugriffe.

Das ist alles schön kompakter perl Code, aber wie könnte man das aus
Datenbanksicht optimieren, also die Anzahl der Zugriffe verringern und nach
Möglichkeit Indexe benutzen?

Die Templatezugriffe können einfach durch ein with_objects verhindert werden:
with_objects => [ 'parts', 'trans_type', 'bin', 'bin.warehouse' ],

Statt einer separaten Abfrage für die trans_ids könnte man diese als eine
Unterabfrage in get_all einführen:
query => [ trans_id => [ \"$query" ] ]

Die get-all-Abfrage kann man aber noch weiter verbessern, indem man nach id
statt trans_id filtert, da es für id im Gegensatz zu trans_id schon einen Index
gibt. Das Ziel für die Unterabfrage sollte also sein, eine Liste von ids zu
bekommen, damit der Index benutzt wird und man sich den Seq Scan spart.

Das mini-Journal zeigt die letzten 10 Lagerbewegungen, die entweder
Einlagerungen, Auslagerungen oder Umlagerungen sein können. Im Fall von
Umlagerungen wären das 2 inventory-Einträge, ansonsten 1. Da wir nicht wissen,
wieviele Umlagerungen dabei sind, holen wir die letzten 20 Einträge, filtern
diese nach den letzten 10 trans_ids, und extrahieren daraus die inventory ids
(zwischen 10 und 20 ids). Die ursprüngliche Abfrage mit dem GROUP BY konnte
keinen index nutzen, da das ORDER BY auf max(itime) statt itime war. Durch das
"limit 20" werden zwar potentiell ein paar Zeilen zu viel geholt, dafür kann
man aber nun einen Index auf inventory(itime) setzen, der von der Abfrage auch
verwendet werden kann, und damit spart man sich auch den letzten Seq Scan auf
inventory.

create index if not exists inventory_itime_idx on inventory (itime);

Unterschiede anzeigen:

SL/Controller/Inventory.pm
20 20
use SL::Helper::Flash;
21 21
use SL::Controller::Helper::ReportGenerator;
22 22
use SL::Controller::Helper::GetModels;
23
use List::MoreUtils qw(uniq);
23 24

  
24 25
use English qw(-no_match_vars);
25 26

  
......
781 782
sub mini_journal {
782 783
  my ($self) = @_;
783 784

  
784
  # get last 10 transaction ids
785
  my $query = 'SELECT trans_id, max(itime) FROM inventory GROUP BY trans_id ORDER BY max(itime) DESC LIMIT 10';
786
  my @ids = selectall_array_query($::form, $::form->get_standard_dbh, $query);
785
  # We want to fetch the last 10 inventory events (inventory rows with the same trans_id)
786
  # To prevent a Seq Scan on inventory set an index on inventory.itime
787
  # Each event may have one (transfer_in/out) or two (transfer) inventory rows
788
  # So fetch the last 20, group by trans_id, limit to the last 10 trans_ids,
789
  # and then extract the inventory ids from those 10 trans_ids
790
  # By querying Inventory->get_all via the id instead of trans_id we can make
791
  # use of the existing index on id
787 792

  
788
  my $objs;
789
  $objs = SL::DB::Manager::Inventory->get_all(query => [ trans_id => \@ids ]) if @ids;
793
  # inventory ids of the most recent 10 inventory trans_ids
794
  my $query = <<SQL;
795
with last_inventories as (
796
   select id,
797
          trans_id,
798
          itime
799
     from inventory
800
 order by itime desc
801
    limit 20
802
),
803
grouped_ids as (
804
   select trans_id,
805
          array_agg(id) as ids
806
     from last_inventories
807
 group by trans_id
808
 order by max(itime)
809
     desc limit 10
810
)
811
select unnest(ids)
812
  from grouped_ids
813
 limit 20  -- so the planner knows how many ids to expect, the cte is an optimisation fence
814
SQL
790 815

  
791
  # at most 2 of them belong to a transaction and the qty determins in or out.
792
  # sort them for display
816
  my $objs  = SL::DB::Manager::Inventory->get_all(
817
    query        => [ id => [ \"$query" ] ],
818
    with_objects => [ 'parts', 'trans_type', 'bin', 'bin.warehouse' ], # prevent lazy loading in template
819
    sort_by      => 'itime DESC',
820
  );
821
  # remember order of trans_ids from query, for ordering hash later
822
  my @sorted_trans_ids = uniq map { $_->trans_id } @$objs;
823

  
824
  # at most 2 of them belong to a transaction and the qty determines in or out.
793 825
  my %transactions;
794 826
  for (@$objs) {
795 827
    $transactions{ $_->trans_id }{ $_->qty > 0 ? 'in' : 'out' } = $_;
796 828
    $transactions{ $_->trans_id }{base} = $_;
797 829
  }
798
  # and get them into order again
799
  my @sorted = map { $transactions{$_} } @ids;
830

  
831
  # because the inventory transactions were built in a hash, we need to sort the
832
  # hash by using the original sort order of the trans_ids
833
  my @sorted = map { $transactions{$_} } @sorted_trans_ids;
800 834

  
801 835
  return \@sorted;
802 836
}

Auch abrufbar als: Unified diff