Quantcast
Channel: kerrubin's blog » SGBDR
Viewing all articles
Browse latest Browse all 8

SQL-Server – RANK et ROW_NUMBER

$
0
0

Récemment, j’ai du faire un petit exercice relativement simple : importer des données en base depuis un fichier CSV.
Avec le bulk insert, c’est assez simple :

BULK INSERT #Bulk
FROM 'C:\Temp\FichierAImporter.csv' 
WITH 
(
	FIELDTERMINATOR = ';',
	ROWTERMINATOR = '\n',
	CODEPAGE = 'ACP',
	ROWS_PER_BATCH = 35000
)

Là où ça s’est compliqué, c’est la génération des identifiants fonctionnels avant leur insertion dans les "vraies" tables.

Pour simplifier un peu, voici le cas.
Je dois importer les nouvelles adresses de clients.
J’ai deux champs : Client et Adresse qui sont chargées depuis le fichier CSV.

Mais surtout, j’ai deux identifiants fonctionnels à créer.
Le premier est un identifiant interne, de la forme "FR00000000" dont la partie numérique est incrémentée pour chaque entrée. Le prochain identifiant est lisible depuis une table.
Le second identifiant est externe, c’est un numérique incrémenté par client.
C’est à dire que le client #1 possèdant déjà 50 adresses, la suivants sera la 51, la prochaine pour le client #2 est la 6ème.

La bonne vieille méthode, c’est de faire des boucles pour alimenter tout le bazar.
Mais comme on est dans une base de données, c’est quand même nettement mieux de faire de l’ensembliste.
C’est là que RANK et ROW_NUMBER viennent à la rescousse !

 

Les choix moches

 

La première solution, la plus violent et aussi la plus moche, c’est de faire une boucle avec un curseur.
C’est mal.
C’est peu performant et ça parcours les lignes un par une. Donc s’il y en a beaucoup, ça va être juste délirant.

La seconde solution, c’est de faire un while.
C’est mal.
Grosso modo pour les mêmes raisons que le curseur.

Un SGBDR, c’est quand même pour traiter des données en masse, pas à l’unité.

 

Le coup de la table variable

 

Une autre idée (d’un collègue qui a eu un cas légèrement différent) est de passer par une table avec tous les identifiants générés.
Inspiré de ce post : SQL SELECT to get the first N positive integers.

DECLARE @taillePlage INT = 5;
DECLARE @prefixe CHAR(2) = 'FR';
DECLARE @debutPlage NUMERIC(9,0);

SET @debutPlage = 12345;

DECLARE @IdInternes TABLE (
      NUMART VARCHAR(10)
);

WITH plage AS
(
            SELECT  0 AS num
      UNION ALL
            SELECT  num + 1
            FROM plage
            WHERE num < @taillePlage - 1
)
INSERT INTO @IdInternes(NUMART)
SELECT @prefixe 
	+ REPLICATE('0', 8 - LEN(@debutPlage + num)) 
	+ CAST(@debutPlage + num AS VARCHAR)
FROM plage;

SELECT * FROM @IdInternes;

Ici, la CTE (Common Table Expression) est assez utile et permet d’éviter une boucle disgracieuse.
C’est plutôt bien, mais ne réponds pas encore à mon besoin (ici, la difficulté va être d’affecter les identifiants, le problème n’est donc que décalé).

 

Le tout en un

 

La solution que j’ai (mis du temps) à apporter est la suivante :
(oui, c’est tout d’un bloc, mais commenté en même temps !)

-- Simulacre que Bulk Insert
SELECT T.Client, T.Adresse
INTO #Bulk
FROM (
	SELECT 'Client #1' AS Client, 'Adresse #1' AS Adresse
	UNION
	SELECT 'Client #1', 'Adresse #2'
	UNION
	SELECT 'Client #1', 'Adresse #3'
	UNION 
	SELECT 'Client #2', 'Adresse #1'
	UNION
	SELECT 'Client #2', 'Adresse #2'
	UNION
	SELECT 'Client #3', 'Adresse #1'
	UNION
	SELECT 'Client #4', 'Adresse #1'
	UNION
	SELECT 'Client #5', 'Adresse #1'
) T

--------------------------------------------------------------
-- Ajout des colonnes qui vont nous être utiles.
-- Pour réaliser les calculs suivants.

-- Pour le Seed, on alimente directement le champ.
-- Il est utilisé pour identifier de façon unique la ligne.
ALTER TABLE #Bulk
	ADD Seed UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL
	
ALTER TABLE #Bulk
	ADD RowId INT NULL
	
ALTER TABLE #Bulk
	ADD RankId INT NULL
	
ALTER TABLE #Bulk
	ADD IdInterne VARCHAR(10)
	
ALTER TABLE #Bulk
	ADD IdExterne INT
	
--------------------------------------------------------------
-- Calcul des ROW_NUMBER et RANK
UPDATE #Bulk
SET #Bulk.RowId = t.ROW_NUMBER,
	#Bulk.RankId = t.RANK
FROM #Bulk
JOIN (
	SELECT
		-- Le ROW_NUMBER est utilisé indifféremment du client,
		-- on l’ordonne juste pour la forme, donc.
		ROW_NUMBER() OVER(ORDER BY Client) - 1 AS ROW_NUMBER,
		-- Le RANK est par client,
		-- il faut donc les grouper ensemble via PARTITION
		-- et ordonner sur un critère (ici, Adresse).
		RANK() OVER(PARTITION BY Client ORDER BY Adresse) AS RANK,
		#Bulk.Seed
	FROM #Bulk
	) AS t ON t.Seed = #Bulk.Seed
-- Le ROW_NUMBER numérote à partir de 1, mais comme on va l'utiliser
-- pour additionner à partir du prochain identifiant on va partir de zéro,
--  donc soustraire 1.
-- Le RANK numérote également à partir de 1,
-- mais ici, on additionnera à partir de l'existant.
	
--------------------------------------------------------------
-- Calcul de l'identifiant externe.
-- La variable @NextIdInterne correspond au prochain identifiant.
DECLARE @NextIdInterne INT = 12345;
UPDATE #Bulk
SET IdInterne = 'FR' 
	+ REPLICATE('0', 8 - LEN(@NextIdInterne + #Bulk.RowId))
	+ CAST(@NextIdInterne + #Bulk.RowId AS VARCHAR);
-- Le REPLICATE va permettre d'ajouter autant de 0 que requit 
-- pour avoir des identifiants de taille fixe

--------------------------------------------------------------
-- Calcul de l'identifiant externe.
-- Common Table Expression simule la récupération de données 
-- depuis une table des adresses.

-- NOTE: l'instruction précédente DOIT finir par un ; 
-- pour que la CTE fonctionne
WITH IdentifiantsExternes AS (
	SELECT 'Client #1' AS Client, 50 AS Identifiant
	UNION
	SELECT 'Client #2' AS Client, 5 AS Identifiant
	UNION
	SELECT 'Client #3' AS Client, 1 AS Identifiant
	UNION
	SELECT 'Client #4' AS Client, 10 AS Identifiant
)
-- Une CTE DOIT être utilisée à l'instruction qui la suit.
-- Elle peut donc être utilisée directement ou son résultat 
-- placé dans une table (temporaire/variable...).
UPDATE #Bulk
SET IdExterne = ISNULL(Identifiant, 0) + RankId
FROM #Bulk
LEFT JOIN IdentifiantsExternes ON #Bulk.Client = IdentifiantsExternes.Client

--------------------------------------------------------------
SELECT * FROM #Bulk
DROP TABLE #Bulk

Pour avoir quelques exemples : T-SQL RANK() , DENSE_RANK() , NTILE(), ROW_NUMBER().

 

Cadeau bonux

 

J’ai eu une erreur de typo dans une requête qui a plus ou moins cette tête :

SELECT FirstName, LastName
INTO TempUsers -- Attendu : #TempUsers
FROM Users

Et là, on a eu la surprise de voir que le SELECT INTO ne faisait pas que servait pas qu’à créer des tables temporaires.
Et oui, dans ce cas, il m’a créé une jolie table physique avec le résultat de ma requête et surtout une structure rigoureusement identique (sans les relations PF/FK cependant).
Je ne sais pas vous, mais moi, je savais pas ! ^^



Viewing all articles
Browse latest Browse all 8

Latest Images

Trending Articles





Latest Images