Newsgroup: it.comp.software.database
Titolo: MSSQL Server e istruzione TOP

Salve a tutti,
volevo chiedervi un parere.
In MSSQL mi sembra che non esista l'istruzione LIMIT presente per 
esempio in mysql, ma l'istruzione TOP.
L'istruzione TOP mi permette di estrarre i primi N record, mentre la 
LIMIT mi permette di estrarre i primi N record a partire dal record M.
Quindi, se volessi estrarre una lista di record da visualizzare su piu' 
pagine (tipo la lista visualizzata da un motore di ricerca),
per simulare l'operazione LIMIT ho cercato di aggirare il problema in 
questo modo:
SELECT TOP (@maxRighe) IDUtente
FROM Utenti
WHERE
IDUtente NOT IN (SELECT TOP (@maxRighe*(@iPag-1)) IDUtente FROM Utenti)
dove @maxRighe e' il numero di record che voglio estrarre ed @iPag il 
numero della pagina che voglio visualizzare.
In questo modo, se voglio visualizzare la prima pagina, la seconda 
SELECT esegue un TOP 0 (quindi non mi restituisce niente) e la prima 
SELECT estrare le mie prime @maxRighe.
Se voglio visualizzare la seconda pagina, la seconda SELECT esegue un 
TOP @maxRighe (quindi estrae gli IDUtente visualizzati nella pagina 1) e 
la prima SELECT estrae le mie prime @maxRighe escludendo gli IDUtente 
estratti dalla seconda SELECT. :-)
Forse un po' contorto, ma spero di essermi spiegato. :-)
Il problema di questo metodo e' che esegue una doppia query che puo' 
diventare pesante quando vengono estratti molti dati e soprattutto 
quando visualizzo le ultime pagine.
Secondo voi esiste un metodo migliore?
Grazie e scusate se sono stato un po' lungo. ;-)
Ciao,
Simone

 IDUtente NOT IN (SELECT TOP (@maxRighe*(@iPag-1)) IDUtente FROM Utenti)
Ciao Simone, considera che l'istruzione IN (e di conseguenza NOT IN) č una
delle istruzioni piů "pesanti" a livello di SQL che qualunque motore possa
affrontare...
Al posto di IN ti conviene usare EXISTS (visto che SQL Server non ha il
MINUS di Oracle)...
Sul mio sito avevo fatto un "come fare" che ti rimetto in linea:
http://spazioweb.inwind.it/it_informatica/comefare_15feb.htm
(lo lascio per tutto il mese di agosto...)
Paulus
http://spazioweb.inwind.it/it_informatica
--------------------------------
Inviato via 
http://usenet.libero.it

 Secondo voi esiste un metodo migliore?
Ciao Simone,
come dice Davide questo problema e' piuttosto ricorrente, pero'
questa volta mi hai dato lo spunto per fare una serie di prove
e vedere cosa succede dietro le quinte.
Il risultato lo puoi vedere di seguito, ci sono alcune cose
a cui fare attenzione rispetto alla soluzione che hai proposto
(sopratutto se ci sono dei duplicati nella colonna utilizzata
per paginare).
Rimango comunque dell'opinione che sia meglio evitare la
paginazione tout-court, magari dai un'occhiata all'ultimo
thread sull'argomento:
http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&oe=UTF-8&th=4e79cfcf6
65eff14&rnum=4
--<CUT HERE>--
/*
** Paginazione dati con Transact-SQL
**
** Avvertenze:
**
** 1. Data la dipendeza di di alcuni esempi dalla tabella
**    Customers del Northwind, questo deve essere come da
**    installazione (per ri-crearlo file instnwnd.sql nella
**    directory Install della directory di installazione di
**    SQL Server.
**
** 2. Usando un numero piu' consistente di righe (ad esempio
**    piu' di 40000) alcuni piani di esecuzione sono differenti
**    (alcune Merge Join vengono sotituite da Inner Loop)
**
** 3. L'output dei piani di esecuzione e delle statistiche di
**    I/O sono stati tagliati per evitare confusione e per
**    rimanere nel limite delle 72 colonne
**
** 4. Testato con SQL Server 2000 SP2
**
*/
USE tempdb
GO
IF OBJECT_ID('dbo.utenti') IS NOT NULL
 DROP TABLE dbo.utenti
GO
CREATE TABLE dbo.utenti(
 IDUtente int IDENTITY(1, 1) NOT NULL,
 NomeUtente nvarchar(30) NOT NULL,
 CONSTRAINT pk_utenti
 PRIMARY KEY (IDUtente)
)
GO
INSERT dbo.utenti (NomeUtente)
SELECT ContactName
FROM Northwind.dbo.Customers
GO
/*
** Se il set di record deve essere ordinato (quasi sempre), per
** prima cosa dobbiamo inserire le clausole di ORDER BY altrimenti
** stiamo assumendo che 1) esista un ordinamento fisico delle righe
** 2) che SQL Server ritorni le righe ordinate per l'ordinamento
** specificato al punto 1.
**
** Entrambe le assunzioni sono errate, in questo caso particolare
** funziona perche' esiste un clustered index ordinato in maniera
** ascendente e per via degli algoritmi di correlazione scelti
** che mantengono l'ordinamento del set prodotto.
**
** L'aggiunta delle clausole non comporta una penalizzazione in
** termini di prestazioni (come vederemo poi dai piani di
** esecuzione), se ci sono le condizioni ideali il Query Processor
** non effettua fisicamente l'ordinamento.
**
*/
CREATE PROCEDURE dbo.UtentiPaginati
 @maxRighe int,
 @iPag  int
AS
DECLARE @sqlstmt nvarchar(4000)
SET @sqlstmt = 'SELECT TOP ' + CAST(@maxRighe AS varchar)
    + ' IDUtente, NomeUtente'
    + ' FROM dbo.utenti WHERE IDUtente NOT IN'
    + ' (SELECT TOP '
    + CAST(@maxRighe*(@iPag -1) AS varchar) + ' IDUtente'
    + ' FROM dbo.utenti ORDER BY IDUtente)'
    + ' ORDER BY IDUtente'
EXEC dbo.sp_executesql @sqlstmt
GO
SET STATISTICS PROFILE ON
SET STATISTICS IO ON
GO
/*
** Prima di tutto notiamo che per il caso della pagina 1 il piano di
** esecuzione viene ridotto (l'ottimizzatore si accorge che la suquery
** con TOP 0 non ritorna niente e quindi la NOT IN e' sempre vera).
**
** Si accorge anche del fatto che stiamo chiedendo di fatto solo le
** prime 10 righe, quindi anche in presenza di decine di migliaia di
** righe fa pochissime operazioni di I/O
**
** Piano di esecuzione
** |--Top(10)
**    |--Clustered Index Scan(OBJECT:(...)
**
** Table 'utenti'. Scan count 1, logical reads 2
*/
EXEC dbo.UtentiPaginati 10, 1
GO
/*
** Dalla pagina 3 in poi effettivamente la tabella viene
** scandita 2 volte, il piano comunque e' piuttosto efficiente
** perche' utilizza l'indice clustered (non ha bisogno di fare
** Bookmark Lookup extra), l'indice e' gia' ordinato (non deve
** fare operazioni di sort extra), utilizza la Merge Join
** (la cui complessita algoritmica e' O(n log n) contro la
** complessita' O(n) di Hash Join o peggio ancora O(n^2) di un
** Nested Loop). Non e' necessario neppure fare l'operazione di
** ordinamento esterna perche' il risultato di una Merge Join e'
** gia' ordianto.
**
**  |--Top(10)
**     |--Merge Join(Right Anti Semi Join, MERGE:(...)
**        |--Top(40)
**        |  |--Clustered Index Scan(OBJECT:(..., ORDERED FORWARD)
**        |--Clustered Index Scan(OBJECT:(..., ORDERED FORWARD)
**
** Table 'utenti'. Scan count 2, logical reads 4
*/
EXEC dbo.UtentiPaginati 10, 3
/*
** Attenzione, la pagina 2 e' un caso particolare ed il piano di
** esecuzione non e' particolarlmente efficiente (viene introdotto
** un filtro e l'algortimo di Nested Loop).
**
**  |--Top(10)
**     |--Nested Loops(Left Anti Semi Join, OUTER REFERENCES:(...)
**        |--Clustered Index Scan(OBJECT:(..., ORDERED FORWARD)
**        |--Filter(WHERE:(...)
**           |--Top(10)
**              |--Clustered Index Scan(..., ORDERED FORWARD)
**
** Table 'utenti'. Scan count 21, logical reads 42
*/
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Non so esattamente perche' internamente venga fatta questa
** trasformazione, se qualcuno ha qualche idea mi piacerebbe
** discuterne.
**
** Comunque, come possiamo fare per migliorare le prestazioni ?
**
** Se la chiave che utilizzata per paginare ha valori monotonici
** crescenti senza buchi nella sequenza, possiamo semplificare
** drasticamente la query in
**
** SELECT TOP @maxRighe IDUtente, NomeUtente
** FROM   dbo.utenti
** WHERE  IDUtente > @maxRighe * (@iPag - 1)
**
** meglio ancora, possiamo direttamente ridurre il tutto
** a questo (senza TOP che non e' previsto dallo standard
** ANSI e con il beneficio di utilizzare un index seek/range scan)
**
** SELECT IDUtente, NomeUtente
** FROM   dbo.utenti
** WHERE  IDUtente BETWEEN (@maxRighe * (@iPag - 1)) + 1
**                     AND (@maxRighe * (@iPag - 1)) + @maxRighe
**
*/
ALTER PROCEDURE dbo.UtentiPaginati
 @maxRighe int,
 @iPag  int
AS
DECLARE @sqlstmt nvarchar(4000)
SET @sqlstmt = 'SELECT IDUtente, NomeUtente'
    + ' FROM dbo.utenti'
    + ' WHERE IDUtente BETWEEN'
    + ' ' + CAST((@maxRighe * (@iPag - 1)) + 1 AS varchar)
    + ' AND'
    + ' ' + CAST((@maxRighe * (@iPag - 1)) + @maxRighe AS varchar)
    + ' ORDER BY IDUtente'
EXEC dbo.sp_executesql @sqlstmt
GO
/*
** Il piano di esecuzione ora e' sempre uguale e sempre molto
** efficiente
**
** |--Clustered Index Seek(OBJECT:(...), SEEK:(...) ORDERED FORWARD)
**
** Table 'utenti'. Scan count 1, logical reads 2
*/
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
EXEC dbo.UtentiPaginati 10, 3
GO
/*
** Il vero problema di tutto questo e' che spesso e volentieri
** non sussistono tali condizioni, nel qual caso i risultati non
** sono corretti.
**
** Ad esempio se eliminamo un paio di utenti
*/
DELETE dbo.utenti
WHERE NomeUtente IN ('Steven Buchanan', 'Pedro Afonso')
GO
/*
** La seconda pagina ritorna solo 8 righe al posto di 10
*/
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** la query originale invece continua ad essere valida
*/
ALTER PROCEDURE dbo.UtentiPaginati
 @maxRighe int,
 @iPag  int
AS
DECLARE @sqlstmt nvarchar(4000)
SET @sqlstmt = 'SELECT TOP ' + CAST(@maxRighe AS varchar)
    + ' IDUtente, NomeUtente'
    + ' FROM dbo.utenti WHERE IDUtente NOT IN'
    + ' (SELECT TOP '
    + CAST(@maxRighe*(@iPag -1) AS varchar) + ' IDUtente'
    + ' FROM dbo.utenti ORDER BY IDUtente)'
    + ' ORDER BY IDUtente'
EXEC dbo.sp_executesql @sqlstmt
GO
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Supponiamo pero' di volere ora paginare non per IDUtente
** ma per NomeUtente
**
** Modifichiamo la Query di conseguenza
*/
ALTER PROCEDURE dbo.UtentiPaginati
 @maxRighe int,
 @iPag  int
AS
DECLARE @sqlstmt nvarchar(4000)
SET @sqlstmt = 'SELECT TOP ' + CAST(@maxRighe AS varchar)
    + ' IDUtente, NomeUtente'
    + ' FROM dbo.utenti WHERE NomeUtente NOT IN'
    + ' (SELECT TOP '
    + CAST(@maxRighe*(@iPag -1) AS varchar) + ' NomeUtente'
    + ' FROM dbo.utenti ORDER BY NomeUtente)'
    + ' ORDER BY NomeUtente'
EXEC dbo.sp_executesql @sqlstmt
GO
/*
** Nel piano di esecuzione e' stato inserito un operatore di sort,
** inoltre l'operazione di Index Scan in realta' e' una vera e
** propria Table Scan (lettura di tutti i valori) perche' trattandosi
** dell'indice clustered e' l'unico modo che SQL Server ha a
** disposizione per accedre ai dati.
**
** Come per le prove precedenti la prima pagina e' un caso
** particolare.
**
** |--Sort(TOP 10, ORDER BY:([utenti].[NomeUtente] ASC))
**    |--Clustered Index Scan(OBJECT:(...))
**
** Table 'utenti'. Scan count 1, logical reads 2
*/
EXEC dbo.UtentiPaginati 10, 1
GO
/*
** Per tutte le pagine seguenti il piano di esecuzione e' piu'
** complesso. La tabella deve essere scandita 2 volte ed anche
** l'operazione di sort deve essere fatta 2 volte.
** L'unico beneficio e' avendo entrambi i flussi ordinati puo'
** utilizzare un algoritmo di Merge Join.
**
** Anche in questo l'operazione di Index Scan si riferisce
** ad una Table Scan .
**
** |--Top(10)
**    |--Merge Join(Right Anti Semi Join, MERGE:(...), RESIDUAL:(...))
**       |--Stream Aggregate(GROUP BY:([utenti].[NomeUtente]))
**       |  |--Sort(TOP 10, ORDER BY:([utenti].[NomeUtente] ASC))
**       |     |--Clustered Index Scan(OBJECT:(...))
**       |--Sort(ORDER BY:([utenti].[NomeUtente] ASC))
**          |--Clustered Index Scan(OBJECT:(...))
**
** Table 'utenti'. Scan count 2, logical reads 4
**
*/
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Per migliorare le prestazioni, proviamo a creare un indice
** univoco a supporto della query.
*/
CREATE UNIQUE INDEX ak_utenti_NomeUtente ON dbo.utenti (NomeUtente)
GO
/*
** L'operazione di sort non e' piu' necessaria perche' l'indice e'
** gia' ordinato correttamente. Da notare il fatto che la query
** ritorna anche l'identificativo dell'utente e non c'e' l'operatore
** di Bookmark Lookup per andare a reperire tali valori.
**
** Questo e' dovuto al fatto che l'indice clustered e' definito proprio
** per l'identificativo utente. Gli indici secondari, in presenza di un
** indice clustered, contengono nelle pagine foglia i valori indicizzati
** dall'indice clustered per reperire eventuali informazioni da altri
** campi della tabella.
**
** In questo caso raggiunte le foglie dell'indice secondario ci sono
** gia' tutte le informazioni, in altri casi potrebbe essere necessario
** fare degli I/O extra.
**
** In ogni caso, come per le prove precedenti, la prima pagina e' un
** caso particolare.
**
** |--Top(10)
**    |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ORDERED FORWARD)
**
** Table 'utenti'. Scan count 1, logical reads 1
*/
EXEC dbo.UtentiPaginati 10, 1
GO
/*
** Per tutte le pagine seguenti il piano di esecuzione e' piu'
** complesso, comunque piu' efficiente della situazione precedente
** grazie al nuovo indice
**
** |--Top(10)
**    |--Merge Join(Left Anti Semi Join, MERGE:(...), RESIDUAL:(...))
**        |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ORDERED FORWARD)
**        |--Top(20)
**             |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ...)
**
** Table 'utenti'. Scan count 2, logical reads 2
*/
EXEC dbo.UtentiPaginati 10, 3
GO
/*
** Come per il primo esempio per la pagina 2 viene generato
** un piano di esecuzione meno efficiente.
**
** |--Top(10)
**    |--Nested Loops(Left Anti Semi Join, OUTER REFERENCES:(...))
**       |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ORDERED FORWARD)
**       |--Filter(WHERE:([utenti].[NomeUtente]=[utenti].[NomeUtente]))
**          |--Top(10)
**             |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ...)
**
** Table 'utenti'. Scan count 21, logical reads 21
*/
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Purtroppo non serve possiamo avvalerci di un indice univoco,
** va notato il fatto che creando un indice non univoco i piani
** di esecuzione sono leggermente differenti per tutti i casi
** tranne quello della pagina 1.
**
** Vengono introdotti un operatore di tipo Stream Aggregate nel
** caso delle pagine > 2, ed un operatore di tipo Row Count Spool
** nel caso della pagina 2.
**
** |--Top(10)
**    |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ORDERED FORWARD)
**
** Table 'utenti'. Scan count 1, logical reads 1
**
** |--Top(10)
**    |--Merge Join(Right Anti Semi Join, MERGE:(...), RESIDUAL:(...))
**       |--Stream Aggregate(GROUP BY:([utenti].[NomeUtente]))
**       |  |--Top(20)
**       |     |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ...)
**       |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ORDERED FORWARD)
**
** Table 'utenti'. Scan count 2, logical reads 2
**
** |--Top(10)
**    |--Nested Loops(Left Anti Semi Join, OUTER REFERENCES:(...))
**       |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ORDERED FORWARD)
**       |--Row Count Spool
**          |--Filter(WHERE:(...))
**             |--Top(10)
**                |--Index Scan(OBJECT:(ak_utenti_NomeUtente), ...)
**
** Table 'utenti'. Scan count 21, logical reads 21
**
*/
DROP INDEX dbo.utenti.ak_utenti_NomeUtente
GO
CREATE INDEX ak_utenti_NomeUtente ON dbo.utenti (NomeUtente)
GO
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 3
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Vediamo un'altro problema.
** Inseriamo alcuni Nomi Utente duplicati.
*/
INSERT dbo.utenti (NomeUtente) VALUES('Anabela Domingues')
INSERT dbo.utenti (NomeUtente) VALUES('Anabela Domingues')
INSERT dbo.utenti (NomeUtente) VALUES('Ana Trujillo')
INSERT dbo.utenti (NomeUtente) VALUES('Ann Devon')
GO
/*
** Eseguendo le due stored procedure possiamo notare che correttamente
** vengono visualizzate 3 righe con Anabela, 2 righe con Ana.
** L'errore consiste nel fatto che viene visualizzata una sola volta
** Ann nella prima pagina, in verita' dovrebbe essere visualizzata
** una seconda Ann (con IDUtente > 100) nella seconda pagina.
*/
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Per risolvere questo problema dobbiamo utilizzare la clausola
** WITH TIES con la clausola TOP n
*/
ALTER PROCEDURE dbo.UtentiPaginati
 @maxRighe int,
 @iPag  int
AS
DECLARE @sqlstmt nvarchar(4000)
SET @sqlstmt = 'SELECT TOP ' + CAST(@maxRighe AS varchar)
    + ' WITH TIES IDUtente, NomeUtente'
    + ' FROM dbo.utenti WHERE NomeUtente NOT IN'
    + ' (SELECT TOP '
    + CAST(@maxRighe*(@iPag -1) AS varchar)
    + ' WITH TIES NomeUtente'
    + ' FROM dbo.utenti ORDER BY NomeUtente)'
    + ' ORDER BY NomeUtente'
EXEC dbo.sp_executesql @sqlstmt
GO
/*
** Ora query e' corretta, ritorna due Ann, pero' abbiamo un'altro
** problema: quando chiediamo i dati per la prima pagina, nonostante
** la richiesta di 10 righe per pagina ne riceviamo 11.
*/
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** La versione definitiva che risolve anche questo problema
** purtroppo richiede la creazione di una tabella di appoggio
** dove generare una sequenza al volo
*/
ALTER PROCEDURE dbo.UtentiPaginati
 @maxRighe int,
 @iPag  int
AS
CREATE TABLE #temp(
 rownumber int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
 IDUtente int     NOT NULL,
 NomeUtente nvarchar(30)  NOT NULL
)
INSERT #temp
SELECT IDUtente, NomeUtente
FROM dbo.utenti ORDER BY NomeUtente
/* Possiamo usre la versione semplificata perche'
** sappiamo che i valori sono crescenti e non hanno
** buchi
*/
SELECT IDUtente, NomeUtente
FROM   #temp
WHERE  rownumber BETWEEN (@maxRighe * (@iPag - 1)) + 1
         AND (@maxRighe * (@iPag - 1)) + @maxRighe
ORDER BY
  rownumber
GO
/*
** Ora query e' corretta, anche se i piani di esecuzione ora sono
** completamente differenti.
**
** Il principale problema e' nel ricopiare i valori nella
** tabella temporanea per assegnare il numero riga: se la
** tabella e' di dimensioni considerevoli le prestazioni sono
** pessime.
*/
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Se la tabella e' sufficientemente piccola possiamo pensare di
** ottimizzare ulteriormente la stored procedure usando una
** variabile di tipo tabella
*/
ALTER PROCEDURE dbo.UtentiPaginati
 @maxRighe int,
 @iPag  int
AS
DECLARE @temp  table(
 rownumber int IDENTITY(1, 1) NOT NULL PRIMARY KEY,
 IDUtente int     NOT NULL,
 NomeUtente nvarchar(30)  NOT NULL
)
INSERT @temp
SELECT IDUtente, NomeUtente
FROM dbo.utenti ORDER BY NomeUtente
/* Possiamo usre la versione semplificata perche'
** sappiamo che i valori sono crescenti e non hanno
** buchi
*/
SELECT IDUtente, NomeUtente
FROM   @temp
WHERE  rownumber BETWEEN (@maxRighe * (@iPag - 1)) + 1
         AND (@maxRighe * (@iPag - 1)) + @maxRighe
ORDER BY
  rownumber
GO
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Vediamo ora l'ultima tecnica proposta da Justin Gunther
** in questo articolo
** 
http://www.sql-server-performance.com/asp_sql_server.asp
*/
ALTER PROCEDURE dbo.UtentiPaginati
 @maxRighe int,
 @iPag  int
AS
SET ROWCOUNT @maxRighe
SELECT IDUtente, NomeUtente,
  (SELECT COUNT(*)
   FROM dbo.utenti AS U2
   WHERE U2.NomeUtente <= U1.NomeUtente
  ) AS RowNumber
FROM dbo.utenti AS U1
WHERE (SELECT COUNT(*)
   FROM dbo.utenti AS U2
   WHERE U2.NomeUtente <= U1.NomeUtente
  ) > (@maxRighe * @iPag) - @maxRighe
ORDER BY
  NomeUtente
SET ROWCOUNT 0
GO
/*
** Attenzione al fatto che questa tecnica ha un problema
** simile a quello risolto con l'inclusione della clausola
** WITH TIES. In quel caso una riga spariva, in questo caso
** una riga (la 23) viene visualizzata due volte in due
** pagine diverse (il problema diventa ancora piu' serio
** a fronte di molti valori duplicati).
*/
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
GO
/*
** Con valori univoci funziona correttamente
*/
DELETE dbo.utenti WHERE NomeUtente = 'Anabela Domingues'
DELETE dbo.utenti WHERE NomeUtente = 'Anabela Domingues'
DELETE dbo.utenti WHERE NomeUtente = 'Ana Trujillo'
DELETE dbo.utenti WHERE NomeUtente = 'Ann Devon'
EXEC dbo.UtentiPaginati 10, 1
EXEC dbo.UtentiPaginati 10, 2
GO
--<CUT HERE>--
Ciao,
--
Gianluca Hotz
Backoffice MVP (SQL Server) MCP SQL Server and MCP Windows
Vice-President of the Italian User Group for SQL Server
Founding Member of the Italian User Group for .NET
http://www.ghotz.com
http://www.ugiss.org

 **    SQL Server.
Siccome sono stupido, la mia tabella del Northwind non era come
da installazione originale :-/
Quindi la
DELETE dbo.utenti
WHERE NomeUtente IN ('Steven Buchanan', 'Pedro Afonso')
Non prende due record nella pagina 2, sostituirla con
DELETE dbo.utenti
WHERE IDUtente IN (12, 17)
E' ora di andare in ferie...
Gianluca.

 MINUS di Oracle)...
Ciao Paulus,
grazie al cielo SQL Server ha smesso di avere problemi di questo tipo
dalla versione 7.0 :-) queste due query, che sono semanticamente
identiche,
vengono risolte con lo stesso piano di esecuzione.
SELECT DISTINCT pub_name
FROM pubs.dbo.publishers
WHERE EXISTS
  (SELECT *
   FROM pubs.dbo.titles
   WHERE pub_id = publishers.pub_id
     AND type = 'business')
SELECT DISTINCT pub_name
FROM pubs.dbo.publishers
WHERE pub_id IN
  (SELECT pub_id
   FROM pubs.dbo.titles
   WHERE type = 'business')
Che e' anche lo stesso di
SELECT DISTINCT P.pub_name
FROM pubs.dbo.publishers AS P
JOIN pubs.dbo.titles AS T
  ON T.pub_id = P.pub_id
WHERE type = 'business'
Per quanto riguarda poi il problema di specifico di Simone
la query non potrebbe essere scritta tout-court come
SELECT TOP (@maxRighe) IDUtente, NomeUtente
FROM dbo.utenti AS U1
WHERE NOT EXISTS
  (SELECT TOP (@maxRighe*(@iPag-1)) NomeUtente
   FROM dbo.utenti AS U2
   WHERE U2.NomeUtente = U1.NomeUtente
   ORDER BY
    U2.NomeUtente
  )
ORDER BY
 U1.NomeUtente
e neppure oppure come
SELECT TOP (@maxRighe) IDUtente, NomeUtente
FROM dbo.utenti AS U1
WHERE NOT EXISTS
  (SELECT TOP (@maxRighe*(@iPag-1)) NomeUtente
   FROM dbo.utenti AS U2
   ORDER BY
   U2.NomeUtente
  )
ORDER BY
 U1.NomeUtente
Potresti scriverla in questo modo
SELECT TOP 10 IDUtente, NomeUtente
FROM dbo.utenti AS U1
WHERE NOT EXISTS
  (SELECT *
   FROM (SELECT TOP 20 NomeUtente
      FROM dbo.utenti
      ORDER BY
      NomeUtente
     ) AS U2
      WHERE U2.NomeUtente = U1.NomeUtente
  )
ORDER BY
 U1.NomeUtente
ma l'ottimizzatore le considera identiche e genera il
medesimo piano di esecuzione.
btw SQL Server non supporta la MINUS perche' e' proprietaria
di Oracle e credo sia funzionalmente identica alla NOT EXISTS.
Forse in una futura versione supportera' le operazioni definite
dallo standard SQL92: EXCEPT (e INTERSECT).


 
Grazie dei suggerimenti e buone vacanze. :-)
Ciao,
Simone