Udostępnij za pośrednictwem


Najlepsze rozwiązania dotyczące semantyki grafu

Dotyczy: ✅Microsoft Fabric

Semantyka grafów obsługuje dwa podstawowe podejścia do pracy z grafami: przejściowe grafy tworzone w pamięci dla każdego zapytania oraz trwałe grafy zdefiniowane jako modele grafów i zrzuty w bazie danych. Ten artykuł zawiera najlepsze rozwiązania dla obu metod, co pozwala wybrać optymalne podejście i efektywnie używać semantyki grafów KQL.

Te wskazówki obejmują:

  • Strategie tworzenia i optymalizacji grafu
  • Zagadnienia dotyczące technik zapytań i aspektów wydajnościowych
  • Projekt schematu dla grafów trwałych
  • Integracja z innymi funkcjami języka KQL
  • Typowe pułapki do unikania

Podejścia do modelowania grafu

Istnieją dwa podejścia do pracy z grafami: przejściowe i trwałe.

Wykresy przejściowe

Utworzono dynamicznie przy użyciu make-graph operatora . Te wykresy istnieją tylko podczas wykonywania zapytań i są optymalne dla analizy ad hoc lub eksploracyjnej na małych i średnich zestawach danych.

Wykresy trwałe

Zdefiniowano przy użyciu modeli grafów i migawek grafów. Te grafy są przechowywane w bazie danych, obsługują schemat i przechowywanie wersji oraz są zoptymalizowane pod kątem powtarzanych, dużych lub wspólnych analiz.

Najlepsze rozwiązania dotyczące wykresów przejściowych

Wykresy przejściowe, utworzone w pamięci przy użyciu make-graph operatora, są idealne do analizy ad hoc, tworzenia prototypów i scenariuszy, w których struktura grafu zmienia się często lub wymaga tylko podzestawu dostępnych danych.

Optymalizowanie rozmiaru grafu pod kątem wydajności

Obiekt make-graph tworzy reprezentację w pamięci, w tym zarówno strukturę, jak i właściwości. Optymalizowanie wydajności poprzez:

  • Wczesne stosowanie filtrów — wybierz tylko odpowiednie węzły, krawędzie i właściwości przed utworzeniem grafu
  • Używanie projekcji — usuwanie niepotrzebnych kolumn w celu zminimalizowania zużycia pamięci
  • Stosowanie agregacji — podsumowywanie danych tam, gdzie jest to właściwe, aby zmniejszyć złożoność grafu

Przykład: zmniejszenie rozmiaru grafu przez filtrowanie i projekcję

W tym scenariuszu Bob zmienił menedżerów z Alice na Eve. Aby wyświetlić tylko najnowszy stan organizacji przy jednoczesnym zminimalizowaniu rozmiaru grafu:

let allEmployees = datatable(organization: string, name:string, age:long)
[
  "R&D", "Alice", 32,
  "R&D","Bob", 31,
  "R&D","Eve", 27,
  "R&D","Mallory", 29,
  "Marketing", "Alex", 35
];
let allReports = datatable(employee:string, manager:string, modificationDate: datetime)
[
  "Bob", "Alice", datetime(2022-05-23),
  "Bob", "Eve", datetime(2023-01-01),
  "Eve", "Mallory", datetime(2022-05-23),
  "Alice", "Dave", datetime(2022-05-23)
];
let filteredEmployees =
    allEmployees
    | where organization == "R&D"
    | project-away age, organization;
let filteredReports =
    allReports
    | summarize arg_max(modificationDate, *) by employee
    | project-away modificationDate;
filteredReports
| make-graph employee --> manager with filteredEmployees on name
| graph-match (employee)-[hasManager*2..5]-(manager)
  where employee.name == "Bob"
  project employee = employee.name, topManager = manager.name

Wyjście:

pracownik główny menedżer
Robert Mallory

Utrzymywanie bieżącego stanu za pomocą zmaterializowanych widoków

W poprzednim przykładzie pokazano, jak uzyskać ostatni znany stan przy użyciu metod summarize i arg_max. Ta operacja może być intensywnie obciążana obliczeniami, dlatego rozważ użycie zmaterializowanych widoków w celu zwiększenia wydajności.

Krok 1. Tworzenie tabel z przechowywaniem wersji

Tworzenie tabel z mechanizmem przechowywania wersji dla serii czasowych grafu:

.create table employees (organization: string, name:string, stateOfEmployment:string, properties:dynamic, modificationDate:datetime)

.create table reportsTo (employee:string, manager:string, modificationDate: datetime)

Krok 2. Tworzenie zmaterializowanych widoków

Użyj funkcji agregującej arg_max, aby określić najnowszy stan:

.create materialized-view employees_MV on table employees
{
    employees
    | summarize arg_max(modificationDate, *) by name
}

.create materialized-view reportsTo_MV on table reportsTo
{
    reportsTo
    | summarize arg_max(modificationDate, *) by employee
}

Krok 3. Tworzenie funkcji pomocnika

Upewnij się, że używany jest tylko zmaterializowany składnik i zastosuj dodatkowe filtry:

.create function currentEmployees () {
    materialized_view('employees_MV')
    | where stateOfEmployment == "employed"
}

.create function reportsTo_lastKnownState () {
    materialized_view('reportsTo_MV')
    | project-away modificationDate
}

Takie podejście zapewnia szybsze zapytania, większą współbieżność i mniejsze opóźnienie dla bieżącej analizy stanu przy jednoczesnym zachowaniu dostępu do danych historycznych.

let filteredEmployees =
    currentEmployees
    | where organization == "R&D"
    | project-away organization;
reportsTo_lastKnownState
| make-graph employee --> manager with filteredEmployees on name
| graph-match (employee)-[hasManager*2..5]-(manager)
  where employee.name == "Bob"
  project employee = employee.name, reportingPath = hasManager.manager

Implementacja podróży czasowej grafu

Analizowanie danych na podstawie historycznych stanów grafu zapewnia cenny kontekst czasowy. Zaimplementuj tę funkcję "podróży czasowej", łącząc filtry czasu z elementami summarize i :arg_max

.create function graph_time_travel (interestingPointInTime:datetime ) {
    let filteredEmployees =
        employees
        | where modificationDate < interestingPointInTime
        | summarize arg_max(modificationDate, *) by name;
    let filteredReports =
        reportsTo
        | where modificationDate < interestingPointInTime
        | summarize arg_max(modificationDate, *) by employee
        | project-away modificationDate;
    filteredReports
    | make-graph employee --> manager with filteredEmployees on name
}

Przykład użycia:

Zapytanie o najwyższego menedżera Boba na podstawie stanu grafu z czerwca 2022 r.:

graph_time_travel(datetime(2022-06-01))
| graph-match (employee)-[hasManager*2..5]-(manager)
  where employee.name == "Bob"
  project employee = employee.name, reportingPath = hasManager.manager

Wyjście:

pracownik główny menedżer
Robert Dave

Obsługa wielu typów węzłów i krawędzi

Podczas pracy z złożonymi grafami zawierającymi wiele typów węzłów należy użyć kanonicznego modelu grafu właściwości. Zdefiniuj węzły z atrybutami, takimi jak nodeId (ciąg), label (ciąg) i properties (dynamiczny), a krawędzie obejmują source (ciąg), destination (ciąg), label (ciąg) i properties (dynamiczny).

Przykład: Analiza konserwacji fabryki

Rozważ kierownika fabryki badającego problemy sprzętu i odpowiedzialnego personelu. Scenariusz łączy grafy zasobów sprzętu produkcyjnego z hierarchią personelu konserwacyjnego:

Wykres pracowników fabryki, wyposażenia i pomiarów

Dane dla tych jednostek można przechowywać bezpośrednio w klastrze lub uzyskiwać przy użyciu federacji zapytań w innej usłudze. Aby zilustrować przykład, następujące dane tabelaryczne są tworzone w ramach zapytania:

let sensors = datatable(sensorId:string, tagName:string, unitOfMeasure:string)
[
  "1", "temperature", "°C",
  "2", "pressure", "Pa",
  "3", "speed", "m/s"
];
let timeseriesData = datatable(sensorId:string, timestamp:string, value:double, anomaly: bool )
[
    "1", datetime(2023-01-23 10:00:00), 32, false,
    "1", datetime(2023-01-24 10:00:00), 400, true,
    "3", datetime(2023-01-24 09:00:00), 9, false
];
let employees = datatable(name:string, age:long)
[
  "Alice", 32,
  "Bob", 31,
  "Eve", 27,
  "Mallory", 29,
  "Alex", 35,
  "Dave", 45
];
let allReports = datatable(employee:string, manager:string)
[
  "Bob", "Alice",
  "Alice", "Dave",
  "Eve", "Mallory",
  "Alex", "Dave"
];
let operates = datatable(employee:string, machine:string, timestamp:datetime)
[
  "Bob", "Pump", datetime(2023-01-23),
  "Eve", "Pump", datetime(2023-01-24),
  "Mallory", "Press", datetime(2023-01-24),
  "Alex", "Conveyor belt", datetime(2023-01-24),
];
let assetHierarchy = datatable(source:string, destination:string)
[
  "1", "Pump",
  "2", "Pump",
  "Pump", "Press",
  "3", "Conveyor belt"
];

Pracownicy, czujniki i inne jednostki i relacje nie współużytkują kanonicznego modelu danych. Operator unii może służyć do łączenia i standaryzacji danych.

Poniższe zapytanie łączy dane czujnika z danymi szeregów czasowych w celu zidentyfikowania czujników z nieprawidłowymi odczytami, a następnie używa projekcji do utworzenia wspólnego modelu dla węzłów grafu.

let nodes =
    union
        (
            sensors
            | join kind=leftouter
            (
                timeseriesData
                | summarize hasAnomaly=max(anomaly) by sensorId
            ) on sensorId
            | project nodeId = sensorId, label = "tag", properties = pack_all(true)
        ),
        ( employees | project nodeId = name, label = "employee", properties = pack_all(true));

Krawędzie są przekształcane w podobny sposób.

let edges =
    union
        ( assetHierarchy | extend label = "hasParent" ),
        ( allReports | project source = employee, destination = manager, label = "reportsTo" ),
        ( operates | project source = employee, destination = machine, properties = pack_all(true), label = "operates" );

Za pomocą ustandaryzowanych węzłów i danych krawędzi można utworzyć graf przy użyciu operatora make-graph

let graph = edges
| make-graph source --> destination with nodes on nodeId;

Po utworzeniu grafu zdefiniuj wzorzec ścieżki i przeprojektuj wymagane informacje. Wzorzec rozpoczyna się od węzła tagu, po którym następuje krawędź o zmiennej długości prowadząca do zasobu. Ten zasób jest obsługiwany przez operatora, który zgłasza się do najwyższego menedżera za pośrednictwem krawędzi o zmiennej długości o nazwie reportsTo. Sekcja ograniczeń operatora dopasowania grafu, w tym przypadku klauzula where , filtruje tagi do tych z anomalią, która była obsługiwana w określonym dniu.

graph
| graph-match (tag)-[hasParent*1..5]->(asset)<-[operates]-(operator)-[reportsTo*1..5]->(topManager)
    where tag.label=="tag" and tobool(tag.properties.hasAnomaly) and
        startofday(todatetime(operates.properties.timestamp)) == datetime(2023-01-24)
        and topManager.label=="employee"
    project
        tagWithAnomaly = tostring(tag.properties.tagName),
        impactedAsset = asset.nodeId,
        operatorName = operator.nodeId,
        responsibleManager = tostring(topManager.nodeId)

Wynik

tagWithAnomaly zasób objęty wpływem Nazwa operatora odpowiedzialnyKierownik
temperatura Pompa Przeddzień Mallory

Projekcja w programie graph-match pokazuje, że czujnik temperatury wykazywał anomalię w określonym dniu. Czujnik był obsługiwany przez Eve, który ostatecznie zgłasza się do Mallory. Dzięki tym informacjom kierownik fabryki może skontaktować się z Eve i, w razie potrzeby, Mallory, aby lepiej zrozumieć anomalię.

Najlepsze rozwiązania dotyczące grafów trwałych

Trwałe grafy zdefiniowane przy użyciu modeli grafów i migawek grafów zapewniają niezawodne rozwiązania dla zaawansowanych potrzeb analizy grafów. Te wykresy doskonale sprawdzają się w scenariuszach wymagających wielokrotnej analizy dużych, złożonych lub zmieniających się relacji danych oraz ułatwiają współpracę, umożliwiając zespołom udostępnianie ustandaryzowanych definicji grafu i spójnych wyników analitycznych. Utrzymując struktury grafów w bazie danych, takie podejście znacznie zwiększa wydajność cyklicznych zapytań i obsługuje zaawansowane możliwości przechowywania wersji.

Używanie schematu i definicji w celu zapewnienia spójności i wydajności

Jasny schemat dla modelu grafu jest niezbędny, ponieważ określa typy węzłów i krawędzi wraz z ich właściwościami. Takie podejście zapewnia spójność danych i umożliwia wydajne wykonywanie zapytań. Skorzystaj z sekcji Definition, aby określić sposób konstruowania węzłów i krawędzi na podstawie danych tabelarycznych poprzez kroki AddNodes i AddEdges.

Korzystanie z etykiet statycznych i dynamicznych na potrzeby elastycznego modelowania

Podczas modelowania grafu można użyć metod etykietowania statycznego i dynamicznego w celu uzyskania optymalnej elastyczności. Etykiety statyczne są idealne dla dobrze zdefiniowanych typów węzłów i krawędzi, które rzadko się zmieniają — zdefiniuj je w Schema sekcji i odwołaj się do nich w Labels tablicy kroków. W przypadku, gdy typy węzłów lub krawędzi są określane przez wartości danych (na przykład gdy typ jest przechowywany w kolumnie), użyj etykiet dynamicznych, określając LabelsColumn w kroku, aby przypisać etykiety w czasie wykonywania. Takie podejście jest szczególnie przydatne w przypadku grafów z heterogenicznymi lub ewoluującymi schematami. Oba mechanizmy można skutecznie łączyć — można zdefiniować tablicę Labels dla etykiet statycznych, a także określić , LabelsColumn aby uwzględnić dodatkowe etykiety z danych, zapewniając maksymalną elastyczność podczas modelowania złożonych grafów z kategoryzacją opartą zarówno na stałe, jak i opartej na danych.

Przykład: używanie etykiet dynamicznych dla wielu typów węzłów i krawędzi

W poniższym przykładzie pokazano skuteczną implementację etykiet dynamicznych na grafie reprezentującym relacje zawodowe. W tym scenariuszu wykres zawiera osoby i firmy jako węzły, a relacje zatrudnienia tworzą krawędzie między nimi. Elastyczność tego modelu wynika z określania typów węzłów i krawędzi bezpośrednio z kolumn w danych źródłowych, dzięki czemu struktura grafu dostosowuje się w sposób organiczny do podstawowych informacji.

.create-or-alter graph_model ProfessionalNetwork ```
{
  "Schema": {
    "Nodes": {
      "Person": {"Name": "string", "Age": "long"},
      "Company": {"Name": "string", "Industry": "string"}
    },
    "Edges": {
      "WORKS_AT": {"StartDate": "datetime", "Position": "string"}
    }
  },
  "Definition": {
    "Steps": [
      {
        "Kind": "AddNodes",
        "Query": "Employees | project Id, Name, Age, NodeType",
        "NodeIdColumn": "Id",
        "Labels": ["Person"],
        "LabelsColumn": "NodeType"
      },
      {
        "Kind": "AddEdges",
        "Query": "EmploymentRecords | project EmployeeId, CompanyId, StartDate, Position, RelationType",
        "SourceColumn": "EmployeeId",
        "TargetColumn": "CompanyId",
        "Labels": ["WORKS_AT"],
        "LabelsColumn": "RelationType"
      }
    ]
  }
}
```

To dynamiczne podejście do etykietowania zapewnia wyjątkową elastyczność podczas modelowania grafów z wieloma typami węzłów i krawędzi, eliminując konieczność modyfikowania schematu za każdym razem, gdy nowy typ jednostki pojawia się w danych. Oddzielenie modelu logicznego od implementacji fizycznej umożliwia ciągłe rozwijanie grafu w celu reprezentowania nowych relacji bez konieczności wprowadzania zmian strukturalnych w bazowym schemacie.

Strategie partycjonowania wielodzierżawności dla scenariuszy ISV na dużą skalę

W dużych organizacjach, szczególnie w scenariuszach ISV, grafy mogą składać się z wielu miliardów węzłów i krawędzi. Ta skala stanowi unikatowe wyzwania, które wymagają podejścia do partycjonowania strategicznego w celu utrzymania wydajności podczas zarządzania kosztami i złożonością.

Zrozumienie wyzwania

Środowiska wielodostępne na dużą skalę często wykazują następujące cechy:

  • Miliardy węzłów i krawędzi — grafy w skali przedsiębiorstwa, które przekraczają tradycyjne możliwości bazy danych grafów
  • Rozkład rozmiaru najemcy — zwykle jest zgodny z prawem potęgowym, w którym 99,9% najemców ma małe i średnie grafy, podczas gdy 0,1% ma ogromne grafy
  • Wymagania dotyczące wydajności — potrzeba analizy w czasie rzeczywistym (bieżących danych) i możliwości analizy historycznej
  • Zagadnienia dotyczące kosztów — równoważenie kosztów infrastruktury i możliwości analitycznych

Partycjonowanie według granic naturalnych

Najbardziej skutecznym podejściem do zarządzania grafami na dużą skalę jest podział wzdłuż granic naturalnych, zazwyczaj identyfikatorów najemców lub jednostek organizacyjnych.

Kluczowe strategie partycjonowania:

  • Partycjonowanie oparte na najemcy — oddziela grafy według klienta, organizacji lub jednostki biznesowej
  • Partycjonowanie geograficzne — dzielenie według regionu, kraju lub lokalizacji centrum danych
  • Partycjonowanie czasowe — oddzielanie według okresów na potrzeby analizy historycznej
  • Partycjonowanie funkcjonalne — podział według domeny biznesowej lub obszaru aplikacji

Przykład: Wielootenantowa struktura organizacyjna

// Partition employees and reports by tenant
let tenantEmployees = 
    allEmployees
    | where tenantId == "tenant_123"
    | project-away tenantId;

let tenantReports = 
    allReports
    | where tenantId == "tenant_123"
    | summarize arg_max(modificationDate, *) by employee
    | project-away modificationDate, tenantId;

tenantReports
| make-graph employee --> manager with tenantEmployees on name
| graph-match (employee)-[hasManager*1..5]-(manager)
  where employee.name == "Bob"
  project employee = employee.name, reportingChain = hasManager.manager

Podejście hybrydowe: przejściowe a trwałe wykresy według rozmiaru dzierżawy

Najbardziej opłacalna strategia łączy zarówno przejściowe, jak i trwałe wykresy oparte na cechach dzierżawy:

Małe i średnie dzierżawy (99,9% dzierżaw)

Użyj wykresów przejściowych dla większości najemców:

Zalety:

  • Zawsze up-to— zawsze aktualne dane — nie jest wymagana konserwacja migawki
  • Mniejsze obciążenie operacyjne — brak modelu grafu ani zarządzania migawkami
  • Ekonomiczne — brak dodatkowych kosztów magazynowania dla struktur grafów
  • Natychmiastowa dostępność — brak opóźnień przetwarzania wstępnego

Wzorzec implementacji:

.create function getTenantGraph(tenantId: string) {
    let tenantEmployees = 
        employees
        | where tenant == tenantId and stateOfEmployment == "employed"
        | project-away tenant, stateOfEmployment;
    let tenantReports = 
        reportsTo
        | where tenant == tenantId
        | summarize arg_max(modificationDate, *) by employee
        | project-away modificationDate, tenant;
    tenantReports
    | make-graph employee --> manager with tenantEmployees on name
}

// Usage for small tenant
getTenantGraph("small_tenant_456")
| graph-match (employee)-[reports*1..3]-(manager)
  where employee.name == "Alice"
  project employee = employee.name, managerChain = reports.manager

Duzi najemcy (0,1% najemców)

Użyj persistent graphs dla największych najemców:

Zalety:

  • Skalowalność — obsługa wykresów przekraczających ograniczenia pamięci
  • Optymalizacja wydajności — eliminowanie opóźnień budowy dla złożonych zapytań
  • Zaawansowana analiza — obsługa zaawansowanych algorytmów grafu i analizy
  • Analiza historyczna — wiele migawek na potrzeby porównania danych czasowych

Wzorzec implementacji:

// Create graph model for large tenant (example: Contoso)
.create-or-alter graph_model ContosoOrgChart ```
{
    "Schema": {
        "Nodes": {
            "Employee": {
                "Name": "string",
                "Department": "string",
                "Level": "int",
                "JoinDate": "datetime"
            }
        },
        "Edges": {
            "ReportsTo": {
                "Since": "datetime",
                "Relationship": "string"
            }
        }
    },
    "Definition": {
        "Steps": [
            {
                "Kind": "AddNodes",
                "Query": "employees | where tenant == 'Contoso' and stateOfEmployment == 'employed' | project Name, Department, Level, JoinDate",
                "NodeIdColumn": "Name",
                "Labels": ["Employee"]
            },
            {
                "Kind": "AddEdges", 
                "Query": "reportsTo | where tenant == 'Contoso' | summarize arg_max(modificationDate, *) by employee | project employee, manager, modificationDate as Since | extend Relationship = 'DirectReport'",
                "SourceColumn": "employee",
                "TargetColumn": "manager",
                "Labels": ["ReportsTo"]
            }
        ]
    }
}
```

// Create snapshot for Contoso
.create graph snapshot ContosoSnapshot from ContosoOrgChart

// Query Contoso's organizational graph
graph("ContosoOrgChart")
| graph-match (employee)-[reports*1..10]-(executive)
  where employee.Department == "Engineering"
  project employee = employee.Name, executive = executive.Name, pathLength = array_length(reports)

Najlepsze praktyki dotyczące scenariuszy dla niezależnych dostawców oprogramowania (ISV)

  1. Zacznij od przejściowych grafów — rozpocznij wszystkie nowe dzierżawy z przejściowymi grafami dla uproszczenia
  2. Monitorowanie wzorców wzrostu — wdrażanie automatycznego wykrywania użytkowników wymagających trwałych grafów
  3. Tworzenie migawek wsadowych — planowanie aktualizacji migawek w okresach niskiego użycia
  4. Izolacja najemców — Zapewnić prawidłową izolację modeli grafowych i migawek między najemcami.
  5. Zarządzanie zasobami — użyj grup roboczych, aby zapobiec wpływowi dużych zapytań klientów na mniejszych klientów
  6. Optymalizacja kosztów — regularne przeglądanie i optymalizowanie trwałego/przejściowego progu na podstawie rzeczywistych wzorców użycia

Takie podejście hybrydowe umożliwia organizacjom zapewnienie zawsze aktualnej analizy danych dla większości dzierżaw, zapewniając jednocześnie możliwości analizy w skali przedsiębiorstwa dla największych dzierżaw, optymalizując zarówno koszty, jak i wydajność w całej bazie klientów.