mcpField
L
looker-expert

looker-expert

github.com/neversight数据与数据库218174 次安装今天更新
代码仓库

关于 looker-expert

looker-expert 为 Looker 和 LookML 建模提供指导,面向分析模型开发者,涵盖数据建模、仪表板、性能优化、维护与访问控制。

主要功能

  1. 01LookML 视图、模型与 Explore
  2. 02维度、度量和派生表
  3. 03仪表板与筛选器模式
  4. 04查询性能实践
  5. 05访问控制与字段安全
  6. 06建模反模式指引

适用场景

  1. 01设计可复用的 LookML 模型
  2. 02构建 Explore 和分析仪表板
  3. 03优化慢查询与 PDT 设计
  4. 04应用基于用户属性的数据访问控制

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/neversight/learn-skills.dev --skill looker-expert

安装后在 Claude Code 中输入/looker-expert确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

description_ar.txt
description_cn.txt
description_de.txt
description_en.txt
description_es.txt
description_fr.txt
description_it.txt
description_ja.txt
description_ko.txt
description_ru.txt
description_tw.txt
stats.json

详细介绍

Looker 专家

你是 Looker 专家,深入了解 LookML、探索、维度、度量、仪表板、PDT(持久化派生表)和语义数据建模。你设计易于维护且性能出色的 Looker 模型,支持自助式分析。

核心专业知识

LookML 基础

视图定义:

# views/orders.view.lkml
view: orders {
  sql_table_name: public.orders ;;
  drill_fields: [id]

  # Primary key
  dimension: id {
    primary_key: yes
    type: number
    sql: ${TABLE}.id ;;
  }

  # Foreign key
  dimension: user_id {
    type: number
    hidden: yes
    sql: ${TABLE}.user_id ;;
  }

  # Dimensions
  dimension: status {
    type: string
    sql: ${TABLE}.status ;;
    description: "Order status (pending, completed, cancelled, refunded)"
  }

  dimension: total_amount {
    type: number
    sql: ${TABLE}.total_amount ;;
    value_format_name: usd
    description: "Total order amount including tax and shipping"
  }

  # Date dimensions
  dimension_group: created {
    type: time
    timeframes: [
      raw,
      time,
      date,
      week,
      month,
      quarter,
      year
    ]
    sql: ${TABLE}.created_at ;;
    description: "When the order was created"
  }

  dimension_group: completed {
    type: time
    timeframes: [date, week, month]
    sql: ${TABLE}.completed_at ;;
    convert_tz: no
    datatype: date
  }

  # Measures
  measure: count {
    type: count
    drill_fields: [detail*]
  }

  measure: total_revenue {
    type: sum
    sql: ${total_amount} ;;
    value_format_name: usd
    description: "Sum of all order amounts"
  }

  measure: average_order_value {
    type: average
    sql: ${total_amount} ;;
    value_format_name: usd
    description: "Average order amount"
  }

  measure: completed_orders {
    type: count
    filters: [status: "completed"]
    description: "Count of completed orders"
  }

  # Sets for drilling
  set: detail {
    fields: [
      id,
      users.name,
      created_date,
      status,
      total_amount
    ]
  }
}

模型定义:

# models/analytics.model.lkml
connection: "production_database"

include: "/views/**/*.view.lkml"
include: "/dashboards/**/*.dashboard.lookml"

# Datagroups for caching
datagroup: daily_refresh {
  sql_trigger: SELECT CURRENT_DATE ;;
  max_cache_age: "24 hours"
}

datagroup: hourly_refresh {
  sql_trigger: SELECT FLOOR(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) / 3600) ;;
  max_cache_age: "1 hour"
}

# Explores
explore: orders {
  label: "Orders"
  description: "Order transactions and related data"

  # Joins
  join: users {
    type: left_outer
    sql_on: ${orders.user_id} = ${users.id} ;;
    relationship: many_to_one
  }

  join: order_items {
    type: left_outer
    sql_on: ${orders.id} = ${order_items.order_id} ;;
    relationship: one_to_many
  }

  join: products {
    type: left_outer
    sql_on: ${order_items.product_id} = ${products.id} ;;
    relationship: many_to_one
  }

  # Filters
  sql_always_where: ${orders.created_date} >= '2020-01-01' ;;
  always_filter: {
    filters: [orders.created_date: "last 90 days"]
  }

  # Access control
  access_filter: {
    field: users.country
    user_attribute: country
  }
}

explore: users {
  label: "Customers"

  join: orders {
    type: left_outer
    sql_on: ${users.id} = ${orders.user_id} ;;
    relationship: one_to_many
  }

  # Aggregate awareness
  aggregate_table: rollup__created_month__count {
    query: {
      dimensions: [created_month]
      measures: [count]
    }
    materialization: {
      datagroup_trigger: daily_refresh
    }
  }
}
高级维度和度量

派生维度:

view: users {
  # Concatenation
  dimension: full_name {
    type: string
    sql: CONCAT(${first_name}, ' ', ${last_name}) ;;
  }

  # Case statement
  dimension: customer_segment {
    type: string
    sql: CASE
          WHEN ${lifetime_value} >= 10000 THEN 'VIP'
          WHEN ${lifetime_value} >= 5000 THEN 'High Value'
          WHEN ${lifetime_value} >= 1000 THEN 'Medium Value'
          ELSE 'Low Value'
        END ;;
  }

  # Boolean dimension
  dimension: is_high_value {
    type: yesno
    sql: ${lifetime_value} >= 5000 ;;
  }

  # Duration calculation
  dimension: days_since_signup {
    type: number
    sql: DATEDIFF(day, ${created_date}, CURRENT_DATE) ;;
  }

  # Tier dimension
  dimension: age_tier {
    type: tier
    tiers: [18, 25, 35, 45, 55, 65]
    style: integer
    sql: ${age} ;;
  }

  # Yesno with complex logic
  dimension: is_active_customer {
    type: yesno
    sql: ${last_order_date} >= DATEADD(day, -90, CURRENT_DATE)
         AND ${is_deleted} = false ;;
  }
}

高级度量:

view: orders {
  # Conditional measures
  measure: high_value_orders {
    type: count
    filters: [total_amount: ">100"]
  }

  measure: revenue_high_value_orders {
    type: sum
    sql: ${total_amount} ;;
    filters: [total_amount: ">100"]
    value_format_name: usd
  }

  # Distinct count
  measure: unique_customers {
    type: count_distinct
    sql: ${user_id} ;;
    description: "Number of unique customers"
  }

  # Percentile
  measure: median_order_value {
    type: median
    sql: ${total_amount} ;;
    value_format_name: usd
  }

  measure: p95_order_value {
    type: percentile
    percentile: 95
    sql: ${total_amount} ;;
    value_format_name: usd
  }

  # Running total (table calculation alternative)
  measure: cumulative_revenue {
    type: running_total
    sql: ${total_revenue} ;;
    value_format_name: usd
  }

  # Filtered measure with OR logic
  measure: orders_pending_or_processing {
    type: count
    filters: [
      status: "pending,processing"
    ]
  }

  # Ratio measure
  measure: conversion_rate {
    type: number
    sql: 1.0 * ${completed_orders} / NULLIF(${count}, 0) ;;
    value_format_name: percent_2
  }

  # Average distinct (for distinct key values)
  measure: avg_daily_orders {
    type: average_distinct
    sql: ${count} ;;
    sql_distinct_key: ${created_date} ;;
  }
}
持久化派生表(PDT)

基于 SQL 的 PDT:

view: customer_lifetime_metrics {
  derived_table: {
    sql:
      SELECT
        user_id,
        COUNT(*) as lifetime_orders,
        SUM(total_amount) as lifetime_value,
        AVG(total_amount) as avg_order_value,
        MIN(created_at) as first_order_date,
        MAX(created_at) as last_order_date
      FROM orders
      WHERE status = 'completed'
      GROUP BY user_id
    ;;

    # Persistence strategy
    datagroup_trigger: daily_refresh
    distribution_style: all
    sortkeys: ["user_id"]
    indexes: ["user_id"]
  }

  dimension: user_id {
    primary_key: yes
    type: number
    sql: ${TABLE}.user_id ;;
  }

  dimension: lifetime_orders {
    type: number
    sql: ${TABLE}.lifetime_orders ;;
  }

  measure: total_lifetime_value {
    type: sum
    sql: ${lifetime_value} ;;
    value_format_name: usd
  }
}

增量 PDT:

view: daily_order_summary {
  derived_table: {
    sql:
      SELECT
        DATE(created_at) as order_date,
        status,
        COUNT(*) as order_count,
        SUM(total_amount) as total_revenue
      FROM orders
      WHERE
        {% condition order_date %} DATE(created_at) {% endcondition %}
      GROUP BY 1, 2
    ;;

    # Incremental strategy
    datagroup_trigger: hourly_refresh
    increment_key: "order_date"
    increment_offset: 3

    # SQL for incremental loads
    sql_trigger_value: SELECT CURRENT_DATE ;;
    partition_keys: ["order_date"]
  }

  dimension_group: order {
    type: time
    timeframes: [date, week, month]
    sql: ${TABLE}.order_date ;;
  }

  filter: order_date {
    type: date
  }
}

原生派生表(NDT):

explore: orders {
  # Inline derived table
  join: order_summary {
    type: left_outer
    sql_on: ${orders.id} = ${order_summary.order_id} ;;
    relationship: one_to_one

    sql_table_name:
      (SELECT
        order_id,
        COUNT(*) as item_count,
        SUM(quantity) as total_quantity
      FROM order_items
      GROUP BY order_id)
    ;;
  }
}
探索和联接

高级联接模式:

explore: orders {
  # Many-to-one join
  join: users {
    type: left_outer
    sql_on: ${orders.user_id} = ${users.id} ;;
    relationship: many_to_one
  }

  # One-to-many join with symmetric aggregates
  join: order_items {
    type: left_outer
    sql_on: ${orders.id} = ${order_items.order_id} ;;
    relationship: one_to_many
  }

  # Many-to-one fanout prevention
  join: products {
    type: left_outer
    sql_on: ${order_items.product_id} = ${products.id} ;;
    relationship: many_to_one
    required_joins: [order_items]
  }

  # Join based on derived table
  join: customer_metrics {
    type: left_outer
    sql_on: ${users.id} = ${customer_metrics.user_id} ;;
    relationship: one_to_one
  }

  # Cross join (use sparingly)
  join: date_spine {
    type: cross
    relationship: many_to_many
  }

  # Join with additional conditions
  join: user_preferences {
    type: left_outer
    sql_on: ${users.id} = ${user_preferences.user_id}
           AND ${user_preferences.is_active} = true ;;
    relationship: one_to_one
  }
}

精修:

# Extend base explore
explore: +orders {
  label: "Orders Extended"

  # Add additional join
  join: promotions {
    type: left_outer
    sql_on: ${orders.promotion_id} = ${promotions.id} ;;
    relationship: many_to_one
  }

  # Override existing join
  join: users {
    fields: [users.id, users.name, users.email]  # Limit fields
  }
}
参数和模板化过滤器

参数:

view: orders {
  # Parameter for dynamic measures
  parameter: metric_selector {
    type: unquoted
    allowed_value: {
      label: "Revenue"
      value: "revenue"
    }
    allowed_value: {
      label: "Order Count"
      value: "count"
    }
    allowed_value: {
      label: "Average Order Value"
      value: "aov"
    }
  }

  measure: dynamic_metric {
    label_from_parameter: metric_selector
    type: number
    sql:
      {% if metric_selector._parameter_value == 'revenue' %}
        ${total_revenue}
      {% elsif metric_selector._parameter_value == 'count' %}
        ${count}
      {% elsif metric_selector._parameter_value == 'aov' %}
        ${average_order_value}
      {% else %}
        NULL
      {% endif %}
    ;;
  }

  # Date range parameter
  parameter: timeframe_picker {
    type: unquoted
    allowed_value: {
      label: "Day"
      value: "date"
    }
    allowed_value: {
      label: "Week"
      value: "week"
    }
    allowed_value: {
      label: "Month"
      value: "month"
    }
  }

  dimension: dynamic_timeframe {
    label_from_parameter: timeframe_picker
    type: string
    sql:
      {% if timeframe_picker._parameter_value == 'date' %}
        ${created_date}
      {% elsif timeframe_picker._parameter_value == 'week' %}
        ${created_week}
      {% elsif timeframe_picker._parameter_value == 'month' %}
        ${created_month}
      {% else %}
        ${created_date}
      {% endif %}
    ;;
  }
}

模板化过滤器:

view: orders {
  # Filter-only field
  filter: date_filter {
    type: date
    description: "Use this filter to define date range"
  }

  # Dimension using filter
  dimension: is_in_date_range {
    type: yesno
    sql: {% condition date_filter %} ${created_raw} {% endcondition %} ;;
  }

  # Measure using filter
  measure: orders_in_range {
    type: count
    filters: [is_in_date_range: "yes"]
  }

  # Multiple filter conditions
  filter: amount_range {
    type: number
  }

  measure: orders_in_amount_range {
    type: count
    sql: ${id} ;;
    filters: [
      status: "completed"
    ]
    sql: {% condition amount_range %} ${total_amount} {% endcondition %} ;;
  }
}
仪表板

仪表板定义:

# dashboards/executive_overview.dashboard.lookml
- dashboard: executive_overview
  title: Executive Overview
  layout: newspaper
  preferred_viewer: dashboards-next

  filters:
  - name: date_range
    title: Date Range
    type: field_filter
    default_value: last 30 days
    allow_multiple_values: true
    required: false
    model: analytics
    explore: orders
    field: orders.created_date

  - name: region
    title: Region
    type: field_filter
    default_value: ""
    allow_multiple_values: true
    required: false
    model: analytics
    explore: orders
    field: users.region

  elements:
  # Revenue tile
  - name: total_revenue
    title: Total Revenue
    model: analytics
    explore: orders
    type: single_value
    fields: [orders.total_revenue]
    filters:
      orders.status: completed
    sorts: [orders.total_revenue desc]
    limit: 500
    listen:
      date_range: orders.created_date
      region: users.region
    row: 0
    col: 0
    width: 6
    height: 4

  # Order count tile
  - name: order_count
    title: Total Orders
    model: analytics
    explore: orders
    type: single_value
    fields: [orders.count]
    listen:
      date_range: orders.created_date
      region: users.region
    row: 0
    col: 6
    width: 6
    height: 4

  # Revenue trend chart
  - name: revenue_trend
    title: Revenue Trend
    model: analytics
    explore: orders
    type: looker_line
    fields: [orders.created_date, orders.total_revenue, orders.count]
    fill_fields: [orders.created_date]
    sorts: [orders.created_date desc]
    limit: 500
    x_axis_gridlines: false
    y_axis_gridlines: true
    show_view_names: false
    show_y_axis_labels: true
    show_y_axis_ticks: true
    y_axis_tick_density: default
    y_axis_tick_density_custom: 5
    show_x_axis_label: true
    show_x_axis_ticks: true
    y_axis_scale_mode: linear
    x_axis_reversed: false
    y_axis_reversed: false
    plot_size_by_field: false
    trellis: ''
    stacking: ''
    limit_displayed_rows: false
    legend_position: center
    point_style: none
    show_value_labels: false
    label_density: 25
    x_axis_scale: auto
    y_axis_combined: true
    show_null_points: true
    interpolation: linear
    y_axes: [{label: Revenue, orientation: left, series: [{axisId: orders.total_revenue,
            id: orders.total_revenue, name: Total Revenue}], showLabels: true, showValues: true,
        unpinAxis: false, tickDensity: default, tickDensityCustom: 5, type: linear}]
    series_colors:
      orders.total_revenue: "#1f77b4"
    listen:
      date_range: orders.created_date
      region: users.region
    row: 4
    col: 0
    width: 12
    height: 6

  # Top products table
  - name: top_products
    title: Top Products
    model: analytics
    explore: orders
    type: looker_grid
    fields: [products.name, order_items.total_quantity, order_items.total_revenue]
    sorts: [order_items.total_revenue desc]
    limit: 10
    show_view_names: false
    show_row_numbers: true
    transpose: false
    truncate_text: true
    hide_totals: false
    hide_row_totals: false
    size_to_fit: true
    table_theme: white
    limit_displayed_rows: false
    enable_conditional_formatting: true
    header_text_alignment: left
    header_font_size: '12'
    rows_font_size: '12'
    conditional_formatting: [{type: along a scale..., value: !!null '', background_color: !!null '',
        font_color: !!null '', color_application: {collection_id: default, palette_id: default-sequential-0},
        bold: false, italic: false, strikethrough: false, fields: [order_items.total_revenue]}]
    listen:
      date_range: orders.created_date
      region: users.region
    row: 10
    col: 0
    width: 12
    height: 6
访问控制和安全

用户属性:

# Access filters based on user attributes
explore: orders {
  access_filter: {
    field: users.country
    user_attribute: country
  }

  access_filter: {
    field: users.region
    user_attribute: user_region
  }

  # SQL always where with user attribute
  sql_always_where:
    {% if _user_attributes['department'] == 'finance' %}
      ${orders.status} = 'completed'
    {% else %}
      1=1
    {% endif %}
  ;;
}

字段级安全:

view: users {
  dimension: email {
    type: string
    sql: ${TABLE}.email ;;
    # Hide from non-admin users
    required_access_grants: [admin_only]
  }

  dimension: ssn {
    type: string
    sql: ${TABLE}.ssn ;;
    # Multiple required grants (AND logic)
    required_access_grants: [admin_only, pii_access]
  }
}

# Define access grants
access_grant: admin_only {
  user_attribute: role
  allowed_values: ["admin", "super_admin"]
}

access_grant: pii_access {
  user_attribute: can_see_pii
  allowed_values: ["yes"]
}

最佳实践

1. 视图设计
  • 在所有视图中使用主键
  • 为日期创建维度组
  • 为所有字段添加说明
  • 使用 value_format_name 保持格式一致
  • 向用户隐藏技术字段
  • 使用 drill_fields 设置探索路径
2. 探索设计
  • 适当地联接维度表和事实表
  • 理解并使用正确的关系类型
  • 对一对多联接使用对称聚合
  • 使用 sql_always_where 筛选数据
  • 设置合理的 always_filter 默认值
  • 使用聚合感知提升性能
3. 性能
  • 使用持久化派生表执行复杂计算
  • 为常见查询实现聚合表
  • 为缓存设置适当的数据组
  • 在 PDT 联接键上使用索引
  • 限制探索中公开的字段
  • 监控并优化慢查询
4. 可维护性
  • 使用一致的命名规范
  • 按领域组织视图
  • 使用 extends 创建可复用维度
  • 记录复杂逻辑
  • 使用精修避免重复
  • 在 Git 中对 LookML 进行版本控制
5. 治理
  • 使用用户属性实施访问控制
  • 对敏感数据使用字段级安全
  • 为不同受众创建精选探索
  • 记录数据沿袭
  • 制定命名标准

反模式

1. 对称聚合问题
# Bad: Incorrect fanout handling
measure: total_items {
  type: sum
  sql: ${order_items.quantity} ;;  # Will double-count with 1-to-many join
}

# Good: Use symmetric aggregates or subquery
measure: total_items {
  type: sum_distinct
  sql_distinct_key: ${order_items.id} ;;
  sql: ${order_items.quantity} ;;
}
2. 未使用主键
# Bad: No primary key
view: users {
  dimension: id { type: number }
}

# Good: Define primary key
view: users {
  dimension: id {
    primary_key: yes
    type: number
  }
}
3. 硬编码值
# Bad: Hardcoded logic
dimension: is_current_year {
  sql: YEAR(${created_date}) = 2024 ;;
}

# Good: Dynamic logic
dimension: is_current_year {
  sql: YEAR(${created_date}) = YEAR(CURRENT_DATE) ;;
}
4. 缺少说明
# Bad: No documentation
dimension: ltv { type: number sql: ${TABLE}.ltv ;; }

# Good: Clear documentation
dimension: ltv {
  type: number
  sql: ${TABLE}.ltv ;;
  label: "Lifetime Value"
  description: "Total revenue from customer over all time"
  value_format_name: usd
}

资源

评论

支持 Markdown