Erstellen Sie einen Planungsleitfaden zum Zwischenspeichern des CTE-Ergebnisses (Lazy Spool)


19

Normalerweise erstelle ich Planhinweise, indem ich zuerst eine Abfrage erstelle, die den richtigen Plan verwendet, und diese dann in die ähnliche Abfrage kopiere, die dies nicht tut. Dies ist jedoch manchmal schwierig, insbesondere wenn die Abfrage nicht genau gleich ist. Was ist der richtige Weg, um Planungsleitfäden von Grund auf neu zu erstellen?

SQLKiwi hat das Erstellen von Plänen in SSIS erwähnt. Gibt es eine Möglichkeit oder ein nützliches Tool, um beim Erstellen eines guten Plans für SQL Server zu helfen?

Die spezifische fragliche Instanz ist dieser CTE: SQLFiddle

with cte(guid,other) as (
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other;

Gibt es ANY Art und Weise das Ergebnis kommt mit genau drei verschieden zu machen guids und nicht mehr? Ich hoffe, dass wir in Zukunft Fragen besser beantworten können, indem wir Planungshandbücher mit CTE-Abfragen einbeziehen, auf die mehrmals verwiesen wird, um einige SQL Server-CTE-Macken zu überwinden.


Antworten:


14

Gibt es eine Möglichkeit, das Ergebnis mit genau 3 verschiedenen Anleitungen und nicht mehr zu erstellen? Ich hoffe, dass wir in Zukunft Fragen besser beantworten können, indem wir Planungshandbücher mit CTE-Abfragen einbeziehen, auf die mehrmals verwiesen wird, um einige SQL Server-CTE-Macken zu überwinden.

Nicht heute. Nicht rekursive allgemeine Tabellenausdrücke (Common Table Expressions, CTEs) werden als Definitionen der Inline-Ansicht behandelt und vor der Optimierung an jeder Stelle, auf die verwiesen wird (genau wie bei regulären Ansichtsdefinitionen), in die logische Abfragestruktur erweitert. Der logische Baum für Ihre Abfrage lautet:

LogOp_OrderByCOL: Union1007 ASC COL: Union1015 ASC 
    LogOp_Project COL: Union1006 COL: Union1007 COL: Union1014 COL: Union1015
        LogOp_Join
            LogOp_ViewAnchor
                LogOp_UnionAll
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const

            LogOp_ViewAnchor
                LogOp_UnionAll
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const

Beachten Sie die beiden View-Anker und die sechs Aufrufe der intrinsischen Funktion, newidbevor mit der Optimierung begonnen wird. Trotzdem sind viele Leute der Meinung, dass der Optimierer in der Lage sein sollte, zu identifizieren, dass die erweiterten Teilbäume ursprünglich ein einzelnes referenziertes Objekt waren, und dies entsprechend zu vereinfachen. Es gab auch mehrere Connect-Anforderungen , um die explizite Materialisierung eines CTE oder einer abgeleiteten Tabelle zu ermöglichen.

Bei einer allgemeineren Implementierung sollte der Optimierer erwägen, beliebige allgemeine Ausdrücke zu materialisieren, um die Leistung zu verbessern ( CASEeine Unterabfrage ist ein weiteres Beispiel, bei dem heute Probleme auftreten können). Microsoft Research veröffentlichte bereits 2007 ein Papier (PDF) zu diesem Thema, das bislang jedoch nicht implementiert wurde. Derzeit beschränken wir uns auf die explizite Materialisierung mit Tabellenvariablen und temporären Tabellen.

SQLKiwi hat das Erstellen von Plänen in SSIS erwähnt. Gibt es eine Möglichkeit oder ein nützliches Tool, um beim Erstellen eines guten Plans für SQL Server zu helfen?

Dies war nur ein Wunschdenken von meiner Seite und ging weit über die Idee der Änderung von Planungsleitfäden hinaus. Grundsätzlich ist es möglich, ein Tool zur direkten Bearbeitung von Showplan-XML zu schreiben, aber ohne spezielle Optimierungsinstrumente wäre die Verwendung des Tools wahrscheinlich eine frustrierende Erfahrung für den Benutzer (und der Entwickler wird darüber nachdenken).

Im speziellen Kontext dieser Frage wäre ein solches Tool immer noch nicht in der Lage, die CTE-Inhalte auf eine Weise zu materialisieren, die von mehreren Verbrauchern verwendet werden könnte (um in diesem Fall beide Eingaben dem Cross-Join zuzuführen). Das Optimierungs- und Ausführungsmodul unterstützt Spools für mehrere Konsumenten, jedoch nur für bestimmte Zwecke - von denen keines für dieses bestimmte Beispiel geeignet sein kann.

Obwohl ich nicht sicher bin, habe ich eine ziemlich starke Vermutung, dass die RelOps verfolgt werden können (Nested Loop, Lazy Spool), auch wenn die Abfrage nicht genau mit dem Plan übereinstimmt - zum Beispiel, wenn Sie 4 und 5 zum CTE hinzugefügt haben Es wird weiterhin derselbe Plan verwendet (scheinbar auf SQL Server 2012 RTM Express getestet).

Hier besteht ein angemessenes Maß an Flexibilität. Die breite Form des XML-Plans wird verwendet, um die Suche nach einem endgültigen Plan zu leiten (obwohl viele Attribute vollständig ignoriert werden, z. B. der Partitionierungstyp bei Börsen), und die normalen Suchregeln werden ebenfalls erheblich gelockert. Beispielsweise wird das vorzeitige Bereinigen von Alternativen aus Kostengründen deaktiviert, die explizite Einführung von Cross-Joins ist zulässig, und skalare Operationen werden ignoriert.

Es gibt zu viele Details in in die Tiefe zu gehen, aber die Platzierung der Filter und Compute Skalare kann nicht erzwungen werden, und Prädikate der Form column = valueverallgemeinert so ein Plan enthalten X = 1oder X = @Xkann auf eine Abfrage angewendet werden , enthalten X = 502oder X = @Y. Diese besondere Flexibilität kann bei der Suche nach einem natürlichen Plan für die Erzwingung von Gewalt sehr hilfreich sein.

Im konkreten Beispiel kann die Konstante Union All immer als Konstantenscan implementiert werden. Die Anzahl der Eingänge in die Union All spielt keine Rolle.


3

Es gibt keine Möglichkeit (SQL Server-Versionen bis 2012), eine einzelne Spool für beide Vorkommen des CTE erneut zu verwenden. Details finden Sie in der Antwort von SQLKiwi. Weiter unten gibt es zwei Möglichkeiten, den CTE zweimal zu materialisieren, was für die Art der Abfrage unvermeidbar ist. Beide Optionen führen zu einer eindeutigen Netto-Guid-Anzahl von 6.

Der Link von Martins Kommentar zu Quassnois Site in einem Blog über den Plan, einen CTE zu leiten, war eine teilweise Inspiration für diese Frage. Es beschreibt eine Möglichkeit, einen CTE für den Zweck einer korrelierten Unterabfrage zu materialisieren, auf die nur einmal verwiesen wird, obwohl die Korrelation dazu führen kann, dass sie mehrmals ausgewertet wird. Dies gilt nicht für die Abfrage in der Frage.

Option 1 - Planungshandbuch

Ausgehend von der Antwort von SQLKiwi habe ich den Leitfaden auf ein Minimum reduziert, das immer noch funktioniert. Beispielsweise ConstantScanlisten die Knoten nur zwei Skalaroperatoren auf, die auf eine beliebige Anzahl erweitert werden können.

;with cte(guid,other) as (
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other
OPTION(USE PLAN
N'<?xml version="1.0" encoding="utf-16"?>
<ShowPlanXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Version="1.2" Build="11.0.2100.60" xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan">
  <BatchSequence>
    <Batch>
      <Statements>
        <StmtSimple StatementCompId="1" StatementEstRows="1600" StatementId="1" StatementOptmLevel="FULL" StatementOptmEarlyAbortReason="GoodEnoughPlanFound" StatementSubTreeCost="0.0444433" StatementText="with cte(guid,other) as (&#xD;&#xA;  select newid(),1 union all&#xD;&#xA;  select newid(),2 union all&#xD;&#xA;  select newid(),3&#xD;&#xA;select a.guid, a.other, b.guid guidb, b.other otherb&#xD;&#xA;from cte a&#xD;&#xA;cross join cte b&#xD;&#xA;order by a.other, b.other;&#xD;&#xA;" StatementType="SELECT" QueryHash="0x43D93EF17C8E55DD" QueryPlanHash="0xF8E3B336792D84" RetrievedFromCache="true">
          <StatementSetOptions ANSI_NULLS="true" ANSI_PADDING="true" ANSI_WARNINGS="true" ARITHABORT="true" CONCAT_NULL_YIELDS_NULL="true" NUMERIC_ROUNDABORT="false" QUOTED_IDENTIFIER="true" />
          <QueryPlan NonParallelPlanReason="EstimatedDOPIsOne" CachedPlanSize="96" CompileTime="13" CompileCPU="13" CompileMemory="1152">
            <MemoryGrantInfo SerialRequiredMemory="0" SerialDesiredMemory="0" />
            <OptimizerHardwareDependentProperties EstimatedAvailableMemoryGrant="157240" EstimatedPagesCached="1420" EstimatedAvailableDegreeOfParallelism="1" />
            <RelOp AvgRowSize="47" EstimateCPU="0.006688" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="1600" LogicalOp="Inner Join" NodeId="0" Parallel="false" PhysicalOp="Nested Loops" EstimatedTotalSubtreeCost="0.0444433">
              <OutputList>
                <ColumnReference Column="Union1163" />
              </OutputList>
              <Warnings NoJoinPredicate="true" />
              <NestedLoops Optimized="false">
                <RelOp AvgRowSize="27" EstimateCPU="0.000432115" EstimateIO="0.0112613" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Sort" NodeId="1" Parallel="false" PhysicalOp="Sort" EstimatedTotalSubtreeCost="0.0117335">
                  <OutputList>
                    <ColumnReference Column="Union1080" />
                    <ColumnReference Column="Union1081" />
                  </OutputList>
                  <MemoryFractions Input="0" Output="0" />
                  <Sort Distinct="false">
                    <OrderBy>
                      <OrderByColumn Ascending="true">
                        <ColumnReference Column="Union1081" />
                      </OrderByColumn>
                    </OrderBy>
                    <RelOp AvgRowSize="27" EstimateCPU="4.0157E-05" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Constant Scan" NodeId="2" Parallel="false" PhysicalOp="Constant Scan" EstimatedTotalSubtreeCost="4.0157E-05">
                      <OutputList>
                        <ColumnReference Column="Union1080" />
                        <ColumnReference Column="Union1081" />
                      </OutputList>
                      <ConstantScan>
                        <Values>
                          <Row>
                            <ScalarOperator ScalarString="newid()">
                              <Intrinsic FunctionName="newid" />
                            </ScalarOperator>
                            <ScalarOperator ScalarString="(1)">
                              <Const ConstValue="(1)" />
                            </ScalarOperator>
                          </Row>
                          <Row>
                            <ScalarOperator ScalarString="newid()">
                              <Intrinsic FunctionName="newid" />
                            </ScalarOperator>
                            <ScalarOperator ScalarString="(2)">
                              <Const ConstValue="(2)" />
                            </ScalarOperator>
                          </Row>
                        </Values>
                      </ConstantScan>
                    </RelOp>
                  </Sort>
                </RelOp>
                <RelOp AvgRowSize="27" EstimateCPU="0.0001074" EstimateIO="0.01" EstimateRebinds="0" EstimateRewinds="39" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Lazy Spool" NodeId="83" Parallel="false" PhysicalOp="Table Spool" EstimatedTotalSubtreeCost="0.0260217">
                  <OutputList>
                    <ColumnReference Column="Union1162" />
                    <ColumnReference Column="Union1163" />
                  </OutputList>
                  <Spool>
                    <RelOp AvgRowSize="27" EstimateCPU="0.000432115" EstimateIO="0.0112613" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Sort" NodeId="84" Parallel="false" PhysicalOp="Sort" EstimatedTotalSubtreeCost="0.0117335">
                      <OutputList>
                        <ColumnReference Column="Union1162" />
                        <ColumnReference Column="Union1163" />
                      </OutputList>
                      <MemoryFractions Input="0" Output="0" />
                      <Sort Distinct="false">
                        <OrderBy>
                          <OrderByColumn Ascending="true">
                            <ColumnReference Column="Union1163" />
                          </OrderByColumn>
                        </OrderBy>
                        <RelOp AvgRowSize="27" EstimateCPU="4.0157E-05" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Constant Scan" NodeId="85" Parallel="false" PhysicalOp="Constant Scan" EstimatedTotalSubtreeCost="4.0157E-05">
                          <OutputList>
                            <ColumnReference Column="Union1162" />
                            <ColumnReference Column="Union1163" />
                          </OutputList>
                          <ConstantScan>
                            <Values>
                              <Row>
                                <ScalarOperator ScalarString="newid()">
                                  <Intrinsic FunctionName="newid" />
                                </ScalarOperator>
                                <ScalarOperator ScalarString="(1)">
                                  <Const ConstValue="(1)" />
                                </ScalarOperator>
                              </Row>
                              <Row>
                                <ScalarOperator ScalarString="newid()">
                                  <Intrinsic FunctionName="newid" />
                                </ScalarOperator>
                                <ScalarOperator ScalarString="(2)">
                                  <Const ConstValue="(2)" />
                                </ScalarOperator>
                              </Row>
                            </Values>
                          </ConstantScan>
                        </RelOp>
                      </Sort>
                    </RelOp>
                  </Spool>
                </RelOp>
              </NestedLoops>
            </RelOp>
          </QueryPlan>
        </StmtSimple>
      </Statements>
    </Batch>
  </BatchSequence>
</ShowPlanXML>'
);

Option 2 - Remote-Scan

Durch Erhöhen des Abfrageaufwands und Einführen eines Fernscans wird das Ergebnis erzielt.

with cte(guid,other) as (
  select *
  from OPENQUERY([TESTSQL\V2012], '
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3') x)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other;

2

Im Ernst, Sie können XML-Ausführungspläne nicht von Grund auf neu schneiden. Ihre Erstellung mit SSIS ist Science-Fiction. Ja, es ist alles XML, aber sie stammen aus verschiedenen Universen. Wenn er sich Pauls Blog zu diesem Thema ansieht , sagt er "viel in der Weise, wie es SSIS erlaubt ...", also haben Sie möglicherweise falsch verstanden? Ich glaube nicht, dass er sagt "Verwenden Sie SSIS, um Pläne zu erstellen", sondern "Wäre es nicht großartig, wenn Sie Pläne mit einer Drag & Drop-Oberfläche wie SSIS erstellen könnten ". Für eine sehr einfache Abfrage könnte man das vielleicht gerade noch schaffen, aber es ist eine Strecke, möglicherweise sogar eine Zeitverschwendung. Beschäftigte Arbeit, könnte man sagen.

Wenn ich einen Plan für einen USE PLAN-Hinweis oder eine Plananleitung erstelle, habe ich einige Ansätze. Beispielsweise kann ich Datensätze aus Tabellen entfernen (z. B. auf einer Kopie der Datenbank), um die Statistik zu beeinflussen und den Optimierer zu einer anderen Entscheidung zu bewegen. Ich habe auch Tabellenvariablen anstelle der gesamten Tabelle in der Abfrage verwendet, sodass das Optimierungsprogramm davon ausgeht, dass jede Tabelle 1 Datensatz enthält. Ersetzen Sie dann im generierten Plan alle Tabellenvariablen durch die ursprünglichen Tabellennamen und tauschen Sie sie als Plan aus. Eine andere Möglichkeit wäre, die WITH STATS_STREAM-Option von UPDATE STATISTICS zu verwenden, um Statistiken zu fälschen. Dies ist die Methode, die beim Klonen von Nur-Statistik-Kopien von Datenbanken verwendet wird, z

UPDATE STATISTICS 
    [dbo].[yourTable]([PK_yourTable]) 
WITH 
    STATS_STREAM = 0x0100etc, 
    ROWCOUNT = 10000, 
    PAGECOUNT = 93

Ich habe in der Vergangenheit einige Zeit mit XML-Ausführungsplänen verbracht und festgestellt, dass SQL am Ende einfach "Ich benutze das nicht" lautet und die Abfrage so ausführt, wie es ohnehin sein soll.

Für Ihr spezielles Beispiel sind Sie sich sicher bewusst, dass Sie die Zeilenanzahl 3 oder TOP 3 in der Abfrage verwenden könnten, um dieses Ergebnis zu erhalten, aber ich denke, das ist nicht Ihr Punkt. Die richtige Antwort wäre wirklich: Verwenden Sie eine temporäre Tabelle. Ich würde dem zustimmen:) Eine falsche Antwort wäre: "Verbringen Sie Stunden oder Tage damit, Ihren eigenen benutzerdefinierten XML-Ausführungsplan zu zerschneiden, und versuchen Sie, den Optimierer dazu zu bringen, eine langsame Spule für den CTE zu erstellen, die möglicherweise ohnehin nicht funktioniert. Das würde schlau aussehen wäre aber auch unmöglich zu pflegen ".

Ich versuche nicht, dort unkonstruktiv zu sein, nur meine Meinung - hoffe, das hilft.


Im Ernst, XML-Pläne sind ignorierbar?!, Dachte ich, das war der springende Punkt? Wenn sie ungültig sind, sollte es werfen.
Crokusek

Ich bezog mich auf das Ereignis Plan Guide erfolglos.
wBob

2

Gibt es ANY Art und Weise ...

Schließlich gibt es in SQL 2016 CTP 3.0 eine Art von:)

Mit dem von Dmitry Pilugin hier beschriebenen Ablaufverfolgungsflag und den erweiterten Ereignissen können Sie (etwas willkürlich) drei eindeutige Guids aus den Zwischenstadien der Abfrageausführung herausfischen.

NB Dieser Code ist NICHT für die Produktion oder ernsthafte Verwendung in Bezug auf das Forcen von CTE- Plänen gedacht, sondern lediglich für einen unbeschwerten Blick auf eine neue Ablaufverfolgungsflagge und eine andere Vorgehensweise:

-- Configure the XEvents session; with ring buffer target so we can collect it
CREATE EVENT SESSION [query_trace_column_values] ON SERVER 
ADD EVENT sqlserver.query_trace_column_values
ADD TARGET package0.ring_buffer( SET max_memory = 2048 )
WITH ( MAX_MEMORY = 4096 KB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, MAX_DISPATCH_LATENCY = 30 SECONDS, MAX_EVENT_SIZE = 0 KB, MEMORY_PARTITION_MODE = NONE, TRACK_CAUSALITY = OFF , STARTUP_STATE = OFF )
GO

-- Start the session
ALTER EVENT SESSION [query_trace_column_values] ON SERVER
STATE = START;
GO

-- Run the query, including traceflag
DBCC TRACEON(2486);
SET STATISTICS XML ON;
GO

-- Original query
;with cte(guid,other) as (
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other
option ( recompile )
go

SET STATISTICS XML OFF;
DBCC TRACEOFF(2486);
GO

DECLARE @target_data XML

SELECT @target_data = CAST( target_data AS XML )
FROM sys.dm_xe_sessions AS s 
    INNER JOIN sys.dm_xe_session_targets AS t ON t.event_session_address = s.address
WHERE s.name = 'query_trace_column_values'


--SELECT @target_data td

-- Arbitrarily fish out 3 unique guids from intermediate stage of the query as collected by XEvent session
;WITH cte AS
(
SELECT
    n.c.value('(data[@name = "row_id"]/value/text())[1]', 'int') row_id,
    n.c.value('(data[@name = "column_value"]/value/text())[1]', 'char(36)') [guid]
FROM @target_data.nodes('//event[data[@name="column_id"]/value[. = 1]][data[@name="row_number"]/value[. < 4]][data[@name="node_name"]/value[. = "Nested Loops"]]') n(c)
)
SELECT *
FROM cte a
    CROSS JOIN cte b
GO

-- Stop the session
ALTER EVENT SESSION [query_trace_column_values] ON SERVER
STATE = STOP;
GO

-- Drop the session
IF EXISTS ( select * from sys.server_event_sessions where name = 'query_trace_column_values' )
DROP EVENT SESSION [query_trace_column_values] ON SERVER 
GO

Getestet mit Version (CTP3.2) - 13.0.900.73 (x64), nur zum Spaß.


1

Ich habe festgestellt, dass Traceflag 8649 (Force Parallel Plan) dieses Verhalten für die linke Guid-Spalte in meinen Instanzen 2008, R2 und 2012 induziert hat. Ich musste das Flag in SQL 2005 nicht verwenden, in dem sich der CTE korrekt verhalten hat. Ich habe versucht, den in SQL 2005 generierten Plan in den höheren Instanzen zu verwenden, aber er konnte nicht validiert werden.

with cte(guid,other) as (
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other
option ( querytraceon 8649 )

Entweder unter Verwendung des Hinweises, unter Verwendung eines Planleitfadens einschließlich des Hinweises oder unter Verwendung des von der Abfrage generierten Plans mit dem Hinweis in einem NUTZUNGSPLAN usw. hat alles funktioniert. cte newid


Vielen Dank für den erneuten Versuch. Die Abfrage sieht mit oder ohne dieses Ablaufverfolgungsflag in 2008/2012 nicht anders aus. Ich bin mir nicht sicher, ob es sich um meine SQL Server-Instanzen handelt oder was Sie anzeigen möchten. Ich sehe immer noch 18 Guids. Was siehst du?
27.

3 verschiedene Hilfslinien auf der linken Seite (Hilfslinienspalte), die sich jeweils dreimal wiederholen. 9 eindeutige Guids auf der rechten Seite (Guidb-Spalte), sodass sich zumindest das linke Bit so verhält, wie Sie es möchten. Ich habe ein Bild zu einer anderen Antwort hinzugefügt, um hoffentlich ein bisschen klarer zu werden. Kleine Schritte. Ich sollte auch in SQL 2005 beachten, ich bekomme 6 eindeutige Anleitungen, 3 auf der linken Seite, 3 auf der rechten Seite.
wBob

Ich habe auch gerade bemerkt, dass das Entfernen des "Alles" auch die 6 einzigartigen Führungen ergibt, 3 auf jeder Seite.
wBob

Kann dazu führen, dass das Traceflag nicht funktioniert, wenn der Server maxdop 1 installiert ist.
wBob
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.