The PIT
Röster från ITM-koncernen.

Beräknad väntetid: 5 minuter.

February 3, 2009 13:00 by Stefan Karlsson

 

Implementerade precis “beräknad väntetid” i vårt köhanteringssystem, ni vet ett sådant där kunden trycker fram ett könummer och sedan väntar på att just det numret ska visas upp på en display så man vet vilken kassa man ska gå till. 

Det förvånade mig att det fanns så många sätt att beräkna just denna väntetid på, många uppenbarligen felaktiga också.

För tydlighetsskull så benämner jag det nummer som kunden får fram på sitt kvitto för TicketNumber och det nummer som kassören matar fram för QueNumber. QueNumber är alltså det nummer som just nu ska betjänas, det kan aldrig vara större än NextTicketNumber (Sista utskrivna TicketNumber+1).

Skillnaden i värde mellan QueNumber och NextTicketNumber är alltså detsamma som antalet kunder som står och väntar på att bli betjänade.

Variant 1

Min första approach var att varje gång en kassör matar fram ett QueNumber i kassan så sparas kassaid, quenumber och en timestamp i en tabell och därefter så är det lätt att få fram hur lång tid det tog mellan varje frammatning och på så sätt få fram en “velocity”, antal frammatningar per timme. Om man räknar fram att man har 10 frammatningar per timme och sedan tar NextTicketNumber-QueNumber (för att få fram antal väntande kunder) så kan man med den informationen räkna ut hur lång genomsnittlig väntetid det blir för de väntande kunderna. För att minska eventuella avvikelser när busslaster kommer så kan man öka tiden man beräknar på till två timmar.

En stored procedure tog snabbt fram detta:

CREATE PROCEDURE GetQueWaitTime AS 
declare @NextTicketNumber int
select @NextTicketNumber=countervalue from skibarcounters where countername='nextticketnumber'
declare @NextQueNumber int
select @NextQueNumber=countervalue from skibarcounters where countername='NextQueNumber'
declare @TicksPerTwoHours int
select @TicksPerTwoHours=count(*) from questamps where datediff(n,stamp,getdate())<120
select 120/@TicksPerTwoHours   * (@NextTicketNumber-@NextQueNumber)

 

Uppenbara nackdelar med denna metoden:

  • Om man inte haft kunder på två timmar, eller väldigt få, så blir antal frammatningar per två timmar 0 eller nära 0, vilket gör att man får en falsk väntetid. Dvs, har man bara haft två kunder så kan det bli så att systemet antar att väntetiden är 30 minuter per kund och kommer det sedan in 10 kunder så kommer systemet visa helt felaktiga väntetider mot verkligheten.
  • Början av dagen när man inte haft några kunder så kommer det inte att finnas tillgängliga data och problemet som uppstår i punkt 1 kommer att uppstå här också.
  • Har man haft kunder och man får fram en genomsnittlig tid på 5 minuters väntetid och sedan har en dödperiod så kommer de där 5 minutrarna att öka hela tiden tills man börjar mata fram nya nummer. Det är inte verklighetsbaserat att väntetiden per kund ska öka bara för att man inte råkar ha några kunder att betjäna.

      Variant 2

      Vi insåg att vi var tvugna att spara ner tidpunkten för när kunden trycker fram sitt nummer, vi modellerade om tabellen lite så att när kunden trycker fram ett TicketNumber så lagras numret och tidpunkt för utskrift och när kassan trycker fram ett QueNumber så uppdateras den raden med kassaid och tidpunkt för betjäning samt en uträknad diff i minuter mellan dessa två tidpunkter. (Genom att matcha mot kvitton skapade i kassan kan man därefter också få fram hur lång tid det tog att betjäna kunden, men det är en annan sak)

      Nu räckte det alltså med en enkel fråga för att få fram genomsnittlig tid, vi nöjer oss med att kolla genomsnittlig faktisk väntetid den senaste timmen, fältet WaitedTime är det uträknade antal minuter kunden fick vänta mellan han tryckte ut sitt könummer och tills könumret visades i displayen, vi kollar bara rader som har ett cashierid som skiljer sig från 0 för det är bara de som blivit “frammatade”:

      select avg(waitedtime) as waitminutes from questamps where

      cashierid<>0 and datediff(n,stamp,getdate())<60

       

      Jag kände mig ganska nöjd med den här lösningen först men sen när jag sovit en natt så kom jag på att denna variant inte tar hänsyn till hur många kunder som står och väntar just nu. Den gör bara halva arbetet, den säger att Senaste timmen var den genomsnittliga väntetiden 5 minuter. Men om det kommit in en ny busslass med personer så att det står 40 personer och väntar så applicerar den inte kunskapen på detta.

      Lösningen är så klart att plocka fram antalet väntande kunder och applicera den genomsnittliga väntetiden på dessa och visa ett modifierat tal för detta.

      Med den modifikationen känns det som att vi har en pålitlig uträkning för att få fram en genomsnittlig väntetid som både tar hänsyn till historik och nuvarande kösituation.

      Det jag skulle vilja lägga till för att förbättra rutinen är att man först filtrerar bort stora avvikelser, t.ex. tar bort de som avviker i tid med mer än x% från genomsnittstiden osv. För att det inte påverkar allt för mycket när 4 busslaster med danskar stormar in. Men det kan man finetune efter att man fått in lite mer verkliga data att titta på.

      Utmaning

      Har du något bättre sätt att få fram en pålitlig siffra eller en helt annan approach (jag har minst 3 varianter till att räkna ut siffran på, mer eller mindre felaktiga eller med falska utslag av väntetider osv) så kommentera gärna artikeln.


    Tags:
    Categories: Programmering | .Net | SQL
    Actions: E-mail | Permalink | Comments (0) | Comment RSSRSS comment feed

    Dataset som inte vill skapa updatecommand

    December 18, 2008 23:56 by Stefan Karlsson

     

    Själv så arbetar jag numera ständigt med datasets och tableadapters. Fler och fler går över till detta överlägset enkla och lättarbetade sätt att kommunicera med databasen så jag tänkte bara tipsa om en grej som jag lite då och då stöter på. Första gången jag stötte på det så vart jag väldigt konfunderad.

    När man skapar en tableadapter så pekar man ju ut en tabell och talar om vilka fält som adaptern ska använda. Wizarden skapar då all nödvändig kod för att göra update, delete, insert och genererar en massa partialklasser som gör att du kan arbeta med tabellen som ett objekt (utan risk för sql-injections).

    image

    Klickar man på tabelladapterns titel så får man upp egenskaperna för den:

     image

    Ovan så ser vi att den skapat update, delete, select och insert-command. Allt är frid och fröjd.

    Helt plötsligt så stötte jag på att insert och update-command inte skapades. Lägligt nog hade jag en silent (illa) try/catch runt CustomerTA.update(mycustomers) när jag körde update så det tog även en stund att komma på varför datat inte sparades. När jag väl upptäckte att det smällde i updaten och såg att det berodde på att det inte fanns någon updatecommand att anropa.

    Så jag öppnade upp datasetet, körde configure på tabelladaptern och gick igenom wizarden, den avslutades snällt med detta meddelande:

    image

    Lägg märke till att den påstår att den skapat alla insert, update statements och metoder. Det gör att man tror att allt är frid och fröjd. Men det är bara ett spel för gallerian. Det den inte säger är att den inte klarade av att skapa updatecommand och insertcommand.

    Tittar man nu på egenskaperna för tabelladaptern så ser det ut så här:

    image

    Eftersom den skapar metoderna så kommer man att kunna anropa tabeladapter.update() men tabelladaptern har inget updatecommand kopplat till sig så den kommer att smälla när man kör det.

    Och så till slutklämmen, varför blir det då på det här viset?

    Jo, tabellen hade ingen primary key. Utan Primary Key så skapar den inte dessa kommandon då den inte kan säkerställa att det är rätt rad man uppdaterar eller raderar. Det räcker inte med att bara sätta identity på kolumnen och tro att allt är bra.

    Nu stöter jag inte på detta så ofta då jag har för vana att alltid ha ett id-fält som är primary key men ibland har man nån skräptabell som bara innehåller nån setting eller liknande och tänker inte på att sätta detta.

    Nu slipper ni ialla fall stöta på det problemet för nu vet ni. ;)


    Tags:
    Categories: Programmering | SQL | dataset
    Actions: E-mail | Permalink | Comments (0) | Comment RSSRSS comment feed

    Job Scheduling Tool for SQL Server Express and MSDE

    December 12, 2008 10:19 by Stefan Karlsson

     

    Vet inte om ni har stött på problemet, men SQL server Express har ingen SQL Server Agent så man kan inte lägga in automatiska tidsstyrda backuper för databaser. Vilket ju är lite tråkigt om man driftar en produkt med SQL express som dbm hos en kund. 

    Workarounds har varit att man skapar ett SQLCmd-script som man lägger in i windows vanliga scheduler men den är som bekant inte lika pålitlig som stabila SQL Server Agent.

    Har hittat detta verktyg som lägger sig som en service på burken och låter dig lägga upp jobb, mycket smidigt, och gratis:

    http://www.mssqltips.com/tip.asp?tip=1083

    Nedladdning:

    http://www.lazycoding.com/downloads/SQLScheduler_v0.1.0.0.zip


    Tags:
    Categories: SQL | SQL Express
    Actions: E-mail | Permalink | Comments (2) | Comment RSSRSS comment feed

    Automatisk Backup av alla databaser i SQL Server

    September 18, 2008 01:26 by dennis johansson

    Jag har fått en ny dator av jobbet och skulle flytta över alla mina data dit. Alla projekt m.m var ju inga problem, men när man kom till databaser osv så tyckte jag att det blev jobbigare. Jag har ca 40 databaser och tänkte göra Backup från min gamla maskin och Restore på nya maskinen. Suck!! Jag gillar inte att klicka en massa...

    Vad behövde jag göra?

    1. Göra Backup via t-sql.
    2. Lista databasernas namn.
    3. Loopa igenom ett resultset.
    4. Exekvera t-sql texten.
    5. Göra Restore via t-sql.
    6. Skapa Restore-textfil för att automatisera restoren.

    ca. 50 min senare hade jag mitt sql-script färdigmekat tack vare sökande i andras bloggar och tyckte därför att jag kan skriva en rad eller två själv om det.

    Jag har ej fixat stöd för databaser i offline-läge, det blir fel vid backup men scriptet fortsätter. Dom kommer även med i AutoRestore.txt. Den har jag inte testat än, sker imorgon! ;)

       1: use master
       2: DECLARE @dbName varchar(255), 
       3:     @cmdtxt varchar(1000)
       4:  
       5: -- Tar bort restore textfilen.
       6: set @cmdtxt='del c:\SQLBackups\AutoRestore.txt'
       7: exec xp_cmdshell @cmdtxt
       8:  
       9: -- Skapar en pekare mot queryn för att hämta databasnamnen
      10: DECLARE TableCrsr CURSOR FOR
      11: -- sid=0x01 är det på databaser som kommer till vid installationen. Iallafall i min DB. ;)
      12: SELECT name FROM sysdatabases where sid<>0x01 order by name
      13:  
      14: -- Öppnar pekaren
      15: OPEN TableCrsr
      16:  
      17: -- Hämtar första raden in i min variabel
      18: FETCH TableCrsr INTO @dbName
      19:  
      20: -- Loopar till status = 0
      21: WHILE @@Fetch_Status = 0
      22:    BEGIN
      23:     -- Skapar min restore text som skrivs ner med gamla hederliga DOS-komandot echo till textfilen med hjälp av xp_cmdshell i SQL Servern.
      24:     select @cmdtxt = 'echo restore database '+@dbName+' from disk =N''c:\SQLBackups\'+@dbName+'.bak'' >> c:\SQLBackups\AutoRestore.txt'
      25:     exec xp_cmdshell @cmdtxt
      26:  
      27:     -- Visar enkelt vart man är under själva exekveringen av backup.
      28:     select 'Backing up DB: ' + @dbName
      29:     -- Gör själva backupen.
      30:     exec (N'backup database '+@dbName+' to disk =N''c:\SQLBackups\'+@dbName+'.bak'' with init')
      31:        
      32:     -- Hämtar nästa rad av databasnamnen
      33:        FETCH TableCrsr INTO @dbName
      34:    END
      35: -- Stänger pekaren
      36: CLOSE TableCrsr
      37: DEALLOCATE TableCrsr

    Tags: , ,
    Categories: SQL
    Actions: E-mail | Permalink | Comments (0) | Comment RSSRSS comment feed

    In() i SQL

    September 15, 2008 15:26 by Christoffer Munkestam

    Ofta ser man sqler som använder sig av IN() på ett eller annat sätt. Tex:
    Select Id from tblA where Id IN(Select Id from tblB)

    Visst är det smidigt, det många dock inte vet är att IN() kan vara en riktig prestandaätare. Jag tänkte därför visa en alternativ lösning som ger exakt samma resultat och även presentera lite prestandaskillnader. Istället för IN() kan vi använda oss av en helt vanlig Inner Join, och istället för Not IN() så kan vi använda oss av en Left Join och sedan kontrollera ifall en post saknas eftersom vi då får Null-resultat om det inte finns någon relaterad post.

    Sqlen som jag visade i början skulle alltså även kunna skrivas såhär för att ge samma resultat:
    Select Distinct tblA.Id From tblA Inner Join tblB On tblA.Id = tblB.Id

    Vi måste förutom att lägga till en Inner Join även lägga till en Distinct, eftersom vi bara vill ha ut en post från tblA och inte en post för varje relaterad post som finns i tblB.

    Båda Sqlerna kommer alltså att plocka ut posterna från tblA där det finns en relaterad post i tblB. Skillnaden är dock prestandan.

    Jag körde de båda frågorna på en mssql 2000 databas via SQL Query Analyzer för att kunna se hur lång tid den behövde för att köra frågorna. Både tabellen A och B hade ungefär 150 000 poster i sig.

    Den sqlen som använde sig av IN() tog exakt 2 minuter och 19 sekunder att köra. Den sqlen som istället använde sig av en Inner Join tog 1 minut och 35 sekunder. Man kan sen lista ut att om man har stora frågor, mer komplexa, med flera IN() så kommer självklart prestandaskillnaden bli ännu större mellan dessa två sqler.

    Jag vill dock poängtera att det enbart är Select i IN() som man ska undvika. Skickar man in hårda id-nummer till databasen så fungerar det utmärkt (tex: IN(1,43,24)). Det är också viktigt att det här testet inte ses som vetenskapligt på något sätt. Utan med rätt indexering, cache etc så är det möjligt att man kan få helt andra siffror.


    Tags:
    Categories: SQL
    Actions: E-mail | Permalink | Comments (3) | Comment RSSRSS comment feed

    Stored Procedures i Access

    September 10, 2008 17:13 by Christoffer Munkestam

    Många underskattar helt klart Access. Visst kan jag hålla med om att det är en värdelös databas om man kör applikationer med mycket samtidiga anslutningar, men det är nog en av de få bristerna den verkligen har. Mycket annat går att göra även om det inte finns grafiska gränssnitt för det.

    Det är tex väldigt vanligt förekommande att folk tror att man inte kan skapa Stored Procedures i Access. Förmodligen enbart för att när de var 14, finniga, tankade musik via Napster och med hjälp av Access försökte förstå skillnaden mellan databaser och excel så stötte de aldrig på stored procedures. Strax innan man upplystes om vad stored procedures är så förkastar man Access och går över till något coolare - MySql eller MsSql. På så sätt reflekterar man aldrig över vad som verkligen saknas i Access och vad som man inte gjorde pga okunskap.

    Testa själva får ni se. Skapa en enkel procedure, typ:
    CREATE PROC sp_Insert (@i  INT) AS Insert into tblSP(Pr0n) Values(@i)

    Och kör den:
    EXECUTE sp_Insert @i=1

    I access så behöver man inte använda @ framför sina variabler, men man kan och jag gör det för att det blir lite mer konsekvent och oberoende av vilken typ av databas man kör.

    Källor:
    http://www.aspemporium.com/tutorials.aspx?tid=4
    http://www.devcity.net/Articles/18/msaccess_sp.aspx
    http://www.devcity.net/Articles/34/msaccess_sp2.aspx


    Tags:
    Categories: SQL
    Actions: E-mail | Permalink | Comments (0) | Comment RSSRSS comment feed

    Lite av Nyheterna i SQL 2008

    August 27, 2008 16:15 by dennis johansson

    Det mest intressanta som man kommer använda dagligen om ett tag är framför allt den nya datumhanteringen och lite hur man kan deklarera variabler och tilldela dem värden på en gång.

    Datum
    Datumhanteringer blir lite annorlunda men bibehåller även den gamla.
    Datum och tid kan nu vara mer precis. Tidiagare kunde DATETIME datatypen vara år 1753-9999 men nu kan DATE datatypen vara år 0001-9999 samt att DATETIME hade en precision på 0.00333 sekunder medan TIME nu har en variabel precision på 0 till 7 decimaler på en sekund (ända ner till hundradels nanosekund).

    Alltså så finns det idag dessa datum datatyper:

    • DATETIME (den gamla)
    • DATETIME2 (NY! den nya som har både den större datumet och den mer precisa tiden)
    • DATETIMEOFFSET (NY! som DATETIME2 men med stöd för Time Zone Offset från UTC)
    • DATE (NY! hanterar bara datum från 0001-01-01 till 9999-12-31)
    • TIME (NY! variabel precision ända ner till hundradels nanosekund)

    DATETIME och de nya datatyperna använder en del samma funktioner såsom DATENAME, DATEPART, DATEDIFF, DATEADD där man skickar in en datepart som nu även kan vara microsekunder, nanosekunder och TZoffset.

    Funktioner enbart för de nya datatyperna är SYSDATETIME, SYSUTCDATETIME, SYSDATETIMEOFFSET.

    Några nice funktioner (som har funnits sedan tidigare) för att hantera Localization och formatering på datum i SQL.
    SET DATEFIRST
    SET DATEFORMAT
    SET LANGUAGE

    Table som datatyp och värde
    Numera finns även TABLE som datatyp och kan användas som parameter för lagrade procedurer.
    T.ex: DECLARE @t TABLE (id int, name varchar(20));

    Möjlighet att tilldela och deklarera variabler samtidigt
    T.ex: DECLARE @i int = 4;

    MERGE Statement
    Detta kommando kan användas för att enkelt förena data från en table till en table. Det finns lite flaggor för att sätta om datat ska matcha eller ej. Den gör då en insert, update och delete utifrån om datat matchar eller ej.


    Tags: ,
    Categories: SQL
    Actions: E-mail | Permalink | Comments (0) | Comment RSSRSS comment feed