ottobre 2007 - Posts

[OT] La data di nascita di mio figlio è un numero primo
30 ottobre 07 12.56 | abenedetti | 3 comment(s)

L'amico Itzik posta sul suo blog questo: "Prime Numbers Challenge and My Little Friend Simone". Che grande che è! :-)

Se volete, provate anche voi a cimentarvi nella scrittura della funzione che dato un numero verifica se si tratti di un numero primo.

Filed under:
We Love Raf
26 ottobre 07 04.05 | abenedetti | with no comments

Sono stato pubblicato anche io. Qui.

Eh... caro Presidente...

Filed under:
Il nostro semaforo
26 ottobre 07 03.13 | abenedetti | with no comments

Torno da due settimane di Spagna e (finalmente!) trovo funzionante, nel nostro ufficio, il semaforo che avevamo comprato qualche mese fa.

Claudio "Scott" Maccari ha messo on line un post proprio su questo.

Di cosa si tratta? Semplicemente di uno strumento legato al sistema automatico di Continuous Integration in grado di mostrare se:

ooo : ci sono errori (build rotta)

ooo : sistemi in compilazione

ooo : ok (build funzionante)

Fantastico! :-)

Credo metteremo on line a breve anche il progettino della scheda che è stata fatta per collegare il semaforo alla porta parallela del pc.

Filed under:
[OT] Lego & Star Wars
19 ottobre 07 02.14 | abenedetti | 2 comment(s)

Perdonate l'OT ma essendo cresciuto a pane e mattoncini lego *devo* comprarmi questo: http://shop.lego.com/product/?p=10179

Un pò caruccio, magari per Natale... :-)

Filed under:
Running Totals: SQLCLR version
19 ottobre 07 11.00 | abenedetti | 5 comment(s)

Riprendo il post "Running totals: cursori, complessità lineari, complessità esponenziali" per presentare una terza soluzione basata su SQLCLR, ovvero tramite una stored procedure costruita ad hoc e, quindi, verificarne le performance.

Questo il codice C#:

[SqlProcedure]
public static void up_runningTotals()
{
    string tsqlStatement = "SELECT customerID, qty, 0 AS qtySum FROM orders ORDER BY customerID";

    SqlConnection cn = new SqlConnection("context connection=true");
    cn.Open();

    SqlCommand cmd = new SqlCommand(tsqlStatement, cn);
    
    SqlPipe myPipe = SqlContext.Pipe;
    SqlDataRecord record = new SqlDataRecord
        (
        new SqlMetaData("customerID", SqlDbType.Char,1),
        new SqlMetaData("qty", SqlDbType.Int),
        new SqlMetaData("qtySum", SqlDbType.Int)
        );
    
    string myCustomerID = "";
    Int32 myQtySum = 0;

    SqlDataReader dr = cmd.ExecuteReader();
    myPipe.SendResultsStart(record);
    if (dr.HasRows)
    {
        while (dr.Read())
        {
            if (myCustomerID != dr["customerID"].ToString())
                myQtySum = 0;

            myCustomerID = dr["customerID"].ToString();
            myQtySum += Convert.ToInt32(dr["qty"]);

            record.SetValue(0, Convert.ToChar(dr["customerID"]));
            record.SetInt32(1, Convert.ToInt32(dr["qty"]));
            record.SetInt32(2, myQtySum);

            myPipe.SendResultsRow(record);
        }
    }
    myPipe.SendResultsEnd();

    dr.Close();
    cmd.Dispose();
    cn.Dispose();
}

Questi i risultati sulla mia macchina:

Num Record Soluzione TSQL Soluzione cursore Soluzione SQLCLR
30 0 106 10
300 30 96 203
3000 1160 513 250
30000 53060 2643 1030

Risultati interessanti, no?

[Quiz] Spostare una colonna su un'altra
18 ottobre 07 11.17 | abenedetti | 5 comment(s)

Sulla falsariga del mio post precedente, "Row_number, aggiornamento di colonne, tempi di esecuzione", lascio a voi un piccolo quiz...

Supponiamo di avere una tabella simile a:

image

Ricreabile tramite:

USE tempdb
GO

CREATE TABLE temp
(
colA VARCHAR(10),
colB VARCHAR(10)
)
GO

INSERT temp VALUES ('a','1')
INSERT temp VALUES ('b','2')
INSERT temp VALUES ('c','3')

SELECT * FROM temp

Vogliamo spostare il contenuto della colonnaA all'interno della colonnaB e viceversa, ovvero ottenere un resultset simile a:

image

I commenti sono aperti! :-)

Filed under:
Row_number, aggiornamento di colonne, tempi di esecuzione
18 ottobre 07 11.13 | abenedetti | 4 comment(s)

Supponiamo di avere una tabella simile a:

image

Ricreabile tramite:

USE [tempdb]
GO

IF OBJECT_ID('dbo.orders', 'U') IS NOT NULL
  DROP TABLE dbo.orders
GO

CREATE TABLE orders
    (
      orderID UNIQUEIDENTIFIER PRIMARY KEY NONCLUSTERED,
      orderCounter INT,
      orderDate DATETIME,
      customerID INT
    )
GO

DECLARE @cusotmerID INT
SET @cusotmerID = 1

WHILE @cusotmerID <= 5
    BEGIN

        INSERT  orders
        VALUES  (
                  NEWID(),
                  0,
                  GETDATE(),
                  @cusotmerID
                )
        SET @cusotmerID = @cusotmerID + 1   
    END

SELECT  *
FROM    orders

Necessità: vogliamo aggiornare la colonna [orderCounter] secondo una numerazione progressiva.

Ovvero ottenere un resultset simile a:

image

Grazie a SQL Server 2005 potremmo utilizzare la funzione ROW_NUMBER(), ovvero scrivere un'istruzione di update simile a:

UPDATE  o2
SET     [orderCounter] = rowNumber
FROM    ( select    ROW_NUMBER() OVER ( ORDER BY customerID ) rowNumber,
                    *
          FROM      orders
        ) o1
        JOIN orders o2 ON o1.orderID = o2.orderID

Questo il piano di esecuzione risolto (un pò grossino... l'immagine non lo visualizza tutto...):

image

Esiste un modo più veloce per risolvere il nostro problema inziale?

Si, semplicemente grazie ad una variabile intera di appoggio che possa essere autoincrementata record per record.

Ovvero tramite (attenzione a come scriviamo la SET - dove andiamo ad autoincrementare la nostra variabile):

DECLARE @i INT
SET @i = 0

UPDATE orders
SET @i = orderCounter = @i+1

Il suo piano di esecuzione:

image

Direi molto più interessante... ;-)

Con una tabella con 50.000 record, per avere un'idea, questi i tempi di esecuzione:

Execution time in ms with row_number
------------------------------------
5493

Execution time in ms without row_number
---------------------------------------
373

Filed under:
Running totals: cursori, complessità lineari, complessità esponenziali
17 ottobre 07 02.10 | abenedetti | 1 comment(s)

Tra una birra ed un'altra qui a Madrid, continuano le chiacchere tra il sottoscritto, Davide e Itzik.

In queste righe voglio cercare di sottolineare come le soluzioni cursore non siano sempre da evitare come la peste...

Supponiamo di avere una tabella ORDERS, simile alla seguente:

image

Ovvero una tabella in cui abbiamo memorizzate, per semplicità, una quantità in ordine ed un identificativo cliente.

Quello che ci viene chiesto è: visualizzare la quantità in ordine e, accanto, il totale aggiornato degli stessi ordini. Ovvero una nuova colonna data dalla somma della quantità in ordine del record sommata alle quantità ordinate in precedenza dallo stesso cliente.

Il nostro problema ci chiede anche di ordinare i risultati secondo la colonna [customerID] (attenzione, i dati nella nostra tabella NON sono ordinati secondo questa colonna).

Un classico esempio di running totals insomma.

Graficamente,quindi, otterremmo qualcosa del genere:

image

Quello che ci serve per le nostre analisi è:

  1. una tabella ordini (orders)
  2. una procedura che popoli con un numero N di record la nostra tabella (up_populateOrders)
  3. una procedura che risolva il nostro problema senza cursorse (up_withoutCursor)
  4. una proceduda che risolva il nostro problema con cursore (up_withCursor)

All'interno delle due procedure che risolvono il nostro problema inseriremo anche una semplice istruzione che ci fornisca, in uscita, il tempo (espresso in millisecondi) trascorso per l'elaborazione ( SELECT datediff(ms,@StartDate, @EndDate) AS 'Execution time in ms' ).

Sottolineo come non sia il mio scopo, almeno non in questo post, analizzare come scriviamo il codice TSQL per risovlere il nostro problema.

Quello che voglio è valutare la bontà delle due soluzioni (con e senza cursore) e ragionare su queste.

Soluzioni che, evidentemente, devono necessariamente lavorare riga per riga per effettuare i calcoli necessari alla risoluzione del problema.

Il codice per costruire tutto quello che ci serve:

/* *** INIZIO CODICE TSQL *** */

USE tempdb
GO

IF OBJECT_ID('dbo.orders', 'U') IS NOT NULL
    DROP TABLE dbo.orders
GO

IF EXISTS ( SELECT  *
            FROM    INFORMATION_SCHEMA.ROUTINES
            WHERE   SPECIFIC_SCHEMA = N'dbo'
                    AND SPECIFIC_NAME = N'up_withoutCursor' )
    DROP PROCEDURE dbo.up_withoutCursor
GO

IF EXISTS ( SELECT  *
            FROM    INFORMATION_SCHEMA.ROUTINES
            WHERE   SPECIFIC_SCHEMA = N'dbo'
                    AND SPECIFIC_NAME = N'up_withCursor' )
    DROP PROCEDURE dbo.up_withCursor
GO

IF EXISTS ( SELECT  *
            FROM    INFORMATION_SCHEMA.ROUTINES
            WHERE   SPECIFIC_SCHEMA = N'dbo'
                    AND SPECIFIC_NAME = N'up_populateOrders' )
    DROP PROCEDURE dbo.up_populateOrders
GO

/* Creo la tabella ordini su cui faremo le nostre analisi */
CREATE TABLE orders
    (
      idRecord INT PRIMARY KEY
                   IDENTITY(1, 1),
      qty SMALLINT,
      customerID CHAR(1)
    )
GO

CREATE PROCEDURE dbo.up_populateOrders ( @numOrders int )
AS
    SET NOCOUNT ON

    TRUNCATE TABLE orders

    DECLARE @ciclo INT
    SET @ciclo = 0

    WHILE @ciclo < @numOrders
        BEGIN
            INSERT  orders
                    SELECT  CAST(RAND() * 100 AS INT),
                            'C'

            SET @ciclo = @ciclo + 1
        END

    SET @ciclo = 0
    WHILE @ciclo < @numOrders
        BEGIN
            INSERT  orders
                    SELECT  CAST(RAND() * 100 AS INT),
                            'A'

            SET @ciclo = @ciclo + 1
        END

    SET @ciclo = 0
    WHILE @ciclo < @numOrders
        BEGIN
            INSERT  orders
                    SELECT  CAST(RAND() * 100 AS INT),
                            'B'

            SET @ciclo = @ciclo + 1
        END

    SELECT  COUNT(*) AS numOrders
    FROM    orders

    SET NOCOUNT OFF
GO

CREATE PROCEDURE dbo.up_withoutCursor
AS
    SET NOCOUNT ON

    DECLARE @StartDate datetime,
        @EndDate DATETIME
    SET @StartDate = getdate()

    SELECT  customerID,
            O1.qty,
            O1.qty + ISNULL(( SELECT    SUM(O2.qty)
                              FROM      orders O2
                              WHERE     O2.idRecord < O1.idRecord
                                        AND O2.customerID = O1.customerID
                            ), 0) AS qtySum
    FROM    orders O1
    ORDER BY customerID,
            idRecord

    SET @EndDate = getdate()
    SELECT  datediff(ms, @StartDate, @EndDate) AS 'Execution time in ms'
    SET NOCOUNT OFF
GO

CREATE PROCEDURE dbo.up_withCursor
AS

  SET NOCOUNT ON

    DECLARE @StartDate datetime,
        @EndDate DATETIME
    SET @StartDate = getdate()

    DECLARE @myQty INT
    DECLARE @customerID CHAR(1)
    DECLARE @CID CHAR(1)
    DECLARE @sum int
    DECLARE @tableResults TABLE
        (
          qty SMALLINT,
          qtySum INT,
          customerID CHAR(1)
        )

    SET @myQty = 0
    SET @CID = ''

    DECLARE myCursor CURSOR FAST_FORWARD READ_ONLY
        FOR SELECT  qty,
                    customerID
            FROM    orders
            ORDER BY customerID,
                    idRecord

    OPEN myCursor

    FETCH NEXT FROM myCursor INTO @myQty, @customerID

    WHILE @@fetch_status = 0
        BEGIN
            IF @CID <> @customerID
            SET @sum = 0
            SET @CID = @customerID
            SET @sum = ( @sum + @myQty )
            INSERT  @tableResults
            VALUES  (
                      @myQty,
                      @sum,
                      @customerID
                    )

            FETCH NEXT FROM myCursor INTO @myQty, @customerID
        END

    SELECT  [customerID],
            [qty],
            [qtySum]
    FROM    @tableResults

    CLOSE myCursor
    DEALLOCATE myCursor

    SET @EndDate = getdate()
    SELECT  datediff(ms, @StartDate, @EndDate) AS 'Execution time in ms'
    SET NOCOUNT OFF

GO

/* *** FINE CODICE TSQL *** */

 

Costruito tutto quello che ci serve possiamo passare alle nostre analisi.

Se avete dato un'occhiata al codice vi sarete accorti che la SP che si occupa di inserire ordini inserirà un numero N di righe per tre ipotetici clienti: A,B,C.

Possiamo analizzare cosa succede con un numero molto basso di ordini, ad esempio: 2

EXEC dbo.up_populateOrders 2
DBCC dropcleanbuffers
CHECKPOINT

Quindi:

EXEC dbo.up_withoutCursor
DBCC dropcleanbuffers
EXEC dbo.up_withCursor

Il tempo di esecuzione è praticamente nullo per entrambe le soluzioni.

Alziamo il numero di ordini a 20 (quindi avremo 60 record in tabella), quindi eseguiamo nuovamente le procedure con  e senza cursore.

Quindi alziamo ancora il numero portandolo a 100, 500, 1000.

Noterete come, più alto sarà il numero di righe da analizzare migliore risulterà la soluzione con cursore.

C'è infatti una grande differenza tra soluzioni cursori e soluzioni set-based:

  • le soluzioni cursore possono basarsi su dati ordinati
  • le soluzioni set-based NON possono basarsi su dati ordinati

Questo significa che le soluzioni cursore, fatto un eventuale table scan possono utilizzare delle operazioni di seek su un resultset già ordinato.

Le soluzioni set-based utilizzeranno, invece, sempre degli scan.

 

Il piano di esecuzione della procedura senza cursore:

image

Il piano di esecuzione della procedura con cursore:

image 

 

La differenza è quindi nella complessità dell'algoritmo di risoluzione.

image

  • le soluzioni cursore hanno una complessità lineare (dove "o" rappresenta l'overhead dovuto ai calcoli richiesti)
  • le soluzioni set-based hanno una complessità esponenziale

image  image

Quindi: maggiori saranno le righe interessate dalla nostra elaborazione, maggiore sarà la possibilità di preferire una soluzione basata su cursore.

Considerazioni?

Logical Query Processing Phases
16 ottobre 07 02.39 | abenedetti | with no comments

Supponiamo di avere due select, abbastanza simili, come le seguenti:

SELECT YEAR(orderDate) AS orderYear
FROM orders
ORDER BY orderYear

SELECT YEAR(orderDate) AS orderYear
FROM orders
WHERE orderYear > 2006

Entrambe definiscono un alias per la funzione che va a calcolare l'anno dell'ordine.
La prima utilizza questo alias per ordinare i risultati, la seconda lo usa all'interno della clausola where.

Quindi le differenze sono soltanto:

  1. ORDER BY orderYear
  2. WHERE orderYear > 2006

Domanda:
- vengono eseguite correttamente?
- una viene eseguita ed una va in errore?
- entrambe vanno in errore?

Come probabilmente saprete già, la prima viene eseguita con successo, mentre la seconda produce un errore:
Msg 207, Level 16, State 1, Line 3
Invalid column name 'orderYear'.

Ovvero: nella seconda query il query processor non è in grado di individuare una colonna che si chiama "orderYear".

L'errore, semplicemente, è dovuto al fatto che la clausola where viene applicata *prima* della clausola where.

Ovvero è come se la query venisse scritta in questo modo:

FROM orders
WHERE orderYear > 2006
SELECT YEAR(orderDate) AS orderYear

In questo modo è evidente come si chiami la colonna prima di definirla.

Per fare un ripasso delle varie fasi logiche che vengono utilizzate per risolvere la query, proviamo a scrivere un'interrogazione un poco più complessa.
Ovvero una query che ci fornisca il mese ed il numero di ordini fatti in quel mese, a patto che ce ne sia più di uno, e solo per gli ordini dell'anno 2007.

Qualcosa come:

SELECT COUNT(orderDate) AS numOrders, MONTH(orderDate) AS orderMonth
FROM orders
WHERE YEAR(orderDate) = 2007
GROUP BY MONTH(orderDate)
HAVING COUNT(orderDate) > 1
ORDER BY orderMonth

Le Logical Query Processing Phases sono:

  1. FROM orders
  2. WHERE YEAR(orderDate) = 2007
  3. GROUP BY MONTH(orderDate)
  4. HAVING COUNT(orderDate) > 1
  5. SELECT COUNT(orderDate) AS numOrders, MONTH(orderDate) AS orderMonth
  6. ORDER BY orderMonth
Filed under:
TSQL Type: auto conversion
16 ottobre 07 12.20 | abenedetti | with no comments

Prendiamo questa select:

SELECT 10/3
/*
Risultato:

------------
3
*/

Il risultato è corretto? Espresso in termini interi si...

Se volessimo invece ottenere il corretto risultato (decimale) possiamo evitare l'utilizzo esplicito di funzioni di conversione (CAST o CONVERT), eseguendo qualcosa come:

SELECT 1.*10/3
/*
Risultato:

------------
3.333333
*/

Decimal ha precedenza maggiore di Int, quindi scrivere qualcosa come: 1. * 10  comporta la conversione automatica nel tipo più forte.

Filed under:
Forum Ricerca & Offerta
10 ottobre 07 03.35 | abenedetti | 1 comment(s)

Abbiamo creato un nuovo forum su UGISS di cui, credo, si sentisse la mancanza: Ricerca & Offerta.

Qui: http://community.ugiss.org/forums/188913.aspx

Filed under:

This Blog

Syndication