Slow JOIN für Tabellen mit Millionen von Zeilen

8

In meiner Anwendung muss ich Tabellen mit Millionen von Zeilen verbinden. Ich habe eine Frage wie diese:

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"  

Die Tabelle "files" hat 10 Millionen Zeilen und die Tabelle "value_text" hat 40 Millionen Zeilen.

Diese Abfrage ist zu langsam. Die Ausführung dauert zwischen 40 Sekunden (15000 Ergebnisse) und 3 Minuten (65000 Ergebnisse).

Ich hatte darüber nachgedacht, die beiden Abfragen zu teilen, kann es aber nicht, weil ich manchmal nach der verbundenen Spalte (Wert) bestellen muss ...

Was kann ich machen? Ich verwende SQL Server mit Azure. Insbesondere Azure SQL-Datenbank mit Preis- / Modellstufe "PRS1 PremiumRS (125 DTUs)" .

Ich erhalte viele Daten, aber ich denke, die Internetverbindung ist kein Engpass, da ich bei anderen Abfragen auch viele Daten erhalte und diese schneller sind.

Ich habe versucht, die Client-Tabelle als Unterabfrage zu verwenden und DISTINCTmit den gleichen Ergebnissen zu entfernen .

Ich habe 1428 Zeilen in der Client-Tabelle.

zusätzliche Information

clients Tabelle:

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 Tabelle:

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 Tabelle:

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

Ausführungsplan:

Ausführungsplan

* Ich habe die Tabellen und Felder in dieser Frage zum allgemeinen Verständnis übersetzt. In diesem Bild entspricht "archivos" "files", "clientes" von "clients" und "valores_texto" von "value_text".

Ausführungsplan ohne DISTINCT:

Geben Sie hier die Bildbeschreibung ein

Ausführungsplan ohne DISTINCTund GROUP BY(etwas schneller abfragen):

Ausführungsplan

Abfragetest (Antwort von Krismorte)

Dies ist der Ausführungsplan der Abfrage, der langsamer als zuvor ist. Hier gibt mir die Abfrage über 400.000 Zeilen zurück, aber selbst wenn die Ergebnisse paginiert werden, gibt es keine Änderungen.

Ausführungsplan detaillierter: https://www.brentozar.com/pastetheplan/?id=By_UC2aBG

Langsame Abfrage

Und dies ist der Ausführungsplan der Abfrage, der schneller als zuvor ist. Hier gibt die Abfrage über 65.000 Zeilen zurück.

Ausführungsplan detaillierter: https://www.brentozar.com/pastetheplan/?id=r116e6pSM

Schnelle Abfrage

RuSSe
quelle
2
Bitte fügen Sie alle anderen Indizes hinzu, die Sie bereits zu den Tabellen hinzugefügt haben. Führen Sie normalerweise die gesamte Abfrage so aus oder haben Sie eine where-Klausel, die Sie uns nicht anzeigen?
Jonathan Fite

Antworten:

7

Ich denke, Sie brauchen diesen Index (wie Krismorte vorgeschlagen hat ):

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

Der folgende Index ist wahrscheinlich nicht erforderlich, da Sie anscheinend über einen geeigneten vorhandenen Index verfügen (in der Frage nicht erwähnt), der Vollständigkeit halber füge ich ihn jedoch hinzu:

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

Drücken Sie die Abfrage aus als:

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);

Dies sollte einen Ausführungsplan wie folgt ergeben:

erwarteter Plan

Das OPTION (RECOMPILE)ist optional. Fügen Sie nur hinzu, wenn Sie feststellen, dass die ideale Planform für verschiedene Parameterwerte unterschiedlich ist. Es gibt andere mögliche Lösungen für solche "Parameter-Sniffing" -Probleme.

Mit dem neuen Index können Sie auch feststellen, dass der ursprüngliche Abfragetext einen sehr ähnlichen Plan erzeugt, auch mit guter Leistung.

Möglicherweise müssen Sie auch die Statistiken in der filesTabelle aktualisieren , da die Schätzung im mitgelieferten Plan für cid = 19nicht korrekt ist:

Ungenaue Schätzung


Nach dem Aktualisieren der Statistiken in der Dateitabelle funktioniert die Abfrage in allen Fällen sehr schnell. Wenn ich in Zukunft weitere Felder in der Tabelle "Dateien" hinzufüge, sollte ich den Index aktualisieren oder so?

Wenn Sie der Dateitabelle weitere Spalten hinzufügen (und diese in Ihrer Abfrage verwenden / zurückgeben), müssen Sie sie dem Index hinzufügen (mindestens als eingeschlossene Spalten), damit der Index "bedeckt" bleibt. Andernfalls scannt der Optimierer möglicherweise die Dateitabelle, anstatt die im Index nicht vorhandenen Spalten nachzuschlagen. Sie können cidstattdessen auch einen Teil eines Clustering-Index für diese Tabelle erstellen. Es hängt davon ab, ob. Stellen Sie eine neue Frage, wenn Sie diese Punkte klären möchten.

Paul White 9
quelle
1

Nun, diese Abfrage war ziemlich schwierig. Ich ändere die Filterordnungen und erstelle zwei Deckungsindizes

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

die Abfrage

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"  

Ergebnisabfrageplan

Geben Sie hier die Bildbeschreibung ein

Versuchen Sie dies, ich hoffe, das wird genug sein

Krismorte
quelle
Zunächst habe ich die where-Klausel geändert, da ich auch die Zeilen mit "Dateien" benötige, in denen id_campo null ist. "where" cl "." id "= 19 AND (" vtVE2 "." id_campo "= 628 OR" vtVE2 "." id_campo "IS NULL)" Ich weiß nicht, ob es eine bessere Lösung gibt. Daher habe ich Ihre Abfrage mit verschiedenen Parametern getestet und das Ergebnis ist in einigen Fällen sehr gut, in anderen Fällen ist das Ergebnis jedoch schlechter als zuvor. Ich habe die Ausführungspläne im ersten Beitrag hinzugefügt.
RuSSe