web-dev-qa-db-fra.com

JOIN lent sur des tables avec des millions de lignes

Dans mon application, je dois joindre des tables avec des millions de lignes. J'ai une requête comme celle-ci:

SELECT DISTINCT "f"."id" AS "FileId"  
, "f"."name" AS "FileName"  
, "f"."year" AS "FileYear"  
, "vt"."value" AS "value"  
FROM files "f"  
JOIN "clients" "cl" ON("f"."cid" = "cl"."id" AND "cl"."id" = 10)  
LEFT JOIN "value_text" "vt" ON ("f"."id" = "vt"."id_file" AND "vt"."id_field" = 65739)  
GROUP BY "f"."id", "f"."name", "f"."year", "vt"."value"  

La table "files" a 10 millions de lignes et la table "value_text" a 40 millions de lignes.

Cette requête est trop lente, elle prend entre 40s (15000 résultats) - 3 minutes (65000 résultats) pour être exécutée.

J'avais pensé à diviser les deux requêtes, mais je ne peux pas car parfois j'ai besoin de trier par la colonne jointe (valeur) ...

Que puis-je faire? J'utilise SQL Server avec Azure. Plus précisément, Azure SQL Database avec tarification/niveau de modèle "PRS1 PremiumRS (125 DTU)" .

Je reçois beaucoup de données, mais je pense que la connexion Internet n'est pas un goulot d'étranglement, car dans d'autres requêtes, je reçois également beaucoup de données et elles sont plus rapides.

J'ai essayé d'utiliser la table client comme sous-requête et de supprimer DISTINCT avec les mêmes résultats.

J'ai 1428 lignes dans la table client.

Information additionnelle

clients table:

CREATE TABLE [dbo].[clients](
    [id] [bigint] IDENTITY(1,1) NOT NULL,
    [code] [nvarchar](70) NOT NULL,
    [password] [nchar](40) NOT NULL,
    [name] [nvarchar](150) NOT NULL DEFAULT (N''),
    [email] [nvarchar](255) NULL DEFAULT (NULL),
    [entity] [int] NOT NULL DEFAULT ((0)),
    [users] [int] NOT NULL DEFAULT ((0)),
    [status] [varchar](8) NOT NULL DEFAULT ('inactive'),
    [created] [datetime2](7) NULL DEFAULT (getdate()),
    [activated] [datetime2](7) NULL DEFAULT (getdate()),
    [client_type] [varchar](10) NOT NULL DEFAULT ('normal'),
    [current_size] [bigint] NOT NULL DEFAULT ((0)),
CONSTRAINT [PK_clients_id] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON),
CONSTRAINT [clients$code] UNIQUE NONCLUSTERED 
(
    [code] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)

files table:

CREATE TABLE [dbo].[files](
    [id] [bigint] IDENTITY(1,1) NOT NULL,
    [cid] [bigint] NOT NULL DEFAULT ((0)),
    [eid] [bigint] NOT NULL DEFAULT ((0)),
    [year] [bigint] NOT NULL DEFAULT ((0)),
    [name] [nvarchar](255) NOT NULL DEFAULT (N''),
    [extension] [int] NOT NULL DEFAULT ((0)),
    [size] [bigint] NOT NULL DEFAULT ((0)),
    [id_doc] [bigint] NOT NULL DEFAULT ((0)),
    [created] [datetime2](7) NULL DEFAULT (getdate())
CONSTRAINT [PK_files_id] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON),
 CONSTRAINT [files$estructure_unique] UNIQUE NONCLUSTERED 
(
    [year] ASC,
    [name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)

GO

ALTER TABLE [dbo].[files]  WITH NOCHECK ADD  CONSTRAINT [FK_files_client] FOREIGN KEY([cid])
REFERENCES [dbo].[clients] ([id])
ON UPDATE CASCADE
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[files] CHECK CONSTRAINT [FK_files_client]
GO

value_text table:

CREATE TABLE [dbo].[value_text](
    [id] [bigint] IDENTITY(1,1) NOT NULL,
    [id_file] [bigint] NOT NULL DEFAULT ((0)),
    [id_field] [bigint] NOT NULL DEFAULT ((0)),
    [value] [nvarchar](255) NULL DEFAULT (NULL),
    [id_doc] [bigint] NULL DEFAULT (NULL)
 CONSTRAINT [PK_value_text_id] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)

GO

ALTER TABLE [dbo].[value_text]  WITH NOCHECK ADD  CONSTRAINT [FK_valuesT_field] FOREIGN KEY([id_field])
REFERENCES [dbo].[fields] ([id])
ON UPDATE CASCADE
ON DELETE CASCADE
GO

ALTER TABLE [dbo].[value_text] CHECK CONSTRAINT [FK_valuesT_field]
GO

Plan d'exécution:

Execution plan

* J'ai traduit les tableaux et les champs de cette question pour une compréhension générale. Dans cette image, "archivos" est l'équivalent de "fichiers", "clientes" de "clients" et "valores_texto" de "value_text".

Plan d'exécution sans DISTINCT:

enter image description here

Plan d'exécution sans DISTINCT et GROUP BY (interrogez un peu plus vite):

Execution Plan

Test de requête (réponse Krismorte)

Il s'agit du plan d'exécution de la requête qui est plus lent qu'auparavant. Ici, la requête me renvoie plus de 400 000 lignes, mais même en paginant les résultats, il n'y a pas de changements.

Plan d'exécution plus détaillé: https://www.brentozar.com/pastetheplan/?id=By_UC2aBG

Slow Query

Et c'est le plan d'exécution de la requête qui est plus rapide qu'auparavant. Ici, la requête renvoie plus de 65 000 lignes.

Plan d'exécution plus détaillé: https://www.brentozar.com/pastetheplan/?id=r116e6pSM

Fast Query

8
RuSSe

Je pense que vous avez besoin de cet index (comme Krismorte a suggéré ):

CREATE NONCLUSTERED INDEX [IX dbo.value_text id_file, id_field, value]
ON dbo.value_text (id_file, id_field, [value]);

L'index suivant n'est probablement pas requis car vous semblez avoir un index existant approprié (non mentionné dans la question) mais je l'inclus pour être complet:

CREATE NONCLUSTERED INDEX [IX dbo.files cid (id, year, name)]
ON dbo.files (cid)
INCLUDE
(
    id,
    [year],
    [name]
);

Exprimez la requête comme suit:

SELECT
    FileId = F.id,
    [FileName] = F.[name],
    FileYear = F.[year],
    V.[value]
FROM dbo.files AS F
JOIN dbo.clients AS C
    ON C.id = F.cid
OUTER APPLY
(
        SELECT DISTINCT
            VT.[value]
        FROM dbo.value_text AS VT
        WHERE
            VT.id_file = F.id
            AND VT.id_field = 65739
) AS V
WHERE 
    C.id = 10
OPTION (RECOMPILE);

Cela devrait donner un plan d'exécution comme:

expected plan

La OPTION (RECOMPILE) est facultative. Ajoutez uniquement si vous trouvez que la forme de plan idéale est différente pour différentes valeurs de paramètre. Il existe d'autres solutions possibles à ces problèmes de "détection de paramètres".

Avec le nouvel index, vous pouvez également trouver que le texte de requête d'origine produit un plan très similaire, également avec de bonnes performances.

Vous devrez peut-être également mettre à jour les statistiques sur la table files, car l'estimation dans le plan fourni pour cid = 19 N'est pas exacte:

Inaccurate estimate


Après la mise à jour des statistiques sur la table des fichiers, la requête fonctionne très rapidement dans tous les cas. Si à l'avenir j'ajoute plus de champs dans la table "fichiers", dois-je mettre à jour l'index ou quelque chose?

Si vous ajoutez plus de colonnes à la table de fichiers (et que vous les utilisez/renvoyez dans votre requête), vous devrez les ajouter à l'index (au minimum en tant que colonnes incluses) pour garder l'index "couvrant". Sinon, l'optimiseur peut choisir d'analyser la table des fichiers plutôt que de rechercher les colonnes non présentes dans l'index. Vous pouvez également choisir d'intégrer cid à un index de clustering sur cette table. Ça dépend. Posez une nouvelle question si vous souhaitez des éclaircissements sur ces points.

7
Paul White 9

Eh bien, cette requête a été assez difficile, je modifie les ordres de filtrage et crée deux index de couverture

create index ix_stackexchange on [value_text] (id_file,id_field,value)
create index ix_stackexchange on [files] (id,cid,name,year)

la requête

SELECT DISTINCT "f"."id" AS "FileId"  
, "f"."name" AS "FileName"  
, "f"."year" AS "FileYear"  
, "vt"."value" AS "value"  
FROM files "f"  
JOIN "clients" "cl" ON("f"."cid" = "cl"."id" )  
LEFT JOIN "value_text" "vt" ON ("f"."id" = "vt"."id_file" )  
where "cl"."id" = 10 AND "vt"."id_field" = 65739
GROUP BY "f"."id", "f"."name", "f"."year", "vt"."value"  

Plan de requête de résultat

enter image description here

Essayez ceci, j'espère que ce sera suffisant

1
Krismorte