shopifydata-engineerinterviewsparkkafkagraphqlmulti-tenantdata-modeling

Shopify 数据工程师面试实录 2026:多租户数据隔离 + GraphQL 数据层 完整复盘

Shopify Data Engineer 面试真实经历:多租户数据隔离设计、GraphQL vs REST 数据层架构、商家分析管道、实时转换漏斗完整复盘。第一人称面经,含面试官对话与解题思路。

Sam · · 16 分钟阅读

公司:Shopify 岗位:Data Engineer (L4) 面试形式:Phone Screen + Virtual Onsite (4 轮) 结果:Pass → Offer

2026 年 2 月通过内推投递了 Shopify 的 Data Engineer 岗位。整个流程大约 4 周。

Shopify 的 DE 面试最独特的地方是:多租户数据隔离贯穿每一轮。 作为一个电商平台,Shopify 要管理数百万商家的数据,每个商家的数据必须严格隔离。面试中多次考察”如何在共享基础设施上保证租户间的数据隔离”这个问题。


Phone Screen:多租户 SQL 查询

题目:计算每个商家的收入(带租户隔离)

给定订单表,计算每个商家在过去 30 天的总收入、订单数和平均客单价。

-- 订单表
-- orders: order_id, merchant_id, created_at, total_price, currency, status

-- 我的解答
SELECT
    o.merchant_id,
    COUNT(*) AS order_count,
    ROUND(SUM(o.total_price), 2) AS total_revenue,
    ROUND(AVG(o.total_price), 2) AS avg_order_value,
    COUNT(DISTINCT o.customer_id) AS unique_customers
FROM orders o
WHERE o.created_at >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
  AND o.status = 'paid'
GROUP BY o.merchant_id;

面试官追问:

“如果有 500 万商家,这个查询的聚合结果怎么安全地暴露给每个商家?”

我回答:

-- 方案 1: 物化视图 + Row-Level Security
CREATE MATERIALIZED VIEW merchant_revenue_summary AS
SELECT
    merchant_id,
    DATE(created_at) AS stat_date,
    COUNT(*) AS order_count,
    ROUND(SUM(total_price), 2) AS total_revenue
FROM orders
WHERE status = 'paid'
GROUP BY merchant_id, DATE(created_at);

-- 刷新策略:每小时刷新一次
REFRESH MATERIALIZED VIEW CONCURRENTLY merchant_revenue_summary;

-- 商家查询(RLS 自动过滤)
SELECT * FROM merchant_revenue_summary
WHERE merchant_id = current_setting('app.current_merchant_id');

-- 方案 2: Spark 分区写入
-- 按 merchant_id 分区,每个商家读自己的分区
spark.conf.set("spark.sql.shuffle.partitions", "2000")

revenue_df.write \
    .partitionBy("merchant_id") \
    .format("delta") \
    .mode("overwrite") \
    .save("s3://shopify-data/revenue_by_merchant/")

面试官继续问:

“GraphQL 和 REST API 在数据层设计上有什么区别?Shopify 为什么选择 GraphQL?”

我回答:

REST API 的问题是每个 endpoint 返回固定结构,前端要么拿到多余数据(over-fetching),要么需要多次请求(under-fetching)。GraphQL 让前端精确声明需要什么字段:

# 前端精确声明需要的数据
query MerchantDashboard($merchantId: ID!, $dateRange: DateRange!) {
  merchant(id: $merchantId) {
    name
    revenue(dateRange: $dateRange) {
      total
      currency
      breakdown {
        date
        amount
        orderCount
      }
    }
    topProducts(limit: 10) {
      title
      salesCount
      revenue
    }
    customerMetrics {
      newCustomers
      repeatRate
    }
  }
}

数据层设计差异:

维度RESTGraphQL
数据粒度固定 endpoint客户端自定义
网络请求多次(N+1 问题)一次
数据聚合前端做后端 resolver 聚合
缓存HTTP 缓存需要自定义缓存层

Shopify 选择 GraphQL 的原因:前端团队可以独立迭代,不需要后端频繁发版。


VO Round 1:Spark 编码 — 商家分析管道

题目:构建商家转换漏斗分析管道

分析商家从 page_view → add_to_cart → checkout → purchase 的完整转换漏斗。

from pyspark.sql import SparkSession
from pyspark.sql.functions import col, count, when, lag, row_number, avg
from pyspark.sql.window import Window

spark = SparkSession.builder \
    .appName("Shopify_Funnel_Analysis") \
    .getOrCreate()

# ====== 漏斗分析 ======

# 定义漏斗步骤
funnel_events = events_df \
    .filter(col("event_type").isin(
        "page_view", "add_to_cart", "checkout_start", "purchase"
    )) \
    .withColumn("step",
        when(col("event_type") == "page_view", 1)
        .when(col("event_type") == "add_to_cart", 2)
        .when(col("event_type") == "checkout_start", 3)
        .when(col("event_type") == "purchase", 4)
    )

# 按商家 + 会话聚合
session_funnel = funnel_events \
    .groupBy("merchant_id", "session_id") \
    .agg(
        F.max("step").alias("max_step_reached"),
        F.collect_list("event_type").alias("events")
    )

# 计算每步转化率
funnel_counts = funnel_events \
    .groupBy("merchant_id", "step") \
    .agg(countDistinct("session_id").alias("unique_sessions"))

# 计算步骤间流失率
funnel_window = Window.partitionBy("merchant_id").orderBy("step")

funnel_rates = funnel_counts \
    .withColumn("prev_sessions",
        lag("unique_sessions").over(funnel_window)
    ) \
    .withColumn("conversion_rate",
        col("unique_sessions") * 100.0 / col("prev_sessions")
    ) \
    .filter(col("prev_sessions").isNotNull())

funnel_rates.show()
# +-----------+----+-----------------+--------------+
# |merchant_id|step|unique_sessions  |conversion_rate|
# +-----------+----+-----------------+--------------+
# |    1001   |  2 |       15000     |        45.5  |
# |    1001   |  3 |        8000     |        53.3  |
# |    1001   |  4 |        4000     |        50.0  |

题目 2:客户生命周期价值(CLV)

-- RFM 模型计算 CLV
WITH rfm AS (
    SELECT
        merchant_id,
        customer_id,
        MAX(order_date) AS last_order_date,
        DATEDIFF(CURRENT_DATE(), MAX(order_date)) AS recency_days,
        COUNT(*) AS frequency,
        SUM(total_price) AS monetary
    FROM orders
    WHERE status = 'paid'
    GROUP BY merchant_id, customer_id
),
rfm_scored AS (
    SELECT
        *,
        NTILE(5) OVER (PARTITION BY merchant_id ORDER BY recency_days DESC) AS r_score,
        NTILE(5) OVER (PARTITION BY merchant_id ORDER BY frequency) AS f_score,
        NTILE(5) OVER (PARTITION BY merchant_id ORDER BY monetary) AS m_score
    FROM rfm
)
SELECT
    merchant_id,
    customer_id,
    r_score * 2 + f_score + m_score AS clv_score,
    monetary AS lifetime_value
FROM rfm_scored
ORDER BY merchant_id, clv_score DESC;

VO Round 2:系统设计 — 多租户数据平台

题目:设计 Shopify 的商家数据平台

我的架构设计:

┌──────────────────────────────────────────────────────────────┐
│                  Shopify Data Platform (Multi-Tenant)         │
│                                                               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌─────────────┐  │
│  │ Store A  │  │ Store B  │  │ Store C  │  │  ... 5M+    │  │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └──────┬──────┘  │
│       │              │              │               │         │
│       ▼              ▼              ▼               │         │
│  ┌────────────────────────────────────────────────────────┐  │
│  │              Kafka (Tenant-Aware)                       │  │
│  │  ┌──────────────────────────────────────────────────┐  │  │
│  │  │ Topic: merchant_events                            │  │  │
│  │  │ Partition Key: merchant_id (确保同商家数据同分区)  │  │  │
│  │  └──────────────────────────────────────────────────┘  │  │
│  └──────────────────────────┬─────────────────────────────┘  │
│                             │                                │
│                             ▼                                │
│  ┌────────────────────────────────────────────────────────┐  │
│  │              Spark Streaming (Enrichment)                │  │
│  │  ┌────────────┐  ┌────────────┐  ┌──────────────────┐  │  │
│  │  │ Process    │  │ Aggregate  │  │  Write to Delta  │  │  │
│  │  │ by Tenant  │  │ by Tenant  │  │  (tenant_isolated)│  │  │
│  │  └────────────┘  └────────────┘  └──────────────────┘  │  │
│  └──────────────────────────┬─────────────────────────────┘  │
│                             │                                │
│         ┌───────────────────┼───────────────────┐            │
│         ▼                   ▼                   ▼            │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐   │
│  │ Delta Lake   │  │  Redshift    │  │  GraphQL API     │   │
│  │ (Raw Data)   │  │  (Serving)   │  │  (Dashboard)     │   │
│  │ tenant_isolated│  │ tenant_isolated│  │ tenant_isolated│   │
│  └──────────────┘  └──────────────┘  └──────────────────┘   │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │              Data Retention & Deletion                   │  │
│  │  ┌────────────┐  ┌────────────┐  ┌──────────────────┐  │  │
│  │  │ Auto-      │  │ GDPR/CCPA  │  │  Tenant Delete   │  │  │
│  │  │ Archive    │  │ Compliance │  │  Cascade         │  │  │
│  │  │ (90 days)  │  │ (Right to  │  │  (Store Closure) │  │  │
│  │  │            │  │  be        │  │                  │  │  │
│  │  │            │  │  forgotten)│  │                  │  │  │
│  │  └────────────┘  └────────────┘  └──────────────────┘  │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

租户隔离的实现方式:

# 1. Kafka: merchant_id 作为 partition key
producer.send(
    'merchant_events',
    key=merchant_id.encode(),  # 确保同商家数据在同一分区
    value=event_json
)

# 2. Delta Lake: 按 merchant_id 分区
df.write \
    .partitionBy("merchant_id", "date") \
    .format("delta") \
    .mode("append") \
    .save("s3://shopify-data/events/")

# 3. 查询时自动过滤(Row-Level Security)
def get_merchant_data(merchant_id: str) -> DataFrame:
    """商家只能读取自己的数据"""
    return spark.read \
        .format("delta") \
        .load("s3://shopify-data/events/") \
        .filter(col("merchant_id") == merchant_id)

# 4. 数据保留策略
from pyspark.sql.functions import col

# 自动归档 90 天前的数据
old_data = spark.read \
    .format("delta") \
    .load("s3://shopify-data/events/") \
    .filter(col("event_date") < F.current_date() - F.lit(90))

old_data.write \
    .mode("append") \
    .save("s3://shopify-archive/events/")

# 删除已归档数据
spark.sql("""
    DELETE FROM events
    WHERE event_date < DATE_SUB(CURRENT_DATE(), 90)
""")

# 5. 商家关闭时的级联删除
def delete_merchant_data(merchant_id: str):
    """GDPR/CCPA 合规:商家关闭时删除所有数据"""
    spark.sql(f"""
        DELETE FROM orders WHERE merchant_id = '{merchant_id}'
    """)
    spark.sql(f"""
        DELETE FROM customers WHERE merchant_id = '{merchant_id}'
    """)
    spark.sql(f"""
        DELETE FROM events WHERE merchant_id = '{merchant_id}'
    """)

面试官追问:

“如果商家数据量差异很大(有的商家每天 10 万订单,有的只有 10 单),怎么保证查询性能?”

我回答:

# 方案 1: 商家分级
# - 小商家(< 1000 orders/day):共享查询引擎
# - 中商家(1000-10000 orders/day):专用查询队列
# - 大商家(> 10000 orders/day):独立数据仓库实例

# 方案 2: 预聚合
# 对小商家,提前聚合好常用指标,查询时直接读聚合表
# 对大商家,保留原始数据 + 物化视图

# 方案 3: 缓存策略
# 热数据(最近 7 天)→ Redis
# 温数据(7-90 天)→ Redshift
# 冷数据(> 90 天)→ S3 + Athena(按需查询)

VO Round 3:行为面试

”讲一次你处理数据泄露或隐私事件的经历”

我分享了一个真实经历:之前公司的一个数据管道把用户的 PII(个人身份信息)没有脱敏就写到了分析数据库中。

处理过程:

  1. 发现后立即停止管道运行
  2. 评估影响范围——哪些数据被泄露、影响了多少用户
  3. 紧急清理——从分析库中删除 PII 数据
  4. 根本原因——schema 定义中没有 sensitive 标记
  5. 长期修复——加了自动 PII 检测和脱敏 pipeline

”你怎么和产品团队在数据功能上有分歧?”

我分享了关于”是否要追踪用户点击热图”的分歧——产品团队想要尽可能多的数据,但我从数据成本和用户隐私角度建议只追踪关键事件。最终我们达成妥协:只追踪核心转化路径上的事件,并做了用户同意管理。


面试总结

成功经验

  1. 多租户思维:从电话面到 system design,每一轮都围绕”租户隔离”展开
  2. GraphQL 理解:Shopify 大量使用 GraphQL,了解其数据层设计是加分项
  3. 数据合规意识:GDPR/CCPA 合规的数据处理流程是面试中的高频话题

注意事项

  1. 商家规模差异:设计时要考虑从 10 单/天到 10 万单/天的商家
  2. 数据保留策略:知道怎么设计自动归档和删除机制
  3. 隐私第一:Shopify 非常重视用户隐私,面试中会考察你的隐私保护意识

推荐阅读


💡 需要面试辅导?

联系我们

准备好拿下下一次面试了吗?

获取针对你的目标岗位和公司的个性化辅导方案。

联系我们