Performance grootboekmutaties SnelStart / query folding

Een aantal tabellen worden effectief geladen. Maar we blijven hangen op de grootboekmutaties. Nu zien we dat onze klant al een lange tijd Snelstart gebruikt. Vandaar dit probleem denken we.

Is er een mogelijkheid om de hoeveelheid data load via OData te beperken, of werkt het leggen van een filter op de datum voldoende. Oftewel: wordt query folding ondersteunt op deze manier?

Nee, “server-side filtering” zoals de Invantive UniversalSQL-variant van “query folding” tussen de Invantive UniversalSQL-engine en de platform API-server heet is momenteel bewust niet geïmplementeerd voor SnelStart.

Voor meer uitleg en geadviseerde vervolgstappen zie onderstaande tekst.

Uitdaging

SnelStart is gebaseerd op een Swagger / OpenAPI-specificatie die SnelStart bijhoudt.

SnelStart ondersteunt filters, zoals beschreven in https://b2bapi-developer.snelstart.nl/odata (vereist mogelijk aanmelden als ontwikkelaar).

Voor server-side filtering is het nodig om een filter-specificatie door te kunnen geven. Het onhandige is dat de filter-specificatie van SnelStart gebruik maakt van properties die case-sensitive zijn en in “UpperCamelCase”. De properties die SnelStart echter in de specificatie heeft staan zijn in een andere schrijfwijze. Deze andere schrijfwijze komt ook terug in de JSON.

Een voorbeeld aan de hand van artikelen:

{
  "isHoofdartikel": false,
  "subartikelen": [],
  "artikelcode": "VOORBEELD",
  "omschrijving": "Diversen diensten hoog",
  "artikelOmzetgroep": {
    "id": "11a35e1b-dce0-48ef-bf25-7480fb0563ff",
    "uri": "/artikelomzetgroepen/11a35e1b-dce0-48ef-bf25-7480fb0563ff"
  },
  "verkoopprijs": 0,
  "inkoopprijs": 0,
  "eenheid": null,
  "modifiedOn": "2023-06-29T21:44:48.51",
  "relatie": null,
  "isNonActief": false,
  "voorraadControle": false,
  "technischeVoorraad": 0,
  "vrijeVoorraad": 0,
  "extraVelden": [],
  "id": "d691991a-9c3c-45d0-b6d0-aae11727e981",
  "uri": "/artikelen/d691991a-9c3c-45d0-b6d0-aae11727e981"
}

Het gefilterd ophalen van een artikel kan met (native call):

https://b2bapi.snelstart.nl/v2/artikelen?$filter=Artikelcode ge ''VANAF''

Een alternatief is om de schrijfwijze artikelcode zoals in de JSON hierboven te gebruiken:

https://b2bapi.snelstart.nl/v2/artikelen?$filter=artikelcode ge ''VANAF''

Deze geeft een HTTP 400 Bad request:

itgenoda137
The SnelStart server cannot or will not process the request due to an apparent client error (e.g., malformed request syntax, size too large, invalid request message framing, or deceptive request routing).
itgenclr217
The remote server returned an error:
(400) Bad Request.

Aanpak

Zoals boven beschreven zouden daarom alle filters met de hand uitgezocht moeten worden en waar mogelijk geïmplementeerd. De meeste SnelStart-administraties hebben een vrij beperkte omvang omdat SnelStart overwegend voorkomt bij wat kleinere organisaties. Daarnaast is de API op zich vrij vlot, met een doorvoersnelheid van meerdere honderden rijen per seconde.

Tot op heden zijn daarom geen filters geïmplementeerd.

Mocht er behoefte zijn aan specifieke filters, gelieve dan een antwoord toe te voegen met daarin per tabel de gewenste velden en de relationele operator, bijvoorbeeld in het formaat:

Tabel Veld Operator
Artikelen artikelcode >=
GrootboekMutaties Mutatiedatum between

Het is handig als per tabel ook opgemerkt wordt hoeveel rijen deze grofweg in totaal bevat (10.000, 100.000, 1 miljoen, etc.).

Ik ben een collega van lrs4, die ook bezig is met dit project.
Wat opvalt is dat we bij het laden van de tabellen in Power BI wel de preview krijgen, maar als we de data daadwerkelijk in willen laden in het model, de melding blijft staan:

Waiting for bridge-online.invantive.com

Voor nu heb ik een analyse gedaan van de tabellen welke we veel gebruiken en waar we met behulp van Query Folding hopen om de dataload te verminderen waardoor de data wel in kunnen laden.

Als ik het goed begrijp zouden we deze stappen dan als eerste door kunnen voeren zodat ze nog als native query verzonden worden?

Voor alle tabellen geldt dat het wenselijk is om kolommen te kunnen verwijderen in Query Folding. Dit kan al een groot verschil maken.

Verder zijn de onderstaande filters wenselijk:

Tabel Veld Operator
SnelStart.Grootboekmutaties.GrootboekMutaties datum >=
SnelStart.Grootboekmutaties.GrootboekMutaties Grootboek_ID =
SnelStart.Grootboekmutaties.GrootboekMutaties factuurnummer = of <>
SnelStart.Handel.VerkoopFacturen factuurdatum >=
SnelStart.Verkooporders.VerkoopOrderRegels datum >=
SnelStart.Handel.Inkoopfacturen factuurdatum >=
SnelStart.Relaties.RelatieRelatiesoorten TEXT = of <>

Is dit mogelijk?

De melding Waiting for bridge-online.invantive.com is normaliter terecht. Via Bridge Online Monitoring kan dan de echte status en voortgang bekeken worden. Bij Voorvertoning worden door Power BI Desktop alleen de eerste paar honderd of duizend rijen opgehaald. Deze verschijnen al snel omdat Invantive UniversalSQL en Bridge Online streaming zijn, zodat enkel die paar rijen opgehaald hoeven te worden.

Het filteren van velden is altijd mogelijk met query folding. In de praktijk zien we echter geen snelheidswinsten of -verliezen door het veranderen van de kolommenlijst, behoudens extreem brede kolommen zoals documenten en afbeeldingen.

De gevraagde filters zullen bestudeerd worden en later aan dit antwoord toegevoegd.

Op dit moment zijn we nog in afwachting van een reactie vanuit SnelStart. Zodra meer informatie beschikbaar is zal dit topic bijgewerkt worden.

Is er toevallig al een update over de connectie en mogelijkheid met Snelstart op de query folding toe te passen?

Op dit moment zijn we nog in afwachting van een reactie vanuit SnelStart. Zodra meer informatie beschikbaar is zal dit topic bijgewerkt worden.

Er is geen officiele documentatie beschikbaar over de filters en hun standaardisatie. E.e.a. zal handmatig verwerkt gaan worden.

Dankjewel voor de reactie. Is er een inschatting mogelijk qua doorlooptijd waarin we de wijzigingen kunnen gaan testen?

Nee, daar is geen zinvolle inschatting voor te geven.

Een analyse van de voorkomende omvang in aantal rijen van de tabellen levert het volgende overzicht:

Tabel Endpoint #Rijen
Grootboek.Dagboeken dagboeken 10
Grootboek.Kostenplaatsen kostenplaatsen 100
Grootboek.GrootboekRekeningen grootboeken 500
Offertes.Offertes offertes 500
KolommenBalansPerPeriode rapportages/kolommenbalans 500
Relaties.RelatieRelatiesoorten relaties 1.000
Handel.Relaties relaties 2.500
Handel.VerkoopFacturen verkoopfacturen 10.000
Handel.Inkoopfacturen inkoopfacturen 10.000
Verkooporders.VerkoopOrderRegels verkooporders 25.000
Grootboekmutaties.GrootboekMutaties grootboekmutaties 500.000

Query

Behoudens uitzondering blijkt de volgende query gebruikt te kunnen worden om de herschrijving te bepalen:

insert into NativePlatformScalarRequests@snr
( url
, http_method
, fail_on_error
)
select 'https://b2bapi.snelstart.nl/v2/...?$top=10&$filter=' 
       --
       -- Vervang de eerste letter van de kolomnaam door de hoofdletter.
       --
       || upper(substr(name, 1, 1)) 
       || substr(name, 2) 
       || ' ne null'
,      'GET'
,      true
from   SystemTableColumns@DataDictionary
where  table_name = '...'
--
-- Sluit alle (afgeleide) kolommen uit met een 
-- underscore ('_') in de kolomnaam.
--
and    instr(name, '_') = 0
order
by     name

GrootboekRekeningen

Alle velden herschrijven als bovenstaande query geeft werkend resultaat.

KolommenBalansPerPeriode

Geen geldig filter gevonden.

Artikelen

Als bovenstaande query met uitsluiting van de kolom met naam isHoofdartikel.

Offertes.Offertes

Alle velden herschrijven als bovenstaande query geeft werkend resultaat.

Relaties.RelatieRelatiesoorten

Buiten reikwijdte vanwege geneste karakter.

Handel.Relaties

Als bovenstaande query met weglating van de kolommen met de naam inkoopBoekingenUri en verkoopBoekingenUri.

Handel.VerkoopFacturen

Alle velden herschrijven als bovenstaande query geeft werkend resultaat.

Handel.Inkoopfacturen

Alle velden herschrijven als bovenstaande query geeft werkend resultaat.

Verkooporders.VerkoopOrderRegels

Buiten reikwijdte vanwege geneste karakter.

Grootboekmutaties.GrootboekMutaties

Alle velden herschrijven als bovenstaande query geeft werkend resultaat.

Operatoren

De volgende mogelijkheden zijn beschikbaar:

  • and of meerdere $filter.
  • or.
  • groeperen met ‘(’ en ‘)’.
  • Constante null.
  • Integers, datums, guid en teksten.
  • eq, ne, lt, le, gt, ge.
  • Alleen tekst: substringof en startswith.
  • Eventueel nesting zoals 'Grootboekrekening/Idvoor bijvoorbeeldRelatieRelatiesoorten`.

Configuratie

Een driverattribuut zoals max-odata-filters is beschikbaar om toepassing van filters in/uit te schakelen, met voorlopig standaardwaarde 0.

In release 24.0.722 zal een eerste versie van server-side filtering voor SnelStart opgenomen zijn.

Met deze versie zullen talrijke geautomatiseerde tests uitgevoerd worden om de impact van eventuele negatieve gevolgen te achterhalen. Dit kan kort of lang duren, afhankelijk van de impact.

Zodra acceptabel stabiel zal deze versie in productie genomen worden op Invantive Cloud.

De controle gebeurt aan de hand van een script zoals:

declare
  l_stmt     varchar2;
  l_cnt      number;
  l_test_cnt number;
begin
  l_test_cnt := 0;
  for r_tbe
  in
  ( select tbe.catalog
    ,      tbe.schema
    ,      tbe.name
    from   systemtables@datadictionary tbe
    left
    outer
    join   ( select distinct table_name
             from   systemtablefunctionparameters@datadictionary
             where  provider_name = 'SnelStart'
             and    PRESENCE_REQUIRED = true
           ) tbefrp
    on     tbefrp.table_name = tbe.name /* Partial is sufficient. */
    where  tbe.provider_name = 'SnelStart'
    and    tbe.name not in ('NATIVEPLATFORMSCALARREQUESTS')
    and    tbe.name not in ('Prijsafspraken')
    --
    -- No required parameters.
    --
    and    tbefrp.table_name is null
    order
    by     tbe.name  
  )
  loop
    dbms_output.put_line
    ( 'Test all columns of ' 
      || r_tbe.name 
      || ', starting at test case #' 
      || to_char(l_test_cnt) 
      || '.'
    );
    for r_cln
    in
    ( select cln.name
      ,      cln.database_data_type
      from   systemtablecolumns@datadictionary cln
      where  cln.table_catalog_name = r_tbe.catalog
      and    cln.table_schema_name  = r_tbe.schema
      and    cln.table_name         = r_tbe.name
      order
      by     cln.name
    )
    loop
      l_test_cnt := l_test_cnt + 1;
      --
      -- Test null.
      --
      begin
        l_stmt :=
        'select count(*)'
        || ' from ' || r_tbe.catalog || '.' || r_tbe.schema || '.' || r_tbe.name
        || ' where ' || r_cln.name || ' is null'
        ;
        execute immediate l_stmt into l_cnt;
      exception
        when others
        then
          dbms_output.put_line
          ( 'Failed on null for ' 
            || r_tbe.name || '.' || r_cln.name
            || ': ' 
            || sqlerrm
          );
      end;
      --
      -- Test not null.
      --
      begin
        l_stmt :=
        'select count(*)'
        || ' from ' || r_tbe.catalog || '.' || r_tbe.schema || '.' || r_tbe.name
        || ' where ' || r_cln.name || ' is not null'
        ;
        execute immediate l_stmt into l_cnt;
      exception
        when others
        then
          dbms_output.put_line
          ( 'Failed on not null for ' 
            || r_tbe.name || '.' || r_cln.name
            || ': ' 
            || sqlerrm
          );
      end;
      --
      -- Test equal.
      --
      begin
        l_stmt :=
        'select count(*)'
        || ' from ' || r_tbe.catalog || '.' || r_tbe.schema || '.' || r_tbe.name
        || ' where ' || r_cln.name || ' = '
        || case
           when r_cln.database_data_type = 'boolean'
           then 'true'
           when r_cln.database_data_type = 'varchar2'
           then '''value'''
           when r_cln.database_data_type = 'char'
           then '''N'''
           when r_cln.database_data_type = 'guid'
           then 'newid()'
           when r_cln.database_data_type in ('date', 'datetime')
           then 'trunc(sysdateutc)'
           when r_cln.database_data_type in ('decimal', 'double', 'int32', 'int64', 'number', 'numeric')
           then '1'
           else 1/0
           end
        ;
        execute immediate l_stmt into l_cnt;
      exception
        when others
        then
          dbms_output.put_line
          ( 'Failed on equals for ' 
            || r_tbe.name || '.' || r_cln.name
            || ': ' 
            || sqlerrm
          );
      end;
    end loop;
  end loop;
end;

De volgende eenvoudige filters zijn getest:

  • is null
  • is not null

To do:

  • =
  • !=
  • >=
  • >
  • <=
  • <
  • between

Complexer Filter 1

Een complexer filter is getest met:

select m.dagboek_id
from   grootboekmutaties m
join   dagboeken d
on     d.id = m.dagboek_id
where  m.datum > trunc(sysdateutc)
and    m.saldo > 400
and    m.saldo < 500
and    m.saldo != 450
and    m.saldo != 451
and    m.saldo != 452
and    m.saldo != 453
and    m.saldo != 454
and    m.saldo != 455
and    m.saldo != 456
and    m.saldo != 457
and    m.saldo != 458
and    m.saldo != 459
and    m.saldo != 460

met resulterende API URL’s:

https://b2bapi.snelstart.nl/v2/grootboekmutaties?$top=500&$filter=Datum+gt+datetime%272025-05-30T00%3A00%3A00%27+and+Saldo+gt+400+and+Saldo+ne+450+and+Saldo+ne+451+and+Saldo+ne+452+and+Saldo+ne+453+and+Saldo+ne+454+and+Saldo+ne+455+and+Saldo+ne+456+and+Saldo+ne+457+and+Saldo+ne+458+and+Saldo+ne+459+and+Saldo+ne+460+and+Saldo+lt+500

en

https://b2bapi.snelstart.nl/v2/dagboeken

Het ontbreken van een filter op dagboeken is terecht: deze (kleine) tabel kent geen filtermogelijkheid.

Complexer Filter 2

Een andere complexere aanpak is het wisselen tussen join-strategieeen.

De volgende query laat zien dat er van join-strategie gewisseld is:

select m.boekstuk
,      r.id
from   grootboekmutaties m
join   relaties r
on     r.id = m.relatiePublicIdentifier_id
where  m.datum > trunc(sysdateutc) - 2
and    m.saldo > 0

met resulterende API URL’s:

https://b2bapi.snelstart.nl/v2/grootboekmutaties?$top=500&$filter=Datum+gt+datetime%272025-05-28T00%3A00%3A00%27+and+Saldo+gt+0

en

https://b2bapi.snelstart.nl/v2/relaties?$top=500&$filter=%28Id+eq+guid%273ffa19ca-490b-4271-84c3-041de3b9a70c%27+or+Id+eq+guid%27cdf7c76c-56ab-4420-a8a5-6a571fed442a%27+or+Id+eq+guid%27bc6c14eb-8679-4f70-ba6f-75ce530d4c0a%27%29

waarbij in dit geval drie relaties opgezocht worden via een snelle route.

Complexer Filter 3

Echter, de SnelStart-driver ondersteunt momenteel het gebruik van maximaal 1 URL voor een dataverzoek en NIET het projecteren van een API-verzoek op meerdere API URL’s zoals de OData-driver.

Dit blijkt uit de query:

select m.boekstuk
,      r.id
from   grootboekmutaties m
join   relaties r
on     r.id = m.relatiePublicIdentifier_id
where  m.datum > trunc(sysdateutc) - 7
and    m.saldo > 0

met als gebruikte API URL’s:

https://b2bapi.snelstart.nl/v2/grootboekmutaties?$top=500&$filter=Datum+gt+datetime%272025-05-23T00%3A00%3A00%27+and+Saldo+gt+0

en

https://b2bapi.snelstart.nl/v2/relaties?$top=500

De aankondiging van beschikbaarheid is te lezen op:

Tijdens de testen van de filters en de aanpassingen werd in PowerBI de data niet goed geladen, dat kan ook te maken hebben met de Snelstart versie, want deze is zelf ook erg traag.

Maar de volgende foutmelding wordt via Monitoring weergegeven bij het aanmaken van bijvoorbeeld een Gen1 dataflow.

itgenclr150
Systeemfout

De query die hierop is toegepast is:

let
  Source = OData.Feed("https://bridge-online.invantive.com/acme-snelstart2/odata4", null, [Implementation = "2.0"]),
  #"Navigation 1" = Source{[Name = "SnelStart.Grootboekmutaties.GrootboekMutaties@snr", Signature = "table"]}[Data],
  #"Filtered rows" = Table.SelectRows(#"Navigation 1", each [datum] > #datetimezone(2024, 1, 1, 0, 0, 0, 1, 0))
in
  #"Filtered rows"

Wat zegt deze foutcode? Hoe kunnen we deze voorkomen en welke partities worden aangeraden om te laden? Bijvoorbeeld elke maand of iets dergelijks? En als laatste, Gen1 kan ook met een incremental refresh werken, wordt dit nu ook ondersteunt met de aanpassingen in de query?

itgenclr150 treedt op als de download geannuleerd wordt; meestal is de veroorzaker een RST_STREAM op basis van HTTP/2. Zie ook:

Merk echter op dat de afbeelding geen SnelStart-downloadverzoek weergeeft, zoals blijkt uit het pad: /app/diagnostics/sessionios/api?filter=all. Dit betreft het downloaden van alle Session I/O’s.

Advies is om het gefaalde verzoek op te zoeken op https://bridge-online.invantive.com via Monitoring en dan de details door op het verzoek te klikken.

Deze vraag is automatisch gesloten na 1 week inactiviteit. Het laatste gegeven antwoord is gemarkeerd als oplossing.

Gelieve een nieuwe vraag te stellen via een apart topic als het probleem opnieuw optreedt. Gelieve in de nieuwe vraag een link naar dit topic op te nemen door de URL er van in de tekst te plakken.

Dit topic is 3 dagen na het laatste antwoord automatisch gesloten. Nieuwe antwoorden zijn niet meer toegestaan.